yapb-noob-edition/source/basecode.cpp
jeefo 5f6a1638d6
2.9 Update (#64)
* Fixed bots not camping in camp spots.
Fixed chatter/radio message cycling. (need feedback).
Fixed CTs unable to defuse bomb.
Fixed backward jump path generation in waypoint editor.
Fixed autoradius in waypoint editor.
Fixed autoradius menu non closeable.
Fixed bots version display on entering game.
Fixed memory leak in DLL-loader. (non metamod).
Fixed bots able to see through smoke.
Fixed team-detection on non-standard modes.
Fixed quota & autovacate management.
Fixed bunch of warnings from static analyzers.
Greatly imporoved grenade throwing.
Grealty reduced bot CPU usage.

* Fixed stack-corruption in memory-file reader.
Fixed A* pathfinder not working correctly.
Fixed 'Tried to write to uninitialized sizebuf_t error' on bot add/remove.
Minor tweaks to camping and bot enemy aiming

* Make clang happy.

* Fixed VIP-dection on some maps.
Fixed occupied waypoint checker.
Small refactoring of code with clang-format.

* Fixed clang compilation

* Fixed compilation.

* Debugging seek cover task.
Some more code cleanup.

* Fixed typos.

* Fixes to attack movement.
Revert Z component updates.

* Fixes for aiming at enemy.
Fixes for seek cover & enemy hunt tasks.
More refactoring.

* Making clang happy once again?
Tweaked grenade timers.

* Revised language comparer hasher

* Fixed build.

* Fixed build.

* Optimized headshot offsets.
Optimized aim errors and enemy searches.
Get rid of preprocessor macroses.
Added back yb_think_fps. Use with caution.

* Minor refactoring of code.

* Check if tracking entity is still alive.
Do not duck in crouch-goal waypoints.
Remove ancient hack with failed goals.

* Get rid of c++14 stuff.
Tweaked isOccupiedPoint.

* Changed pickup check radius.

* Fix compilation.

* Fixed bots ignore breakables.
Fixed A* pathfinder.
Fixed searching for optimal waypoints.
Fixed bot waypoint reachability functions.

* Get rid of new/delete calls in pathfinder.
Disallow access to yapb waypoint menu on hlds.
Minor refactoring.

* Updated linux/osx makefile

* Spaces -> Tabs in makefile.
Made G++ happy.

* Updated makefile.

* Fixed heap buffer overflow in config loader code.

* Lowered CPU usage a bit, by using "waypoint buckets" for searching closest node.
Do not traceline for doors on map, that have no doors.
Get rid stack-based containers.

* Remove win-only debug crap.

* Refactored string class.

* Fix OSX compiling.

* Minor refactoring of corelib to use cpp move-semantic.

* Use reference for active grenades searcher.

* Use system's atan2f () as it's eror rate is a bit lower.
Fixed bots continuously stays in throw smoke task.
Fixed bots reaching camp-goal jumping or stays they for some time.
Increased radius for searching targets for grenades.
Tweaked bot difficulty levels.
Improved sniper weapon handling. Trying to stand still while shooting.
Increase retreat level only if sniper weapon is low on ammo.
Fixed predict path enemy tracking timer is always true.
Allow bots to process their tasks while on freezetime, so on small maps they already aiming enemies when freezetime ends.
Fied bots endlessy trying to pickup weapons.
Reduce surpise timers when holding sniper weapons.
New aim-at-head position calculation.
Shoot delay timers are now based on bot's difficulty.
Prefer smoke grenades more than flashbangs.
Fixed kill-all bot command not killing one random bot for first time use.
Do not play with jump velocity, now using the same as in waypoints.
Tweaked shift move, so zero move speed not overriden with shift speed.
Radius waypoint searcher use waypoint bucket as well.
Increase reachability radius for dest waypoint, if it's  currenlty owned by other bot.
Partially fixed bots choice to use unreachable waypoints.

* Makes OSX clang happy?

* Support for compiling on llvm-win32, makefile to be done.
Increased default reachability time.

* Fixed build.

* Move level-initialization stuff from Spawn to ServerActivate, so bot will not check init-stuff every entity spawn. This should save few CPU cycles.

* Fixed active grenades list not working after changelevel.
Reworked items pickup code, so every bot is not firing sphere search every time, but instead we maintain our own list of intresting entities, so every bot is accessing this list. This should lower CPU usage more a little.

* Precache should be done in spawn...

* Do not use engfuncs in intresting entities.

* Fixed GCC-8.2 warnings.
Minor refactoring.

* Added some safety checks to intresting entities.
Get rid of stdc++ dependency for GCC & ICC under linux.

* Remove -g from release make.
Cosmetic changes.

* Re-enabled debug overlay.

* Remove test header...

* Some static-analyzer warnings fixed.
Support for X64 build for FWGS Xash3D Engine.

* Reduced time between selecting grenade and throwing it away.
Do not try to kill bots that already dead with kill command.
Several fixes from static-analyzers.

* Update CI.

* Fixed bot's not added after the changelevel on Xash3D engine.

* Revert commit that enables movement during freezetime. Everything goes bad, when there is no freezetime....

* Bots will try to  not strafe while in combat if seeing enemy only partially.
Do not use "shift" when considering stuck.

* Weapon price for Elite is 800$ since CS 1.6...

* Fixed bots at difficulty 0 can't shoot enemies.

* Cosmetic change.

* Fixed assert in ClientDisconnect when quitting game while meta unloaded yapb module.
Consider freed entities as invalid.

* Bigger distance for throwing he grenades.

* Faster version of atan2f().

* Removed accidentally left SSE header.

* Cosmetic changes to enums.

* Tweaked difficulty levels.
Bots on Android will have a difficulty level 2 by default.
Fixed LTO builds under linux.

* Do not consider Android CS as legacy.

* Get rid of system's math functions. Just for fun)

* Use SSE2 for sincos function.

* Fixed failed during load wayponts still allows to add bots, thus causing bot to crash.
Added ability to delete waypoint by number using "yb wp delete".
Enabled Link Time Optimization for Linux and OSX.

* Fixed CI Builds.
2018-10-28 19:26:36 +03:00

5867 lines
187 KiB
C++

//
// Yet Another POD-Bot, based on PODBot by Markus Klinge ("CountFloyd").
// Copyright (c) YaPB Development Team.
//
// This software is licensed under the BSD-style license.
// Additional exceptions apply. For full license details, see LICENSE.txt or visit:
// https://yapb.ru/license
//
#include <yapb.h>
ConVar yb_debug ("yb_debug", "0");
ConVar yb_debug_goal ("yb_debug_goal", "-1");
ConVar yb_user_follow_percent ("yb_user_follow_percent", "20");
ConVar yb_user_max_followers ("yb_user_max_followers", "1");
ConVar yb_jasonmode ("yb_jasonmode", "0");
ConVar yb_communication_type ("yb_communication_type", "2");
ConVar yb_economics_rounds ("yb_economics_rounds", "1");
ConVar yb_walking_allowed ("yb_walking_allowed", "1");
ConVar yb_camping_allowed ("yb_camping_allowed", "1");
ConVar yb_tkpunish ("yb_tkpunish", "1");
ConVar yb_freeze_bots ("yb_freeze_bots", "0");
ConVar yb_spraypaints ("yb_spraypaints", "1");
ConVar yb_botbuy ("yb_botbuy", "1");
ConVar yb_chatter_path ("yb_chatter_path", "sound/radio/bot");
ConVar yb_restricted_weapons ("yb_restricted_weapons", "");
ConVar yb_best_weapon_picker_type ("yb_best_weapon_picker_type", "1");
// game console variables
ConVar mp_c4timer ("mp_c4timer", nullptr, VT_NOREGISTER);
ConVar mp_buytime ("mp_buytime", nullptr, VT_NOREGISTER, true, "1");
ConVar mp_footsteps ("mp_footsteps", nullptr, VT_NOREGISTER);
ConVar sv_gravity ("sv_gravity", nullptr, VT_NOREGISTER);
int Bot::getMsgQueue (void) {
// 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 == GAME_MSG_SAY_CMD) {
// 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 (int i = 0; i < engine.maxClients (); i++) {
Bot *otherBot = bots.getBot (i);
if (otherBot != nullptr && otherBot->pev != pev) {
if (m_notKilled == otherBot->m_notKilled) {
otherBot->m_sayTextBuffer.entityIndex = entityIndex;
otherBot->m_sayTextBuffer.sayText = m_tempStrings;
}
otherBot->m_sayTextBuffer.timeNextChat = engine.timebase () + otherBot->m_sayTextBuffer.chatDelay;
}
}
}
m_messageQueue[m_pushMessageIndex++] = message;
m_pushMessageIndex &= 0x1f; // wraparound
}
float Bot::isInFOV (const Vector &destination) {
float entityAngle = cr::angleMod (destination.toYaw ()); // find yaw angle from source to destination...
float viewAngle = cr::angleMod (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 ::isInViewCone (origin, ent ());
}
bool Bot::seesItem (const Vector &destination, const char *itemName) {
TraceResult tr;
// trace a line from bot's eyes to destination..
engine.testLine (eyePos (), destination, TRACE_IGNORE_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...
engine.testLine (fromBody ? pev->origin : eyePos (), dest, TRACE_IGNORE_EVERYTHING, ent (), &tr);
// check if line of sight to object is not blocked (i.e. visible)
return tr.flFraction >= 1.0f;
}
void Bot::checkGrenadesThrow (void) {
// do not check cancel if we have grenade in out hands
bool checkTasks = taskId () == TASK_PLANTBOMB || taskId () == TASK_DEFUSEBOMB;
auto clearThrowStates = [] (unsigned int &states) {
states &= ~(STATE_THROW_HE | STATE_THROW_FB | STATE_THROW_SG);
};
// check if throwing a grenade is a good thing to do...
if (checkTasks || yb_ignore_enemies.boolean () || m_isUsingGrenade || m_grenadeRequested || m_isReloading || yb_jasonmode.boolean () || m_grenadeCheckTime >= engine.timebase ()) {
clearThrowStates (m_states);
return;
}
// check again in some seconds
m_grenadeCheckTime = engine.timebase () + 0.5f;
if (!isAlive (m_lastEnemy) || !(m_states & (STATE_SUSPECT_ENEMY | STATE_HEARING_ENEMY))) {
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 = engine.timebase () + 15.0f; // changed since, conzero can drop grens from dead players
clearThrowStates (m_states);
return;
}
else {
int cancelProb = 20;
if (grenadeToThrow == WEAPON_FLASHBANG) {
cancelProb = 10;
}
else if (grenadeToThrow == WEAPON_SMOKE) {
cancelProb = 5;
}
if (rng.getInt (0, 100) < 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 = 9999.0f;
}
// too high to throw?
if (m_lastEnemy->v.origin.z > pev->origin.z + 500.0f) {
distance = 9999.0f;
}
// 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).make2D () + m_lastEnemy->v.origin;
if (radius < 164.0f) {
radius = 164.0f;
}
auto predicted = waypoints.searchRadius (radius, pos, 12);
if (predicted.empty ()) {
m_states &= ~STATE_THROW_HE;
break;
}
for (const auto predict : predicted) {
allowThrowing = true;
if (!waypoints.exists (predict)) {
allowThrowing = false;
continue;
}
m_throw = waypoints[predict].origin;
auto throwPos = calcThrow (eyePos (), m_throw);
if (throwPos.lengthSq () < 100.0f) {
throwPos = calcToss (eyePos (), m_throw);
}
if (throwPos.empty ()) {
allowThrowing = false;
}
else {
m_throw.z += 110.0f;
break;
}
}
}
if (allowThrowing) {
m_states |= STATE_THROW_HE;
}
else {
m_states &= ~STATE_THROW_HE;
}
break;
case WEAPON_FLASHBANG: {
int nearest = waypoints.getNearest ((m_lastEnemy->v.velocity * 0.5f).make2D () + m_lastEnemy->v.origin);
if (nearest != INVALID_WAYPOINT_INDEX) {
m_throw = waypoints[nearest].origin;
if (numFriendsNear (m_throw, 256.0f) > 0) {
allowThrowing = false;
}
}
else {
allowThrowing = false;
}
if (allowThrowing) {
auto throwPos = calcThrow (eyePos (), m_throw);
if (throwPos.lengthSq () < 100.0f) {
throwPos = calcToss (eyePos (), m_throw);
}
if (throwPos.empty ()) {
allowThrowing = false;
}
else {
m_throw.z += 110.0f;
}
}
if (allowThrowing) {
m_states |= STATE_THROW_FB;
}
else {
m_states &= ~STATE_THROW_FB;
}
break;
}
case WEAPON_SMOKE:
if (allowThrowing && !engine.isNullEntity (m_lastEnemy)) {
if (getShootingConeDeviation (m_lastEnemy, pev->origin) >= 0.9f) {
allowThrowing = false;
}
}
if (allowThrowing) {
m_states |= STATE_THROW_SG;
}
else {
m_states &= ~STATE_THROW_SG;
}
break;
}
const float MaxThrowTime = engine.timebase () + 0.3f;
if (m_states & STATE_THROW_HE) {
startTask (TASK_THROWHEGRENADE, TASKPRI_THROWGRENADE, INVALID_WAYPOINT_INDEX, MaxThrowTime, false);
}
else if (m_states & STATE_THROW_FB) {
startTask (TASK_THROWFLASHBANG, TASKPRI_THROWGRENADE, INVALID_WAYPOINT_INDEX, MaxThrowTime, false);
}
else if (m_states & STATE_THROW_SG) {
startTask (TASK_THROWSMOKE, TASKPRI_THROWGRENADE, INVALID_WAYPOINT_INDEX, MaxThrowTime, false);
}
}
else {
clearThrowStates (m_states);
}
}
void Bot::avoidGrenades (void) {
// checks if bot 'sees' a grenade, and avoid it
if (!bots.hasActiveGrenades ()) {
return;
}
// check if old pointers to grenade is invalid
if (engine.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 - eyePos ()) > pev->fov * 0.5f) {
continue;
}
auto model = STRING (pent->v.model) + 9;
if (m_turnAwayFromFlashbang < engine.timebase () && m_personality == PERSONALITY_RUSHER && m_difficulty == 4 && strcmp (model, "flashbang.mdl") == 0) {
// don't look at flash bang
if (!(m_states & STATE_SEEING_ENEMY)) {
pev->v_angle.y = cr::angleNorm ((engine.getAbsPos (pent) - eyePos ()).toAngles ().y + 180.0f);
m_canChooseAimDirection = false;
m_turnAwayFromFlashbang = engine.timebase () + rng.getFloat (1.0f, 2.0f);
}
}
else if (strcmp (model, "hegrenade.mdl") == 0) {
if (!engine.isNullEntity (m_avoidGrenade)) {
return;
}
if (engine.getTeam (pent->v.owner) == m_team || pent->v.owner == ent ()) {
return;
}
if (!(pent->v.flags & FL_ONGROUND)) {
float distance = (pent->v.origin - pev->origin).length ();
float distanceMoved = ((pent->v.origin + pent->v.velocity * calcThinkInterval ()) - pev->origin).length ();
if (distanceMoved < distance && distance < 500.0f) {
makeVectors (pev->v_angle);
const Vector &dirToPoint = (pev->origin - pent->v.origin).normalize2D ();
const Vector &rightSide = g_pGlobals->v_right.normalize2D ();
if ((dirToPoint | rightSide) > 0.0f) {
m_needAvoidGrenade = -1;
}
else {
m_needAvoidGrenade = 1;
}
m_avoidGrenade = pent;
}
}
}
else if ((pent->v.flags & FL_ONGROUND) == 0 && strcmp (model, "smokegrenade.mdl") == 0) {
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 (rng.getInt (0, 100) < 45) {
pushChatterMessage (CHATTER_BEHIND_SMOKE);
}
}
}
}
}
int Bot::bestPrimaryCarried (void) {
// this function returns the best weapon of this bot (based on personality prefs)
int *ptr = g_weaponPrefs[m_personality];
int weaponIndex = 0;
int weapons = pev->weapons;
WeaponSelect *weaponTab = &g_weaponSelect[0];
// take the shield in account
if (hasShield ()) {
weapons |= (1 << WEAPON_SHIELD);
}
for (int i = 0; i < NUM_WEAPONS; i++) {
if (weapons & (1 << weaponTab[*ptr].id)) {
weaponIndex = i;
}
ptr++;
}
return weaponIndex;
}
int Bot::bestSecondaryCarried (void) {
// this function returns the best secondary weapon of this bot (based on personality prefs)
int *ptr = g_weaponPrefs[m_personality];
int weaponIndex = 0;
int weapons = pev->weapons;
// take the shield in account
if (hasShield ()) {
weapons |= (1 << WEAPON_SHIELD);
}
WeaponSelect *weaponTab = &g_weaponSelect[0];
for (int i = 0; i < NUM_WEAPONS; i++) {
int id = weaponTab[*ptr].id;
if ((weapons & (1 << weaponTab[*ptr].id)) && (id == WEAPON_USP || id == WEAPON_GLOCK || id == WEAPON_DEAGLE || id == WEAPON_P228 || id == WEAPON_ELITE || id == WEAPON_FIVESEVEN)) {
weaponIndex = i;
break;
}
ptr++;
}
return weaponIndex;
}
bool Bot::rateGroundWeapon (edict_t *ent) {
// this function compares weapons on the ground to the one the bot is using
int hasWeapon = 0;
int groundIndex = 0;
int *ptr = g_weaponPrefs[m_personality];
WeaponSelect *weaponTab = &g_weaponSelect[0];
for (int i = 0; i < NUM_WEAPONS; i++) {
if (strcmp (weaponTab[*ptr].modelName, STRING (ent->v.model) + 9) == 0) {
groundIndex = i;
break;
}
ptr++;
}
if (groundIndex < 7) {
hasWeapon = bestSecondaryCarried ();
}
else {
hasWeapon = bestPrimaryCarried ();
}
return groundIndex > hasWeapon;
}
void Bot::processBreakables (edict_t *touch) {
if (!isShootableBreakable (touch)) {
return;
}
m_breakableEntity = lookupBreakable ();
if (engine.isNullEntity (m_breakableEntity)) {
return;
}
m_campButtons = pev->button & IN_DUCK;
startTask (TASK_SHOOTBREAKABLE, TASKPRI_SHOOTBREAKABLE, INVALID_WAYPOINT_INDEX, 0.0f, false);
}
edict_t *Bot::lookupBreakable (void) {
// this function checks if bot is blocked by a shoot able breakable in his moving direction
TraceResult tr;
engine.testLine (pev->origin, pev->origin + (m_destOrigin - pev->origin).normalize () * 72.0f, TRACE_IGNORE_NONE, ent (), &tr);
if (tr.flFraction != 1.0f) {
edict_t *ent = tr.pHit;
// check if this isn't a triggered (bomb) breakable and if it takes damage. if true, shoot the crap!
if (isShootableBreakable (ent)) {
m_breakableOrigin = engine.getAbsPos (ent);
return ent;
}
}
engine.testLine (eyePos (), eyePos () + (m_destOrigin - eyePos ()).normalize () * 72.0f, TRACE_IGNORE_NONE, ent (), &tr);
if (tr.flFraction != 1.0f) {
edict_t *ent = tr.pHit;
if (isShootableBreakable (ent)) {
m_breakableOrigin = engine.getAbsPos (ent);
return ent;
}
}
m_breakableEntity = nullptr;
m_breakableOrigin.nullify ();
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 = rng.getFloat (reaction.min, reaction.max);
}
void Bot::processPickups (void) {
// 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 () || taskId () == TASK_ESCAPEFROMBOMB || yb_jasonmode.boolean () || !bots.hasIntrestingEntities ()) {
m_pickupItem = nullptr;
m_pickupType = PICKUP_NONE;
return;
}
auto &intresting = bots.searchIntrestingEntities ();
Bot *bot = nullptr;
constexpr float radius = cr::square (320.0f);
if (!engine.isNullEntity (m_pickupItem)) {
bool itemExists = false;
auto pickupItem = m_pickupItem;
for (auto ent : intresting) {
if (isPlayer (ent->v.owner)) {
continue; // someone owns this weapon or it hasn't re spawned yet
}
const Vector &origin = engine.getAbsPos (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;
PickupType pickupType = PICKUP_NONE;
Vector pickupPos = Vector::null ();
m_pickupItem = nullptr;
m_pickupType = PICKUP_NONE;
for (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 = engine.getAbsPos (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_DROPPED_C4;
}
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_COUNTER && !m_hasDefuser) {
allowPickup = true;
pickupType = PICKUP_DEFUSEKIT;
}
else if (strncmp ("grenade", classname, 7) == 0 && strcmp (model, "c4.mdl") == 0) {
allowPickup = true;
pickupType = PICKUP_PLANTED_C4;
}
}
// if the bot found something it can pickup...
if (allowPickup) {
if (pickupType == PICKUP_WEAPON) // found weapon on ground?
{
int weaponCarried = bestPrimaryCarried ();
int secondaryWeaponCarried = bestSecondaryCarried ();
if (secondaryWeaponCarried < 7 && (m_ammo[g_weaponSelect[secondaryWeaponCarried].id] > 0.3 * g_weaponDefs[g_weaponSelect[secondaryWeaponCarried].id].ammo1Max) && strcmp (model, "w_357ammobox.mdl") == 0) {
allowPickup = false;
}
else if (!m_isVIP && weaponCarried >= 7 && (m_ammo[g_weaponSelect[weaponCarried].id] > 0.3 * g_weaponDefs[g_weaponSelect[weaponCarried].id].ammo1Max) && strncmp (model, "w_", 2) == 0) {
bool isSniperRifle = weaponCarried == WEAPON_AWP || weaponCarried == WEAPON_G3SG1 || weaponCarried == WEAPON_SG550;
bool isSubmachine = weaponCarried == WEAPON_MP5 || weaponCarried == WEAPON_TMP || weaponCarried == WEAPON_P90 || weaponCarried == WEAPON_MAC10 || weaponCarried == WEAPON_UMP45;
bool isShotgun = weaponCarried == WEAPON_M3;
bool isRifle = weaponCarried == WEAPON_FAMAS || weaponCarried == WEAPON_AK47 || weaponCarried == WEAPON_M4A1 || weaponCarried == WEAPON_GALIL || weaponCarried == WEAPON_AUG || weaponCarried == 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 && weaponCarried != 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 & (1 << WEAPON_FLASHBANG))) {
allowPickup = false;
}
else if (strcmp (model, "hegrenade.mdl") == 0 && (pev->weapons & (1 << WEAPON_EXPLOSIVE))) {
allowPickup = false;
}
else if (strcmp (model, "smokegrenade.mdl") == 0 && (pev->weapons & (1 << WEAPON_SMOKE))) {
allowPickup = false;
}
}
else if (pickupType == PICKUP_SHIELD) // found a shield on ground?
{
if ((pev->weapons & (1 << WEAPON_ELITE)) || hasShield () || m_isVIP || (hasPrimaryWeapon () && !rateGroundWeapon (ent))) {
allowPickup = false;
}
}
else if (m_team == TEAM_TERRORIST) // terrorist team specific
{
if (pickupType == PICKUP_DROPPED_C4) {
allowPickup = true;
m_destOrigin = origin; // ensure we reached dropped bomb
pushChatterMessage (CHATTER_FOUND_BOMB); // play info about that
clearSearchNodes ();
}
else if (pickupType == PICKUP_HOSTAGE) {
m_itemIgnore = ent;
allowPickup = false;
if (!m_defendHostage && m_difficulty > 2 && rng.getInt (0, 100) < 30 && m_timeCamping + 15.0f < engine.timebase ()) {
int index = getDefendPoint (origin);
startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, engine.timebase () + rng.getFloat (30.0f, 60.0f), true); // push camp task on to stack
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, index, engine.timebase () + rng.getFloat (3.0f, 6.0f), true); // push move command
if (waypoints[index].vis.crouch <= waypoints[index].vis.stand) {
m_campButtons |= IN_DUCK;
}
else {
m_campButtons &= ~IN_DUCK;
}
m_defendHostage = true;
pushChatterMessage (CHATTER_GOING_TO_GUARD_HOSTAGES); // play info about that
return;
}
}
else if (pickupType == PICKUP_PLANTED_C4) {
allowPickup = false;
if (!m_defendedBomb) {
m_defendedBomb = true;
int index = getDefendPoint (origin);
Path &path = waypoints[index];
float bombTimer = mp_c4timer.flt ();
float timeMidBlowup = g_timeBombPlanted + (bombTimer * 0.5f + bombTimer * 0.25f) - waypoints.calculateTravelTime (pev->maxspeed, pev->origin, path.origin);
if (timeMidBlowup > engine.timebase ()) {
clearTask (TASK_MOVETOPOSITION); // remove any move tasks
startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, 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 (rng.getInt (0, 100) < 90) {
pushChatterMessage (CHATTER_DEFENDING_BOMBSITE);
}
}
else {
pushRadioMessage (RADIO_SHES_GONNA_BLOW); // issue an additional radio message
}
}
}
}
else if (m_team == TEAM_COUNTER) {
if (pickupType == PICKUP_HOSTAGE) {
if (engine.isNullEntity (ent) || ent->v.health <= 0) {
allowPickup = false; // never pickup dead hostage
}
else
for (int i = 0; i < engine.maxClients (); i++) {
if ((bot = bots.getBot (i)) != nullptr && bot->m_notKilled) {
for (auto hostage : bot->m_hostages) {
if (hostage == ent) {
allowPickup = false;
break;
}
}
}
}
}
else if (pickupType == PICKUP_PLANTED_C4) {
if (isPlayer (m_enemy)) {
allowPickup = false;
return;
}
if (isOutOfBombTimer ()) {
allowPickup = false;
return;
}
if (rng.getInt (0, 100) < 90) {
pushChatterMessage (CHATTER_FOUND_BOMB_PLACE);
}
allowPickup = !isBombDefusing (origin) || m_hasProgressBar;
pickupType = PICKUP_PLANTED_C4;
if (!m_defendedBomb && !allowPickup) {
m_defendedBomb = true;
int index = getDefendPoint (origin);
Path &path = waypoints[index];
float timeToExplode = g_timeBombPlanted + mp_c4timer.flt () - waypoints.calculateTravelTime (pev->maxspeed, pev->origin, path.origin);
clearTask (TASK_MOVETOPOSITION); // remove any move tasks
startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, 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 (rng.getInt (0, 100) < 90) {
pushChatterMessage (CHATTER_DEFENDING_BOMBSITE);
}
}
}
else if (pickupType == PICKUP_DROPPED_C4) {
m_itemIgnore = ent;
allowPickup = false;
if (!m_defendedBomb && m_difficulty > 2 && rng.getInt (0, 100) < 75 && pev->health < 80) {
int index = getDefendPoint (origin);
startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, engine.timebase () + rng.getFloat (30.0f, 70.0f), true); // push camp task on to stack
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, index, engine.timebase () + rng.getFloat (10.0f, 30.0f), true); // push move command
if (waypoints[index].vis.crouch <= waypoints[index].vis.stand) {
m_campButtons |= IN_DUCK;
}
else {
m_campButtons &= ~IN_DUCK;
}
m_defendedBomb = true;
pushChatterMessage (CHATTER_GOING_TO_GUARD_DROPPED_BOMB); // 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 (!engine.isNullEntity (pickupItem)) {
for (int i = 0; i < engine.maxClients (); i++) {
if ((bot = bots.getBot (i)) != nullptr && bot->m_notKilled && bot->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 > eyePos ().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::getCampDir (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 = eyePos ();
engine.testLine (src, *dest, TRACE_IGNORE_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 = waypoints.getNearest (*dest);
int tempIndex = waypoints.getNearest (pev->origin);
if (tempIndex == INVALID_WAYPOINT_INDEX || enemyIndex == INVALID_WAYPOINT_INDEX) {
return;
}
float minDistance = 99999.0f;
int lookAtWaypoint = INVALID_WAYPOINT_INDEX;
Path &path = waypoints[tempIndex];
for (int i = 0; i < MAX_PATH_INDEX; i++) {
if (path.index[i] == INVALID_WAYPOINT_INDEX) {
continue;
}
float distance = static_cast <float> (waypoints.getPathDist (path.index[i], enemyIndex));
if (distance < minDistance) {
minDistance = distance;
lookAtWaypoint = path.index[i];
}
}
if (waypoints.exists (lookAtWaypoint)) {
*dest = waypoints[lookAtWaypoint].origin;
}
}
}
void Bot::showChaterIcon (bool show) {
// this function depending on show boolen, shows/remove chatter, icon, on the head of bot.
if (!(g_gameFlags & GAME_SUPPORT_BOT_VOICE) || yb_communication_type.integer () != 2) {
return;
}
auto sendBotVoice = [](bool show, edict_t *ent, int ownId) {
MessageWriter (MSG_ONE, engine.getMessageId (NETMSG_BOTVOICE), Vector::null (), ent) // begin message
.writeByte (show) // switch on/off
.writeByte (ownId);
};
int ownId = index ();
for (int i = 0; i < engine.maxClients (); i++) {
Client &client = g_clients[i];
if (!(client.flags & CF_USED) || (client.ent->v.flags & FL_FAKECLIENT) || client.team != m_team) {
continue;
}
if (!show && (client.iconFlags[ownId] & CF_ICON) && client.iconTimestamp[ownId] < engine.timebase ()) {
sendBotVoice (false, client.ent, ownId);
client.iconTimestamp[ownId] = 0.0f;
client.iconFlags[ownId] &= ~CF_ICON;
}
else if (show && !(client.iconFlags[ownId] & CF_ICON)) {
sendBotVoice (true, client.ent, ownId);
}
}
}
void Bot::instantChatter (int type) {
// this function sends instant chatter messages.
if (!(g_gameFlags & GAME_SUPPORT_BOT_VOICE) || yb_communication_type.integer () != 2 || g_chatterFactory[type].empty ()) {
return;
}
// delay only report team
if (type == RADIO_REPORT_TEAM) {
if (m_timeRepotingInDelay < engine.timebase ()) {
return;
}
m_timeRepotingInDelay = engine.timebase () + rng.getFloat (30.0f, 60.0f);
}
auto playbackSound = g_chatterFactory[type].random ();
auto painSound = g_chatterFactory[CHATTER_PAIN_DIED].random ();
if (m_notKilled) {
showChaterIcon (true);
}
MessageWriter msg;
for (int i = 0; i < engine.maxClients (); i++) {
Client &client = g_clients[i];
if (!(client.flags & CF_USED) || (client.ent->v.flags & FL_FAKECLIENT) || client.team != m_team) {
continue;
}
msg.start (MSG_ONE, engine.getMessageId (NETMSG_SENDAUDIO), Vector::null (), client.ent) // begin message
.writeByte (index ());
if (pev->deadflag & DEAD_DYING) {
client.iconTimestamp[index ()] = engine.timebase () + painSound.duration;
msg.writeString (format ("%s/%s.wav", yb_chatter_path.str (), painSound.name.chars ()));
}
else if (!(pev->deadflag & DEAD_DEAD)) {
client.iconTimestamp[index ()] = engine.timebase () + playbackSound.duration;
msg.writeString (format ("%s/%s.wav", yb_chatter_path.str (), playbackSound.name.chars ()));
}
msg.writeShort (m_voicePitch).end ();
client.iconFlags[index ()] |= CF_ICON;
}
}
void Bot::pushRadioMessage (int message) {
// this function inserts the radio message into the message queue
if (yb_communication_type.integer () == 0 || m_numFriendsLeft == 0) {
return;
}
if (!(g_gameFlags & GAME_SUPPORT_BOT_VOICE) || g_chatterFactory[message].empty () || yb_communication_type.integer () != 2) {
m_forceRadio = true; // use radio instead voice
}
else {
m_forceRadio = false;
}
m_radioSelect = message;
pushMsgQueue (GAME_MSG_RADIO);
}
void Bot::pushChatterMessage (int message) {
// this function inserts the voice message into the message queue (mostly same as above)
if (!(g_gameFlags & GAME_SUPPORT_BOT_VOICE) || yb_communication_type.integer () != 2 || g_chatterFactory[message].empty () || m_numFriendsLeft == 0) {
return;
}
bool sendMessage = false;
float &messageTimer = m_chatterTimes[message];
float &messageRepeat = g_chatterFactory[message][0].repeat;
if (messageTimer < engine.timebase () || cr::fequal (messageTimer, MAX_CHATTER_REPEAT)) {
if (!cr::fequal (messageTimer, MAX_CHATTER_REPEAT) && !cr::fequal (messageRepeat, MAX_CHATTER_REPEAT)) {
messageTimer = engine.timebase () + messageRepeat;
}
sendMessage = true;
}
if (!sendMessage) {
return;
}
m_radioSelect = message;
pushMsgQueue (GAME_MSG_RADIO);
}
void Bot::checkMsgQueue (void) {
// this function checks and executes pending messages
// no new message?
if (m_actMessageIndex == m_pushMessageIndex) {
return;
}
// get message from stack
int state = getMsgQueue ();
// nothing to do?
if (state == GAME_MSG_NONE || (state == GAME_MSG_RADIO && (g_gameFlags & GAME_CSDM_FFA))) {
return;
}
switch (state) {
case GAME_MSG_PURCHASE: // general buy message
// buy weapon
if (m_nextBuyTime > engine.timebase ()) {
// keep sending message
pushMsgQueue (GAME_MSG_PURCHASE);
return;
}
if (!m_inBuyZone || (g_gameFlags & GAME_CSDM)) {
m_buyPending = true;
m_buyingFinished = true;
break;
}
m_buyPending = false;
m_nextBuyTime = engine.timebase () + rng.getFloat (0.5f, 1.3f);
// if bot buying is off then no need to buy
if (!yb_botbuy.boolean ()) {
m_buyState = BUYSTATE_FINISHED;
}
// if fun-mode no need to buy
if (yb_jasonmode.boolean ()) {
m_buyState = BUYSTATE_FINISHED;
selectWeaponByName ("weapon_knife");
}
// prevent vip from buying
if (m_isVIP) {
m_buyState = BUYSTATE_FINISHED;
m_pathType = SEARCH_PATH_FASTEST;
}
// prevent terrorists from buying on es maps
if ((g_mapFlags & MAP_ES) && m_team == TEAM_TERRORIST) {
m_buyState = 6;
}
// prevent teams from buying on fun maps
if (g_mapFlags & (MAP_KA | MAP_FY)) {
m_buyState = BUYSTATE_FINISHED;
if (g_mapFlags & MAP_KA) {
yb_jasonmode.set (1);
}
}
if (m_buyState > BUYSTATE_FINISHED - 1) {
m_buyingFinished = true;
return;
}
pushMsgQueue (GAME_MSG_NONE);
buyStuff ();
break;
case GAME_MSG_RADIO:
// if last bot radio command (global) happened just a 3 seconds ago, delay response
if (g_lastRadioTime[m_team] + 3.0f < engine.timebase ()) {
// if same message like previous just do a yes/no
if (m_radioSelect != RADIO_AFFIRMATIVE && m_radioSelect != RADIO_NEGATIVE) {
if (m_radioSelect == g_lastRadio[m_team] && g_lastRadioTime[m_team] + 1.5f > engine.timebase ())
m_radioSelect = -1;
else {
if (m_radioSelect != RADIO_REPORTING_IN) {
g_lastRadio[m_team] = m_radioSelect;
}
else {
g_lastRadio[m_team] = -1;
}
for (int i = 0; i < engine.maxClients (); i++) {
Bot *bot = bots.getBot (i);
if (bot != nullptr) {
if (pev != bot->pev && bot->m_team == m_team) {
bot->m_radioOrder = m_radioSelect;
bot->m_radioEntity = ent ();
}
}
}
}
}
if (m_radioSelect == RADIO_REPORTING_IN) {
switch (taskId ()) {
case TASK_NORMAL:
if (task ()->data != INVALID_WAYPOINT_INDEX && rng.getInt (0, 100) < 70) {
Path &path = waypoints[task ()->data];
if (path.flags & FLAG_GOAL) {
if ((g_mapFlags & MAP_DE) && m_team == TEAM_TERRORIST && m_hasC4) {
instantChatter (CHATTER_GOING_TO_PLANT_BOMB);
}
else {
instantChatter (CHATTER_NOTHING);
}
}
else if (path.flags & FLAG_RESCUE) {
instantChatter (CHATTER_RESCUING_HOSTAGES);
}
else if ((path.flags & FLAG_CAMP) && rng.getInt (0, 100) > 15) {
instantChatter (CHATTER_GOING_TO_CAMP);
}
else {
instantChatter (CHATTER_HEARD_NOISE);
}
}
else if (rng.getInt (0, 100) < 30) {
instantChatter (CHATTER_REPORTING_IN);
}
break;
case TASK_MOVETOPOSITION:
if (rng.getInt (0, 100) < 20) {
instantChatter (CHATTER_GOING_TO_CAMP);
}
break;
case TASK_CAMP:
if (rng.getInt (0, 100) < 40) {
if (g_bombPlanted && m_team == TEAM_TERRORIST) {
instantChatter (CHATTER_GUARDING_DROPPED_BOMB);
}
else if (m_inVIPZone && m_team == TEAM_TERRORIST) {
instantChatter (CHATTER_GUARDING_VIP_SAFETY);
}
else {
instantChatter (CHATTER_CAMP);
}
}
break;
case TASK_PLANTBOMB:
instantChatter (CHATTER_PLANTING_BOMB);
break;
case TASK_DEFUSEBOMB:
instantChatter (CHATTER_DEFUSING_BOMB);
break;
case TASK_ATTACK:
instantChatter (CHATTER_IN_COMBAT);
break;
case TASK_HIDE:
case TASK_SEEKCOVER:
instantChatter (CHATTER_SEEK_ENEMY);
break;
default:
if (rng.getInt (0, 100) < 50) {
instantChatter (CHATTER_NOTHING);
}
break;
}
}
if (m_radioSelect != -1) {
if ((m_radioSelect != RADIO_REPORTING_IN && m_forceRadio) || yb_communication_type.integer () != 2 || g_chatterFactory[m_radioSelect].empty () || !(g_gameFlags & GAME_SUPPORT_BOT_VOICE)) {
if (m_radioSelect < RADIO_GO_GO_GO) {
engine.execBotCmd (ent (), "radio1");
}
else if (m_radioSelect < RADIO_AFFIRMATIVE) {
m_radioSelect -= RADIO_GO_GO_GO - 1;
engine.execBotCmd (ent (), "radio2");
}
else {
m_radioSelect -= RADIO_AFFIRMATIVE - 1;
engine.execBotCmd (ent (), "radio3");
}
// select correct menu item for this radio message
engine.execBotCmd (ent (), "menuselect %d", m_radioSelect);
}
else if (m_radioSelect != RADIO_REPORTING_IN) {
instantChatter (m_radioSelect);
}
}
m_forceRadio = false; // reset radio to voice
g_lastRadioTime[m_team] = engine.timebase (); // store last radio usage
}
else {
pushMsgQueue (GAME_MSG_RADIO);
}
break;
// team independent saytext
case GAME_MSG_SAY_CMD:
say (m_tempStrings.chars ());
break;
// team dependent saytext
case GAME_MSG_SAY_TEAM_MSG:
sayTeam (m_tempStrings.chars ());
break;
default:
return;
}
}
bool Bot::isWeaponRestricted (int weaponIndex) {
// this function checks for weapon restrictions.
if (isEmptyStr (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 (getWeaponData (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 ((1 << weaponIndex) & (WEAPON_PRIMARY | WEAPON_SECONDARY | WEAPON_SHIELD)) {
const char *restrictedWeapons = g_engfuncs.pfnCVarGetString ("amx_restrweapons");
if (isEmptyStr (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 = g_engfuncs.pfnCVarGetString ("amx_restrequipammo");
if (isEmptyStr (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 (void) {
// this function determines currently owned primary weapon, and checks if bot has
// enough money to buy more powerful weapon.
// if bot is not rich enough or non-standard weapon mode enabled return false
if (g_weaponSelect[25].teamStandard != 1 || m_moneyAmount < 4000) {
return false;
}
if (!isEmptyStr (yb_restricted_weapons.str ())) {
auto bannedWeapons = String (yb_restricted_weapons.str ()).split (";");
// check if its banned
for (auto &ban : bannedWeapons) {
if (m_currentWeapon == getWeaponData (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
if (yb_best_weapon_picker_type.integer () == 1) {
auto pick = [] (const float factor) -> float {
return (static_cast <int> (((unsigned int &) factor >> 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 (rng.getFloat (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;
}
}
for (int i = 0; i < count; i++) {
auto weapon = &g_weaponSelect[vec[i]];
// if wea have enough money for weapon buy it
if (weapon->price + moneySave < m_moneyAmount + rng.getInt (50, 200) && rng.getInt (0, 100) < chance) {
return vec[i];
}
}
return vec[rng.getInt (0, count - 1)];
}
void Bot::buyStuff (void) {
// this function does all the work in selecting correct buy menus for most weapons/items
WeaponSelect *selectedWeapon = nullptr;
m_nextBuyTime = engine.timebase ();
if (!m_ignoreBuyDelay) {
m_nextBuyTime += rng.getFloat (0.3f, 0.5f);
}
int count = 0, weaponCount = 0;
int choices[NUM_WEAPONS];
// select the priority tab for this personality
int *ptr = g_weaponPrefs[m_personality] + NUM_WEAPONS;
bool isPistolMode = g_weaponSelect[25].teamStandard == -1 && g_weaponSelect[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 = (g_gameFlags & GAME_LEGACY) && !(g_gameFlags & GAME_XASH_ENGINE);
switch (m_buyState) {
case BUYSTATE_PRIMARY_WEAPON: // 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;
ptr--;
assert (*ptr > -1);
assert (*ptr < NUM_WEAPONS);
selectedWeapon = &g_weaponSelect[*ptr];
count++;
if (selectedWeapon->buyGroup == 1) {
continue;
}
// weapon available for every team?
if ((g_mapFlags & MAP_AS) && 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;
}
int *limit = g_botBuyEconomyTable;
int prostock = 0;
// filter out weapons with bot economics
switch (m_personality) {
case PERSONALITY_RUSHER:
prostock = limit[ECO_PROSTOCK_RUSHER];
break;
case PERSONALITY_CAREFUL:
prostock = limit[ECO_PROSTOCK_CAREFUL];
break;
case PERSONALITY_NORMAL:
prostock = limit[ECO_PROSTOCK_NORMAL];
break;
}
if (m_team == TEAM_COUNTER) {
switch (selectedWeapon->id) {
case WEAPON_TMP:
case WEAPON_UMP45:
case WEAPON_P90:
case WEAPON_MP5:
if (m_moneyAmount > limit[ECO_SMG_GT_CT] + prostock) {
ignoreWeapon = true;
}
break;
}
if (selectedWeapon->id == WEAPON_SHIELD && m_moneyAmount > limit[ECO_SHIELDGUN_GT]) {
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[ECO_SMG_GT_TE] + prostock) {
ignoreWeapon = true;
}
break;
}
}
switch (selectedWeapon->id) {
case WEAPON_XM1014:
case WEAPON_M3:
if (m_moneyAmount < limit[ECO_SHOTGUN_LT]) {
ignoreWeapon = true;
}
if (m_moneyAmount >= limit[ECO_SHOTGUN_GT]) {
ignoreWeapon = false;
}
break;
}
switch (selectedWeapon->id) {
case WEAPON_SG550:
case WEAPON_G3SG1:
case WEAPON_AWP:
case WEAPON_M249:
if (m_moneyAmount < limit[ECO_HEAVY_LT]) {
ignoreWeapon = true;
}
if (m_moneyAmount >= limit[ECO_HEAVY_GT]) {
ignoreWeapon = false;
}
break;
}
if (ignoreWeapon && g_weaponSelect[25].teamStandard == 1 && yb_economics_rounds.boolean ()) {
continue;
}
// save money for grenade for example?
moneySave = rng.getInt (500, 1000);
if (bots.getLastWinner () == m_team) {
moneySave = 0;
}
if (selectedWeapon->price <= (m_moneyAmount - moneySave)) {
choices[weaponCount++] = *ptr;
}
} while (count < NUM_WEAPONS && 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 = &g_weaponSelect[chosenWeapon];
}
else {
selectedWeapon = nullptr;
}
if (selectedWeapon != nullptr) {
engine.execBotCmd (ent (), "buy;menuselect %d", selectedWeapon->buyGroup);
if (isOldGame) {
engine.execBotCmd (ent (), "menuselect %d", selectedWeapon->buySelect);
}
else {
if (m_team == TEAM_TERRORIST) {
engine.execBotCmd (ent (), "menuselect %d", selectedWeapon->newBuySelectT);
}
else {
engine.execBotCmd (ent (), "menuselect %d", selectedWeapon->newBuySelectCT);
}
}
}
}
else if (hasPrimaryWeapon () && !hasShield ()) {
m_reloadState = RELOAD_PRIMARY;
break;
}
else if ((hasSecondaryWeapon () && !hasShield ()) || hasShield ()) {
m_reloadState = RELOAD_SECONDARY;
break;
}
break;
case BUYSTATE_ARMOR_VESTHELM: // if armor is damaged and bot has some money, buy some armor
if (pev->armorvalue < rng.getInt (50, 80) && (isPistolMode || (teamEcoValid && hasPrimaryWeapon ()))) {
// if bot is rich, buy kevlar + helmet, else buy a single kevlar
if (m_moneyAmount > 1500 && !isWeaponRestricted (WEAPON_ARMORHELM)) {
engine.execBotCmd (ent (), "buyequip;menuselect 2");
}
else if (!isWeaponRestricted (WEAPON_ARMOR)) {
engine.execBotCmd (ent (), "buyequip;menuselect 1");
}
}
break;
case BUYSTATE_SECONDARY_WEAPON: // if bot has still some money, buy a better secondary weapon
if (isPistolMode || (hasPrimaryWeapon () && (pev->weapons & ((1 << WEAPON_USP) | (1 << WEAPON_GLOCK))) && m_moneyAmount > rng.getInt (7500, 9000))) {
do {
ptr--;
assert (*ptr > -1);
assert (*ptr < NUM_WEAPONS);
selectedWeapon = &g_weaponSelect[*ptr];
count++;
if (selectedWeapon->buyGroup != 1) {
continue;
}
// ignore weapon if this weapon is restricted
if (isWeaponRestricted (selectedWeapon->id)) {
continue;
}
// weapon available for every team?
if ((g_mapFlags & MAP_AS) && 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 - rng.getInt (100, 200))) {
choices[weaponCount++] = *ptr;
}
} while (count < NUM_WEAPONS && weaponCount < 4);
// found a desired weapon?
if (weaponCount > 0) {
int chosenWeapon;
// choose randomly from the best ones...
if (weaponCount > 1) {
chosenWeapon = pickBestWeapon (choices, weaponCount, rng.getInt (100, 200));
}
else {
chosenWeapon = choices[weaponCount - 1];
}
selectedWeapon = &g_weaponSelect[chosenWeapon];
}
else {
selectedWeapon = nullptr;
}
if (selectedWeapon != nullptr) {
engine.execBotCmd (ent (), "buy;menuselect %d", selectedWeapon->buyGroup);
if (isOldGame) {
engine.execBotCmd (ent (), "menuselect %d", selectedWeapon->buySelect);
}
else {
if (m_team == TEAM_TERRORIST) {
engine.execBotCmd (ent (), "menuselect %d", selectedWeapon->newBuySelectT);
}
else {
engine.execBotCmd (ent (), "menuselect %d", selectedWeapon->newBuySelectCT);
}
}
}
}
break;
case BUYSTATE_GRENADES: // if bot has still some money, choose if bot should buy a grenade or not
if (rng.getInt (1, 100) < g_grenadeBuyPrecent[0] && m_moneyAmount >= 400 && !isWeaponRestricted (WEAPON_EXPLOSIVE)) {
// buy a he grenade
engine.execBotCmd (ent (), "buyequip");
engine.execBotCmd (ent (), "menuselect 4");
}
if (rng.getInt (1, 100) < g_grenadeBuyPrecent[1] && m_moneyAmount >= 300 && teamEcoValid && !isWeaponRestricted (WEAPON_FLASHBANG)) {
// buy a concussion grenade, i.e., 'flashbang'
engine.execBotCmd (ent (), "buyequip");
engine.execBotCmd (ent (), "menuselect 3");
}
if (rng.getInt (1, 100) < g_grenadeBuyPrecent[2] && m_moneyAmount >= 400 && teamEcoValid && !isWeaponRestricted (WEAPON_SMOKE)) {
// buy a smoke grenade
engine.execBotCmd (ent (), "buyequip");
engine.execBotCmd (ent (), "menuselect 5");
}
break;
case BUYSTATE_DEFUSER: // if bot is CT and we're on a bomb map, randomly buy the defuse kit
if ((g_mapFlags & MAP_DE) && m_team == TEAM_COUNTER && rng.getInt (1, 100) < 80 && m_moneyAmount > 200 && !isWeaponRestricted (WEAPON_DEFUSER)) {
if (isOldGame) {
engine.execBotCmd (ent (), "buyequip;menuselect 6");
}
else {
engine.execBotCmd (ent (), "defuser"); // 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++) {
engine.execBotCmd (ent (), "buyammo%d", rng.getInt (1, 2)); // simulate human
}
// buy enough secondary ammo
if (hasPrimaryWeapon ()) {
engine.execBotCmd (ent (), "buy;menuselect 7");
}
// buy enough primary ammo
engine.execBotCmd (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 (GAME_MSG_PURCHASE);
}
void Bot::updateEmotions (void) {
// slowly increase/decrease dynamic emotions back to their base level
if (m_nextEmotionUpdate > engine.timebase ()) {
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 = engine.timebase () + 1.0f;
}
void Bot::overrideConditions (void) {
if (m_currentWeapon != WEAPON_KNIFE && m_difficulty > 2 && ((m_aimFlags & AIM_ENEMY) || (m_states & STATE_SEEING_ENEMY)) && !yb_jasonmode.boolean () && taskId () != TASK_CAMP && taskId () != TASK_SEEKCOVER && !isOnLadder ()) {
m_moveToGoal = false; // don't move to goal
m_navTimeset = engine.timebase ();
if (isPlayer (m_enemy)) {
attackMovement ();
}
}
// check if we need to escape from bomb
if ((g_mapFlags & MAP_DE) && g_bombPlanted && m_notKilled && taskId () != TASK_ESCAPEFROMBOMB && taskId () != TASK_CAMP && isOutOfBombTimer ()) {
completeTask (); // complete current task
// then start escape from bomb immediate
startTask (TASK_ESCAPEFROMBOMB, TASKPRI_ESCAPEFROMBOMB, INVALID_WAYPOINT_INDEX, 0.0f, true);
}
// special handling, if we have a knife in our hands
if ((g_timeRoundStart + 6.0f > engine.timebase () || !hasAnyWeapons ()) && m_currentWeapon == WEAPON_KNIFE && isPlayer (m_enemy) && (taskId () != TASK_MOVETOPOSITION || task ()->desire != TASKPRI_HIDE)) {
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 & STATE_SEEING_ENEMY)) {
int nearestToEnemyPoint = waypoints.getNearest (m_enemy->v.origin);
if (nearestToEnemyPoint != INVALID_WAYPOINT_INDEX && nearestToEnemyPoint != m_currentWaypointIndex && cr::abs (waypoints[nearestToEnemyPoint].origin.z - m_enemy->v.origin.z) < 16.0f) {
startTask (TASK_MOVETOPOSITION, TASKPRI_HIDE, nearestToEnemyPoint, engine.timebase () + rng.getFloat (5.0f, 10.0f), true);
m_isEnemyReachable = false;
m_enemy = nullptr;
m_enemyIgnoreTimer = engine.timebase () + length / pev->maxspeed * 0.5f;
}
}
}
// special handling for sniping
if (usesSniper () && (m_states & (STATE_SEEING_ENEMY | STATE_SUSPECT_ENEMY)) && m_sniperStopTime > engine.timebase () && taskId () != TASK_SEEKCOVER) {
m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f;
m_navTimeset = engine.timebase ();
}
}
void Bot::setConditions (void) {
// 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?
if (lookupEnemies ()) {
m_states |= STATE_SEEING_ENEMY;
}
else {
m_states &= ~STATE_SEEING_ENEMY;
m_enemy = nullptr;
}
// did bot just kill an enemy?
if (!engine.isNullEntity (m_lastVictim)) {
if (engine.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 (rng.getInt (1, 100) < 10) {
pushChatMessage (CHAT_KILLING);
}
if (rng.getInt (1, 100) < 10) {
pushRadioMessage (RADIO_ENEMY_DOWN);
}
else {
if ((m_lastVictim->v.weapons & (1 << WEAPON_AWP)) || (m_lastVictim->v.weapons & (1 << WEAPON_SCOUT)) || (m_lastVictim->v.weapons & (1 << WEAPON_G3SG1)) || (m_lastVictim->v.weapons & (1 << WEAPON_SG550))) {
pushChatterMessage (CHATTER_SNIPER_KILLED);
}
else {
switch (numEnemiesNear (pev->origin, 99999.0f)) {
case 0:
if (rng.getInt (0, 100) < 50) {
pushChatterMessage (CHATTER_NO_ENEMIES_LEFT);
}
else {
pushChatterMessage (CHATTER_ENEMY_DOWN);
}
break;
case 1:
pushChatterMessage (CHATTER_ONE_ENEMY_LEFT);
break;
case 2:
pushChatterMessage (CHATTER_TWO_ENEMIES_LEFT);
break;
case 3:
pushChatterMessage (CHATTER_THREE_ENEMIES_LEFT);
break;
default:
pushChatterMessage (CHATTER_ENEMY_DOWN);
}
}
}
// if no more enemies found AND bomb planted, switch to knife to get to bombplace faster
if (m_team == TEAM_COUNTER && m_currentWeapon != WEAPON_KNIFE && m_numEnemiesLeft == 0 && g_bombPlanted) {
selectWeaponByName ("weapon_knife");
m_plantedBombWptIndex = locatePlantedC4 ();
if (isOccupiedPoint (m_plantedBombWptIndex)) {
instantChatter (CHATTER_BOMB_SITE_SECURED);
}
}
}
else {
pushChatMessage (CHAT_TEAMKILL, true);
pushChatterMessage (CHATTER_TEAM_ATTACK);
}
m_lastVictim = nullptr;
}
// check if our current enemy is still valid
if (!engine.isNullEntity (m_lastEnemy)) {
if (!isAlive (m_lastEnemy) && m_shootAtDeadTime < engine.timebase ()) {
m_lastEnemyOrigin.nullify ();
m_lastEnemy = nullptr;
}
}
else {
m_lastEnemyOrigin.nullify ();
m_lastEnemy = nullptr;
}
// don't listen if seeing enemy, just checked for sounds or being blinded (because its inhuman)
if (!yb_ignore_enemies.boolean () && m_soundUpdateTime < engine.timebase () && m_blindTime < engine.timebase () && m_seeEnemyTime + 1.0f < engine.timebase ()) {
processHearing ();
m_soundUpdateTime = engine.timebase () + 0.25f;
}
else if (m_heardSoundTime < engine.timebase ()) {
m_states &= ~STATE_HEARING_ENEMY;
}
if (engine.isNullEntity (m_enemy) && !engine.isNullEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty ()) {
m_aimFlags |= AIM_PREDICT_PATH;
if (seesEntity (m_lastEnemyOrigin, true)) {
m_aimFlags |= AIM_LAST_ENEMY;
}
}
// check for grenades depending on difficulty
if (rng.getInt (0, 100) < m_difficulty * 25) {
checkGrenadesThrow ();
}
// check if there are items needing to be used/collected
if (m_itemCheckTime < engine.timebase () || !engine.isNullEntity (m_pickupItem)) {
m_itemCheckTime = engine.timebase () + 0.5f;
processPickups ();
}
filterTasks ();
}
void Bot::filterTasks (void) {
// initialize & calculate the desire for all actions based on distances, emotions and other stuff
task ();
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;
}
// bot found some item to use?
if (!engine.isNullEntity (m_pickupItem) && taskId () != TASK_ESCAPEFROMBOMB) {
m_states |= STATE_PICKUP_ITEM;
if (m_pickupType == PICKUP_BUTTON) {
g_taskFilters[TASK_PICKUPITEM].desire = 50.0f; // always pickup button
}
else {
float distance = (500.0f - (engine.getAbsPos (m_pickupItem) - pev->origin).length ()) * 0.2f;
if (distance > 50.0f) {
distance = 50.0f;
}
g_taskFilters[TASK_PICKUPITEM].desire = distance;
}
}
else {
m_states &= ~STATE_PICKUP_ITEM;
g_taskFilters[TASK_PICKUPITEM].desire = 0.0f;
}
// calculate desire to attack
if ((m_states & STATE_SEEING_ENEMY) && reactOnEnemy ()) {
g_taskFilters[TASK_ATTACK].desire = TASKPRI_ATTACK;
}
else {
g_taskFilters[TASK_ATTACK].desire = 0.0f;
}
float &seekCoverDesire = g_taskFilters[TASK_SEEKCOVER].desire;
float &huntEnemyDesire = g_taskFilters[TASK_HUNTENEMY].desire;
float &blindedDesire = g_taskFilters[TASK_BLINDED].desire;
// calculate desires to seek cover or hunt
if (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 < engine.timebase () && m_seeEnemyTime - rng.getFloat (2.0f, 4.0f) < engine.timebase ()) {
float timeSeen = m_seeEnemyTime - engine.timebase ();
float timeHeard = m_heardSoundTime - engine.timebase ();
float ratio = 0.0f;
m_retreatTime = engine.timebase () + rng.getFloat (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] < getMaxClip (m_currentWeapon) * 0.18f;
if (g_bombPlanted || 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 *= 2.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 (taskId () != TASK_ESCAPEFROMBOMB && engine.isNullEntity (m_enemy) && g_timeRoundMid < engine.timebase () && !m_isUsingGrenade && m_currentWaypointIndex != waypoints.getNearest (m_lastEnemyOrigin) && m_personality != PERSONALITY_CAREFUL && !yb_ignore_enemies.boolean ()) {
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 > engine.timebase () ? TASKPRI_BLINDED : 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 = [] (Task *first, Task *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 = [] (Task *first, Task *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 = [] (Task *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 (g_taskFilters[TASK_ATTACK].desire, 40.0f, 90.0f, m_oldCombatDesire);
g_taskFilters[TASK_ATTACK].desire = m_oldCombatDesire;
auto offensive = &g_taskFilters[TASK_ATTACK];
auto pickup = &g_taskFilters[TASK_PICKUPITEM];
// calc survive (cover/hide)
auto survive = thresholdDesire (&g_taskFilters[TASK_SEEKCOVER], 40.0f, 0.0f);
survive = subsumeDesire (&g_taskFilters[TASK_HIDE], survive);
auto def = thresholdDesire (&g_taskFilters[TASK_HUNTENEMY], 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 (&g_taskFilters[TASK_BLINDED], maxDesire (survive, sub)); // reason about fleeing instead
if (!m_tasks.empty ()) {
final = maxDesire (final, task ());
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 (void) {
// this function resets bot tasks stack, by removing all entries from the stack.
m_tasks.clear ();
}
void Bot::startTask (TaskID 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.push ({ id, desire, data, time, resume });
clearSearchNodes ();
ignoreCollision ();
int tid = taskId ();
// leader bot?
if (m_isLeader && tid == TASK_SEEKCOVER) {
processTeamCommands (); // reorganize team if fleeing
}
if (tid == TASK_CAMP) {
selectBestWeapon ();
}
// this is best place to handle some voice commands report team some info
if (rng.getInt (0, 100) < 95) {
if (tid == TASK_BLINDED) {
instantChatter (CHATTER_BLINDED);
}
else if (tid == TASK_PLANTBOMB) {
instantChatter (CHATTER_PLANTING_BOMB);
}
}
if (rng.getInt (0, 100) < 80 && tid == TASK_CAMP) {
if ((g_mapFlags & MAP_DE) && g_bombPlanted) {
pushChatterMessage (CHATTER_GUARDING_DROPPED_BOMB);
}
else {
pushChatterMessage (CHATTER_GOING_TO_CAMP);
}
}
if (yb_debug_goal.integer () != INVALID_WAYPOINT_INDEX) {
m_chosenGoalIndex = yb_debug_goal.integer ();
}
else {
m_chosenGoalIndex = task ()->data;
}
if (rng.getInt (0, 100) < 80 && tid == TASK_CAMP && m_team == TEAM_TERRORIST && m_inVIPZone) {
pushChatterMessage (CHATTER_GOING_TO_GUARD_VIP_SAFETY);
}
}
Task *Bot::task (void) {
if (m_tasks.empty ()) {
m_tasks.push ({ TASK_NORMAL, TASKPRI_NORMAL, INVALID_WAYPOINT_INDEX, 0.0f, true });
}
return &m_tasks.back ();
}
void Bot::clearTask (TaskID id) {
// this function removes one task from the bot task stack.
if (m_tasks.empty () || taskId () == TASK_NORMAL) {
return; // since normal task can be only once on the stack, don't remove it...
}
if (taskId () == id) {
clearSearchNodes ();
ignoreCollision ();
m_tasks.pop ();
return;
}
for (auto &task : m_tasks) {
if (task.id == id) {
m_tasks.erase (task);
}
}
ignoreCollision ();
clearSearchNodes ();
}
void Bot::completeTask (void) {
// this function called whenever a task is completed.
ignoreCollision ();
if (m_tasks.empty ()) {
return;
}
do {
m_tasks.pop ();
} while (!m_tasks.empty () && !m_tasks.back ().resume);
clearSearchNodes ();
}
bool Bot::isEnemyThreat (void) {
if (engine.isNullEntity (m_enemy) || taskId () == TASK_SEEKCOVER) {
return false;
}
// if bot is camping, he should be firing anyway and not leaving his position
if (taskId () == 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 (void) {
// 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 < engine.timebase ()) {
int ownIndex = m_currentWaypointIndex;
if (ownIndex == INVALID_WAYPOINT_INDEX) {
ownIndex = getNearestPoint ();
}
int enemyIndex = waypoints.getNearest (m_enemy->v.origin);
float lineDist = (m_enemy->v.origin - pev->origin).length ();
float pathDist = static_cast <float> (waypoints.getPathDist (ownIndex, enemyIndex));
if (pathDist - lineDist > 112.0f) {
m_isEnemyReachable = false;
}
else {
m_isEnemyReachable = true;
}
m_enemyReachableTimer = engine.timebase () + 1.0f;
}
if (m_isEnemyReachable) {
m_navTimeset = engine.timebase (); // override existing movement by attack movement
return true;
}
return false;
}
bool Bot::lastEnemyShootable (void) {
// don't allow shooting through walls
if (!(m_aimFlags & AIM_LAST_ENEMY) || m_lastEnemyOrigin.empty () || engine.isNullEntity (m_lastEnemy)) {
return false;
}
return getShootingConeDeviation (ent (), m_lastEnemyOrigin) >= 0.90f && isPenetrableObstacle (m_lastEnemyOrigin);
}
void Bot::checkRadioQueue (void) {
// this function handling radio and reacting to it
float distance = (m_radioEntity->v.origin - pev->origin).length ();
// don't allow bot listen you if bot is busy
if ((taskId () == TASK_DEFUSEBOMB || taskId () == TASK_PLANTBOMB || hasHostage () || m_hasC4) && m_radioOrder != RADIO_REPORT_TEAM) {
m_radioOrder = 0;
return;
}
switch (m_radioOrder) {
case RADIO_COVER_ME:
case RADIO_FOLLOW_ME:
case RADIO_STICK_TOGETHER_TEAM:
case CHATTER_GOING_TO_PLANT_BOMB:
case CHATTER_COVER_ME:
// check if line of sight to object is not blocked (i.e. visible)
if ((seesEntity (m_radioEntity->v.origin)) || (m_radioOrder == RADIO_STICK_TOGETHER_TEAM)) {
if (engine.isNullEntity (m_targetEntity) && engine.isNullEntity (m_enemy) && rng.getInt (0, 100) < (m_personality == PERSONALITY_CAREFUL ? 80 : 20)) {
int numFollowers = 0;
// Check if no more followers are allowed
for (int i = 0; i < engine.maxClients (); i++) {
Bot *bot = bots.getBot (i);
if (bot != nullptr) {
if (bot->m_notKilled) {
if (bot->m_targetEntity == m_radioEntity) {
numFollowers++;
}
}
}
}
int allowedFollowers = yb_user_max_followers.integer ();
if (m_radioEntity->v.weapons & (1 << WEAPON_C4)) {
allowedFollowers = 1;
}
if (numFollowers < allowedFollowers) {
pushRadioMessage (RADIO_AFFIRMATIVE);
m_targetEntity = m_radioEntity;
// don't pause/camp/follow anymore
TaskID taskID = taskId ();
if (taskID == TASK_PAUSE || taskID == TASK_CAMP) {
task ()->time = engine.timebase ();
}
startTask (TASK_FOLLOWUSER, TASKPRI_FOLLOWUSER, INVALID_WAYPOINT_INDEX, 0.0f, true);
}
else if (numFollowers > allowedFollowers) {
for (int i = 0; (i < engine.maxClients () && numFollowers > allowedFollowers); i++) {
Bot *bot = bots.getBot (i);
if (bot != nullptr) {
if (bot->m_notKilled) {
if (bot->m_targetEntity == m_radioEntity) {
bot->m_targetEntity = nullptr;
numFollowers--;
}
}
}
}
}
else if (m_radioOrder != CHATTER_GOING_TO_PLANT_BOMB && rng.getInt (0, 100) < 15) {
pushRadioMessage (RADIO_NEGATIVE);
}
}
else if (m_radioOrder != CHATTER_GOING_TO_PLANT_BOMB && rng.getInt (0, 100) < 25) {
pushRadioMessage (RADIO_NEGATIVE);
}
}
break;
case RADIO_HOLD_THIS_POSITION:
if (!engine.isNullEntity (m_targetEntity)) {
if (m_targetEntity == m_radioEntity) {
m_targetEntity = nullptr;
pushRadioMessage (RADIO_AFFIRMATIVE);
m_campButtons = 0;
startTask (TASK_PAUSE, TASKPRI_PAUSE, INVALID_WAYPOINT_INDEX, engine.timebase () + rng.getFloat (30.0f, 60.0f), false);
}
}
break;
case CHATTER_NEW_ROUND:
pushChatterMessage (CHATTER_YOU_HEARD_THE_MAN);
break;
case RADIO_TAKING_FIRE:
if (engine.isNullEntity (m_targetEntity)) {
if (engine.isNullEntity (m_enemy) && m_seeEnemyTime + 4.0f < engine.timebase ()) {
// 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 (rng.getInt (0, 100) < 45 && yb_communication_type.integer () == 2) {
pushChatterMessage (CHATTER_ON_MY_WAY);
}
else if (m_radioOrder == RADIO_NEED_BACKUP && yb_communication_type.integer () != 2) {
pushRadioMessage (RADIO_AFFIRMATIVE);
}
tryHeadTowardRadioMessage ();
}
else if (rng.getInt (0, 100) < 25) {
pushRadioMessage (RADIO_NEGATIVE);
}
}
break;
case RADIO_YOU_TAKE_THE_POINT:
if (seesEntity (m_radioEntity->v.origin) && m_isLeader) {
pushRadioMessage (RADIO_AFFIRMATIVE);
}
break;
case RADIO_ENEMY_SPOTTED:
case RADIO_NEED_BACKUP:
case CHATTER_SCARED_EMOTE:
case CHATTER_PINNED_DOWN:
if (((engine.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 2048.0f || !m_moveToC4) && rng.getInt (0, 100) > 50 && m_seeEnemyTime + 4.0f < engine.timebase ()) {
m_fearLevel -= 0.1f;
if (m_fearLevel < 0.0f) {
m_fearLevel = 0.0f;
}
if (rng.getInt (0, 100) < 45 && yb_communication_type.integer () == 2) {
pushChatterMessage (CHATTER_ON_MY_WAY);
}
else if (m_radioOrder == RADIO_NEED_BACKUP && yb_communication_type.integer () != 2) {
pushRadioMessage (RADIO_AFFIRMATIVE);
}
tryHeadTowardRadioMessage ();
}
else if (rng.getInt (0, 100) < 30 && m_radioOrder == RADIO_NEED_BACKUP) {
pushRadioMessage (RADIO_NEGATIVE);
}
break;
case RADIO_GO_GO_GO:
if (m_radioEntity == m_targetEntity) {
if (rng.getInt (0, 100) < 45 && yb_communication_type.integer () == 2) {
pushRadioMessage (RADIO_AFFIRMATIVE);
}
else if (m_radioOrder == RADIO_NEED_BACKUP && yb_communication_type.integer () != 2) {
pushRadioMessage (RADIO_AFFIRMATIVE);
}
m_targetEntity = nullptr;
m_fearLevel -= 0.2f;
if (m_fearLevel < 0.0f) {
m_fearLevel = 0.0f;
}
}
else if ((engine.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 2048.0f) {
TaskID taskID = taskId ();
if (taskID == TASK_PAUSE || taskID == TASK_CAMP) {
m_fearLevel -= 0.2f;
if (m_fearLevel < 0.0f) {
m_fearLevel = 0.0f;
}
pushRadioMessage (RADIO_AFFIRMATIVE);
// don't pause/camp anymore
task ()->time = engine.timebase ();
m_targetEntity = nullptr;
makeVectors (m_radioEntity->v.v_angle);
m_position = m_radioEntity->v.origin + g_pGlobals->v_forward * rng.getFloat (1024.0f, 2048.0f);
clearSearchNodes ();
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, INVALID_WAYPOINT_INDEX, 0.0f, true);
}
}
else if (!engine.isNullEntity (m_doubleJumpEntity)) {
pushRadioMessage (RADIO_AFFIRMATIVE);
resetDoubleJump ();
}
else if (rng.getInt (0, 100) < 35) {
pushRadioMessage (RADIO_NEGATIVE);
}
break;
case RADIO_SHES_GONNA_BLOW:
if (engine.isNullEntity (m_enemy) && distance < 2048.0f && g_bombPlanted && m_team == TEAM_TERRORIST) {
pushRadioMessage (RADIO_AFFIRMATIVE);
if (taskId () == TASK_CAMP) {
clearTask (TASK_CAMP);
}
m_targetEntity = nullptr;
startTask (TASK_ESCAPEFROMBOMB, TASKPRI_ESCAPEFROMBOMB, INVALID_WAYPOINT_INDEX, 0.0f, true);
}
else if (rng.getInt (0, 100) < 35) {
pushRadioMessage (RADIO_NEGATIVE);
}
break;
case RADIO_REGROUP_TEAM:
// if no more enemies found AND bomb planted, switch to knife to get to bombplace faster
if (m_team == TEAM_COUNTER && m_currentWeapon != WEAPON_KNIFE && m_numEnemiesLeft == 0 && g_bombPlanted && taskId () != TASK_DEFUSEBOMB) {
selectWeaponByName ("weapon_knife");
clearSearchNodes ();
m_position = waypoints.getBombPos ();
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, INVALID_WAYPOINT_INDEX, 0.0f, true);
pushRadioMessage (RADIO_AFFIRMATIVE);
}
break;
case RADIO_STORM_THE_FRONT:
if (((engine.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 1024.0f) && rng.getInt (0, 100) > 50) {
pushRadioMessage (RADIO_AFFIRMATIVE);
// don't pause/camp anymore
TaskID taskID = taskId ();
if (taskID == TASK_PAUSE || taskID == TASK_CAMP) {
task ()->time = engine.timebase ();
}
m_targetEntity = nullptr;
makeVectors (m_radioEntity->v.v_angle);
m_position = m_radioEntity->v.origin + g_pGlobals->v_forward * rng.getFloat (1024.0f, 2048.0f);
clearSearchNodes ();
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, INVALID_WAYPOINT_INDEX, 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_TEAM_FALLBACK:
if ((engine.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 (taskId () == TASK_CAMP) {
task ()->time += rng.getFloat (10.0f, 15.0f);
}
else {
// don't pause/camp anymore
TaskID taskID = taskId ();
if (taskID == TASK_PAUSE) {
task ()->time = engine.timebase ();
}
m_targetEntity = nullptr;
m_seeEnemyTime = engine.timebase ();
// if bot has no enemy
if (m_lastEnemyOrigin.empty ()) {
float nearestDistance = 99999.0f;
// take nearest enemy to ordering player
for (int i = 0; i < engine.maxClients (); i++) {
const Client &client = g_clients[i];
if (!(client.flags & CF_USED) || !(client.flags & CF_ALIVE) || client.team == m_team) {
continue;
}
edict_t *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_REPORT_TEAM:
if (rng.getInt (0, 100) < 30) {
pushRadioMessage ((numEnemiesNear (pev->origin, 400.0f) == 0 && yb_communication_type.integer () != 2) ? RADIO_SECTOR_CLEAR : RADIO_REPORTING_IN);
}
break;
case RADIO_SECTOR_CLEAR:
// is bomb planted and it's a ct
if (!g_bombPlanted) {
break;
}
// check if it's a ct command
if (engine.getTeam (m_radioEntity) == TEAM_COUNTER && m_team == TEAM_COUNTER && isFakeClient (m_radioEntity) && g_timeNextBombUpdate < engine.timebase ()) {
float minDistance = 99999.0f;
int bombPoint = INVALID_WAYPOINT_INDEX;
// find nearest bomb waypoint to player
for (auto &point : waypoints.m_goalPoints) {
distance = (waypoints[point].origin - m_radioEntity->v.origin).lengthSq ();
if (distance < minDistance) {
minDistance = distance;
bombPoint = point;
}
}
// mark this waypoint as restricted point
if (bombPoint != INVALID_WAYPOINT_INDEX && !waypoints.isVisited (bombPoint)) {
// does this bot want to defuse?
if (taskId () == TASK_NORMAL) {
// is he approaching this goal?
if (task ()->data == bombPoint) {
task ()->data = INVALID_WAYPOINT_INDEX;
pushRadioMessage (RADIO_AFFIRMATIVE);
}
}
waypoints.setVisited (bombPoint);
}
g_timeNextBombUpdate = engine.timebase () + 0.5f;
}
break;
case RADIO_GET_IN_POSITION:
if ((engine.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 1024.0f) {
pushRadioMessage (RADIO_AFFIRMATIVE);
if (taskId () == TASK_CAMP) {
task ()->time = engine.timebase () + rng.getFloat (30.0f, 60.0f);
}
else {
// don't pause anymore
TaskID taskID = taskId ();
if (taskID == TASK_PAUSE) {
task ()->time = engine.timebase ();
}
m_targetEntity = nullptr;
m_seeEnemyTime = engine.timebase ();
// if bot has no enemy
if (m_lastEnemyOrigin.empty ()) {
float nearestDistance = 99999.0f;
// take nearest enemy to ordering player
for (int i = 0; i < engine.maxClients (); i++) {
const Client &client = g_clients[i];
if (!(client.flags & CF_USED) || !(client.flags & CF_ALIVE) || client.team == m_team)
continue;
edict_t *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 = getDefendPoint (m_radioEntity->v.origin);
// push camp task on to stack
startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, engine.timebase () + rng.getFloat (30.0f, 60.0f), true);
// push move command
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, index, engine.timebase () + rng.getFloat (30.0f, 60.0f), true);
if (waypoints[index].vis.crouch <= waypoints[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 (void) {
TaskID taskID = taskId ();
if (taskID == TASK_MOVETOPOSITION || m_headedTime + 15.0f < engine.timebase () || !isAlive (m_radioEntity) || m_hasC4)
return;
if ((isFakeClient (m_radioEntity) && rng.getInt (0, 100) < 25 && m_personality == PERSONALITY_NORMAL) || !(m_radioEntity->v.flags & FL_FAKECLIENT)) {
if (taskID == TASK_PAUSE || taskID == TASK_CAMP) {
task ()->time = engine.timebase ();
}
m_headedTime = engine.timebase ();
m_position = m_radioEntity->v.origin;
clearSearchNodes ();
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, INVALID_WAYPOINT_INDEX, 0.0f, true);
}
}
void Bot::updateAimDir (void) {
unsigned int flags = m_aimFlags;
// don't allow bot to look at danger positions under certain circumstances
if (!(flags & (AIM_GRENADE | AIM_ENEMY | AIM_ENTITY))) {
if (isOnLadder () || isInWater () || (m_waypointFlags & FLAG_LADDER) || (m_currentTravelFlags & PATHFLAG_JUMP)) {
flags &= ~(AIM_LAST_ENEMY | AIM_PREDICT_PATH);
m_canChooseAimDirection = false;
}
}
if (flags & AIM_OVERRIDE) {
m_lookAt = m_camp;
}
else if (flags & AIM_GRENADE) {
m_lookAt = m_throw;
float throwDistance = (m_throw - pev->origin).length ();
float coordCorrection = 0.0f;
float angleCorrection = 0.0f;
if (throwDistance > 100.0f && throwDistance < 800.0f) {
angleCorrection = 0.0f;
coordCorrection = 0.25f * (m_throw.z - pev->origin.z);
}
else if (throwDistance >= 800.0f) {
angleCorrection = 37.0f * (throwDistance - 800.0f) / 800.0f;
if (angleCorrection > 45.0f) {
angleCorrection = 45.0f;
}
coordCorrection = throwDistance * cr::tanf (cr::deg2rad (angleCorrection)) + 0.25f * (m_throw.z - pev->origin.z);
}
m_lookAt.z += coordCorrection * 0.5f;
}
else if (flags & AIM_ENEMY) {
focusEnemy ();
}
else if (flags & AIM_ENTITY) {
m_lookAt = m_entity;
}
else if (flags & AIM_LAST_ENEMY) {
m_lookAt = m_lastEnemyOrigin;
// did bot just see enemy and is quite aggressive?
if (m_seeEnemyTime + 1.0f - m_actualReactionTime + m_baseAgressionLevel > engine.timebase ()) {
// feel free to fire if shootable
if (!usesSniper () && lastEnemyShootable ()) {
m_wantsToFire = true;
}
}
}
else if (flags & AIM_PREDICT_PATH) {
bool changePredictedEnemy = true;
if (m_timeNextTracking > engine.timebase () && m_trackingEdict == m_lastEnemy && isAlive (m_lastEnemy)) {
changePredictedEnemy = false;
}
if (changePredictedEnemy) {
int aimPoint = searchAimingPoint (m_lastEnemyOrigin);
if (aimPoint != INVALID_WAYPOINT_INDEX) {
m_lookAt = waypoints[aimPoint].origin;
m_camp = m_lookAt;
m_timeNextTracking = engine.timebase () + 0.5f;
m_trackingEdict = m_lastEnemy;
}
else {
m_aimFlags &= ~AIM_PREDICT_PATH;
if (!m_camp.empty ()) {
m_lookAt = m_camp;
}
}
}
else {
m_lookAt = m_camp;
}
}
else if (flags & AIM_CAMP) {
m_lookAt = m_camp;
}
else if (flags & AIM_NAVPOINT) {
m_lookAt = m_destOrigin;
if (m_canChooseAimDirection && m_currentWaypointIndex != INVALID_WAYPOINT_INDEX && !(m_currentPath->flags & FLAG_LADDER)) {
auto experience = (g_experienceData + (m_currentWaypointIndex * waypoints.length ()) + m_currentWaypointIndex);
if (m_team == TEAM_TERRORIST) {
if (experience->team0DangerIndex != INVALID_WAYPOINT_INDEX && waypoints.isVisible (m_currentWaypointIndex, experience->team0DangerIndex)) {
m_lookAt = waypoints[experience->team0DangerIndex].origin;
}
}
else {
if (experience->team1DangerIndex != INVALID_WAYPOINT_INDEX && waypoints.isVisible (m_currentWaypointIndex, experience->team1DangerIndex)) {
m_lookAt = waypoints[experience->team1DangerIndex].origin;
}
}
}
}
if (m_lookAt.empty ()) {
m_lookAt = m_destOrigin;
}
}
void Bot::framePeriodic (void) {
if (m_thinkFps <= engine.timebase ()) {
// execute delayed think
frameThink ();
// skip some frames
m_thinkFps = engine.timebase () + m_thinkInterval;
}
else {
processLookAngles ();
}
}
void Bot::frameThink (void) {
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.nullify ();
m_canChooseAimDirection = true;
m_notKilled = isAlive (ent ());
m_team = engine.getTeam (ent ());
if ((g_mapFlags & MAP_AS) && !m_isVIP) {
m_isVIP = isPlayerVIP (ent ());
}
if (m_team == TEAM_TERRORIST && (g_mapFlags & MAP_DE)) {
m_hasC4 = !!(pev->weapons & (1 << 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) {
processTeamJoin (); // select team & class
}
else if (!m_notKilled) {
// we got a teamkiller? vote him away...
if (m_voteKickIndex != m_lastVoteKick && yb_tkpunish.boolean ()) {
engine.execBotCmd (ent (), "vote %d", m_voteKickIndex);
m_lastVoteKick = m_voteKickIndex;
// if bot tk punishment is enabled slay the tk
if (yb_tkpunish.integer () != 2 || isFakeClient (engine.entityOfIndex (m_voteKickIndex))) {
return;
}
edict_t *killer = engine.entityOfIndex (m_lastVoteKick);
killer->v.frags++;
MDLL_ClientKill (killer);
}
// host wants us to kick someone
else if (m_voteMap != 0) {
engine.execBotCmd (ent (), "votemap %d", m_voteMap);
m_voteMap = 0;
}
}
else if (m_notKilled && m_buyingFinished && !(pev->maxspeed < 10.0f && taskId () != TASK_PLANTBOMB && taskId () != TASK_DEFUSEBOMB) && !yb_freeze_bots.boolean () && !waypoints.hasChanged ()) {
botMovement = true;
}
checkMsgQueue (); // check for pending messages
if (botMovement) {
ai (); // execute main code
}
runMovement (); // run the player movement
}
void Bot::frame (void) {
if (m_timePeriodicUpdate > engine.timebase ()) {
return;
}
m_numFriendsLeft = numFriendsNear (pev->origin, 99999.0f);
m_numEnemiesLeft = numEnemiesNear (pev->origin, 99999.0f);
if (g_bombPlanted && m_team == TEAM_COUNTER) {
const Vector &bombPosition = waypoints.getBombPos ();
if (!m_hasProgressBar && taskId () != TASK_ESCAPEFROMBOMB && (pev->origin - bombPosition).length () < 700.0f && !isBombDefusing (bombPosition)) {
clearTasks ();
}
}
checkSpawnConditions ();
extern ConVar yb_chat;
// bot chatting turned on?
if (!m_notKilled && yb_chat.boolean () && m_lastChatTime + 10.0 < engine.timebase () && g_lastChatTime + 5.0f < engine.timebase () && !isReplyingToChat ()) {
// say a text every now and then
if (rng.getInt (1, 1500) < 50) {
m_lastChatTime = engine.timebase ();
g_lastChatTime = engine.timebase ();
if (!g_chatFactory[CHAT_DEAD].empty ()) {
const String &phrase = g_chatFactory[CHAT_DEAD].random ();
bool sayBufferExists = false;
// search for last messages, sayed
for (auto &sentence : m_sayTextBuffer.lastUsedSentences) {
if (strncmp (sentence.chars (), phrase.chars (), sentence.length ()) == 0) {
sayBufferExists = true;
break;
}
}
if (!sayBufferExists) {
prepareChatMessage (const_cast <char *> (phrase.chars ()));
pushMsgQueue (GAME_MSG_SAY_CMD);
// add to ignore list
m_sayTextBuffer.lastUsedSentences.push (phrase);
}
}
// clear the used line buffer every now and then
if (static_cast <int> (m_sayTextBuffer.lastUsedSentences.length ()) > rng.getInt (4, 6)) {
m_sayTextBuffer.lastUsedSentences.clear ();
}
}
}
if (g_gameFlags & GAME_SUPPORT_BOT_VOICE) {
showChaterIcon (false); // end voice feedback
}
// clear enemy far away
if (!m_lastEnemyOrigin.empty () && !engine.isNullEntity (m_lastEnemy) && (pev->origin - m_lastEnemyOrigin).lengthSq () >= cr::square (1600.0f)) {
m_lastEnemy = nullptr;
m_lastEnemyOrigin.nullify ();
}
m_timePeriodicUpdate = engine.timebase () + 0.5f;
}
void Bot::normal_ (void) {
m_aimFlags |= AIM_NAVPOINT;
int debugGoal = yb_debug_goal.integer ();
// user forced a waypoint as a goal?
if (debugGoal != INVALID_WAYPOINT_INDEX && task ()->data != debugGoal) {
clearSearchNodes ();
task ()->data = debugGoal;
}
// stand still if reached debug goal
else if (m_currentWaypointIndex == 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 && (engine.isNullEntity (m_lastEnemy) || !isAlive (m_lastEnemy)) && engine.isNullEntity (m_enemy) && m_knifeAttackTime < engine.timebase () && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) {
if (rng.getInt (0, 100) < 40) {
pev->button |= IN_ATTACK;
}
else {
pev->button |= IN_ATTACK2;
}
m_knifeAttackTime = engine.timebase () + rng.getFloat (2.5f, 6.0f);
}
if (m_reloadState == RELOAD_NONE && ammo () != 0 && ammoClip () < 5 && g_weaponDefs[m_currentWeapon].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 && g_bombPlanted && m_team == TEAM_COUNTER && task ()->data != INVALID_WAYPOINT_INDEX && !(waypoints[task ()->data].flags & FLAG_GOAL) && taskId () != TASK_ESCAPEFROMBOMB) {
clearSearchNodes ();
task ()->data = INVALID_WAYPOINT_INDEX;
}
if (!g_bombPlanted && m_currentWaypointIndex != INVALID_WAYPOINT_INDEX && (m_currentPath->flags & FLAG_GOAL) && rng.getInt (0, 100) < 50 && numEnemiesNear (pev->origin, 650.0f) == 0) {
pushRadioMessage (RADIO_SECTOR_CLEAR);
}
// reached the destination (goal) waypoint?
if (processNavigation ()) {
completeTask ();
m_prevGoalIndex = INVALID_WAYPOINT_INDEX;
// spray logo sometimes if allowed to do so
if (m_timeLogoSpray < engine.timebase () && yb_spraypaints.boolean () && rng.getInt (1, 100) < 60 && m_moveSpeed > getShiftSpeed () && engine.isNullEntity (m_pickupItem)) {
if (!((g_mapFlags & MAP_DE) && g_bombPlanted && m_team == TEAM_COUNTER)) {
startTask (TASK_SPRAY, TASKPRI_SPRAYLOGO, INVALID_WAYPOINT_INDEX, engine.timebase () + 1.0f, false);
}
}
// reached waypoint is a camp waypoint
if ((m_currentPath->flags & FLAG_CAMP) && !(g_gameFlags & GAME_CSDM) && yb_camping_allowed.boolean ()) {
// check if bot has got a primary weapon and hasn't camped before
if (hasPrimaryWeapon () && m_timeCamping + 10.0f < engine.timebase () && !hasHostage ()) {
bool campingAllowed = true;
// Check if it's not allowed for this team to camp here
if (m_team == TEAM_TERRORIST) {
if (m_currentPath->flags & FLAG_CF_ONLY) {
campingAllowed = false;
}
}
else {
if (m_currentPath->flags & FLAG_TF_ONLY) {
campingAllowed = false;
}
}
// don't allow vip on as_ maps to camp + don't allow terrorist carrying c4 to camp
if (campingAllowed && (m_isVIP || ((g_mapFlags & MAP_DE) && m_team == TEAM_TERRORIST && !g_bombPlanted && m_hasC4))) {
campingAllowed = false;
}
// check if another bot is already camping here
if (campingAllowed && isOccupiedPoint (m_currentWaypointIndex)) {
campingAllowed = false;
}
if (campingAllowed) {
// crouched camping here?
if (m_currentPath->flags & FLAG_CROUCH) {
m_campButtons = IN_DUCK;
}
else {
m_campButtons = 0;
}
selectBestWeapon ();
if (!(m_states & (STATE_SEEING_ENEMY | STATE_HEARING_ENEMY)) && !m_reloadState) {
m_reloadState = RELOAD_PRIMARY;
}
makeVectors (pev->v_angle);
m_timeCamping = engine.timebase () + rng.getFloat (10.0f, 25.0f);
startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, m_timeCamping, true);
m_camp = Vector (m_currentPath->campStartX, m_currentPath->campStartY, 0.0f);
m_aimFlags |= AIM_CAMP;
m_campDirection = 0;
// tell the world we're camping
if (rng.getInt (0, 100) < 40) {
pushRadioMessage (RADIO_IN_POSITION);
}
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 (g_mapFlags & MAP_CS) {
// CT Bot has some hostages following?
if (m_team == TEAM_COUNTER && hasHostage ()) {
// and reached a Rescue Point?
if (m_currentPath->flags & FLAG_RESCUE) {
m_hostages.clear ();
}
}
else if (m_team == TEAM_TERRORIST && rng.getInt (0, 100) < 75) {
int index = getDefendPoint (m_currentPath->origin);
startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, engine.timebase () + rng.getFloat (60.0f, 120.0f), true); // push camp task on to stack
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, index, engine.timebase () + rng.getFloat (5.0f, 10.0f), true); // push move command
auto &path = waypoints[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_GOING_TO_GUARD_VIP_SAFETY); // play info about that
}
}
else if ((g_mapFlags & MAP_DE) && ((m_currentPath->flags & FLAG_GOAL) || m_inBombZone)) {
// is it a terrorist carrying the bomb?
if (m_hasC4) {
if ((m_states & STATE_SEEING_ENEMY) && numFriendsNear (pev->origin, 768.0f) == 0) {
// request an help also
pushRadioMessage (RADIO_NEED_BACKUP);
instantChatter (CHATTER_SCARED_EMOTE);
startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, engine.timebase () + rng.getFloat (4.0f, 8.0f), true);
}
else {
startTask (TASK_PLANTBOMB, TASKPRI_PLANTBOMB, INVALID_WAYPOINT_INDEX, 0.0f, false);
}
}
else if (m_team == TEAM_COUNTER) {
if (!g_bombPlanted && numFriendsNear (pev->origin, 210.0f) < 4) {
int index = getDefendPoint (m_currentPath->origin);
float campTime = rng.getFloat (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, INVALID_WAYPOINT_INDEX, engine.timebase () + campTime, true); // push camp task on to stack
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, index, engine.timebase () + rng.getFloat (5.0f, 11.0f), true); // push move command
auto &path = waypoints[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_DEFENDING_BOMBSITE); // 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 = task ()->data != INVALID_WAYPOINT_INDEX ? task ()->data : searchGoal ();
m_prevGoalIndex = destIndex;
// remember index
task ()->data = destIndex;
// do pathfinding if it's not the current waypoint
if (destIndex != m_currentWaypointIndex) {
searchPath (m_currentWaypointIndex, destIndex, m_pathType);
}
}
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.boolean () && mp_footsteps.boolean ()) && m_difficulty > 2 && !(m_aimFlags & AIM_ENEMY) && (m_heardSoundTime + 6.0f >= engine.timebase () || (m_states & STATE_SUSPECT_ENEMY)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !yb_jasonmode.boolean () && !g_bombPlanted) {
m_moveSpeed = shiftSpeed;
}
// bot hasn't seen anything in a long time and is asking his teammates to report in
if (m_seeEnemyTime + rng.getFloat (45.0f, 80.0f) < engine.timebase () && rng.getInt (0, 100) < 30 && g_timeRoundStart + 20.0f < engine.timebase () && m_askCheckTime < engine.timebase ()) {
m_askCheckTime = engine.timebase () + rng.getFloat (45.0f, 80.0f);
pushRadioMessage (RADIO_REPORT_TEAM);
}
}
void Bot::spraypaint_ (void) {
m_aimFlags |= AIM_ENTITY;
// bot didn't spray this round?
if (m_timeLogoSpray < engine.timebase () && task ()->time > engine.timebase ()) {
makeVectors (pev->v_angle);
Vector sprayOrigin = eyePos () + g_pGlobals->v_forward * 128.0f;
TraceResult tr;
engine.testLine (eyePos (), sprayOrigin, TRACE_IGNORE_MONSTERS, ent (), &tr);
// no wall in front?
if (tr.flFraction >= 1.0f)
sprayOrigin.z -= 128.0f;
m_entity = sprayOrigin;
if (task ()->time - 0.5f < engine.timebase ()) {
// emit spraycan sound
g_engfuncs.pfnEmitSound (ent (), CHAN_VOICE, "player/sprayer.wav", 1.0f, ATTN_NORM, 0, 100);
engine.testLine (eyePos (), eyePos () + g_pGlobals->v_forward * 128.0f, TRACE_IGNORE_MONSTERS, ent (), &tr);
// paint the actual logo decal
traceDecals (pev, &tr, m_logotypeIndex);
m_timeLogoSpray = engine.timebase () + rng.getFloat (60.0f, 90.0f);
}
}
else {
completeTask ();
}
m_moveToGoal = false;
m_checkTerrain = false;
m_navTimeset = engine.timebase ();
m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f;
ignoreCollision ();
}
void Bot::huntEnemy_ (void) {
m_aimFlags |= AIM_NAVPOINT;
// if we've got new enemy...
if (!engine.isNullEntity (m_enemy) || engine.isNullEntity (m_lastEnemy)) {
// forget about it...
clearTask (TASK_HUNTENEMY);
m_prevGoalIndex = INVALID_WAYPOINT_INDEX;
}
else if (engine.getTeam (m_lastEnemy) == m_team) {
// don't hunt down our teammate...
clearTask (TASK_HUNTENEMY);
m_prevGoalIndex = INVALID_WAYPOINT_INDEX;
m_lastEnemy = nullptr;
}
else if (processNavigation ()) // reached last enemy pos?
{
// forget about it...
completeTask ();
m_prevGoalIndex = INVALID_WAYPOINT_INDEX;
m_lastEnemyOrigin.nullify ();
}
else if (!hasActiveGoal ()) // do we need to calculate a new path?
{
clearSearchNodes ();
int destIndex = INVALID_WAYPOINT_INDEX;
int goal = task ()->data;
// is there a remembered index?
if (waypoints.exists (goal)) {
destIndex = goal;
}
// find new one instead
else {
destIndex = waypoints.getNearest (m_lastEnemyOrigin);
}
// remember index
m_prevGoalIndex = destIndex;
task ()->data = destIndex;
if (destIndex != m_currentWaypointIndex) {
searchPath (m_currentWaypointIndex, destIndex, m_pathType);
}
}
// bots skill higher than 60?
if (yb_walking_allowed.boolean () && mp_footsteps.boolean () && m_difficulty > 1 && !yb_jasonmode.boolean ()) {
// then make him move slow if near enemy
if (!(m_currentTravelFlags & PATHFLAG_JUMP)) {
if (m_currentWaypointIndex != INVALID_WAYPOINT_INDEX) {
if (m_currentPath->radius < 32.0f && !isOnLadder () && !isInWater () && m_seeEnemyTime + 4.0f > engine.timebase () && m_difficulty < 3) {
pev->button |= IN_DUCK;
}
}
if ((m_lastEnemyOrigin - pev->origin).lengthSq () < cr::square (512.0f)) {
m_moveSpeed = getShiftSpeed ();
}
}
}
}
void Bot::seekCover_ (void) {
m_aimFlags |= AIM_NAVPOINT;
if (!isAlive (m_lastEnemy)) {
completeTask ();
m_prevGoalIndex = INVALID_WAYPOINT_INDEX;
}
// reached final waypoint?
else if (processNavigation ()) {
// yep. activate hide behaviour
completeTask ();
m_prevGoalIndex = INVALID_WAYPOINT_INDEX;
// start hide task
startTask (TASK_HIDE, TASKPRI_HIDE, INVALID_WAYPOINT_INDEX, engine.timebase () + rng.getFloat (3.0f, 12.0f), false);
Vector dest = m_lastEnemyOrigin;
// get a valid look direction
getCampDir (&dest);
m_aimFlags |= AIM_CAMP;
m_camp = dest;
m_campDirection = 0;
// chosen waypoint is a camp waypoint?
if (m_currentPath->flags & FLAG_CAMP) {
// use the existing camp wpt prefs
if (m_currentPath->flags & FLAG_CROUCH) {
m_campButtons = IN_DUCK;
}
else {
m_campButtons = 0;
}
}
else {
// choose a crouch or stand pos
if (m_currentPath->vis.crouch <= m_currentPath->vis.stand) {
m_campButtons = IN_DUCK;
}
else {
m_campButtons = 0;
}
// enter look direction from previously calculated positions
m_currentPath->campStartX = dest.x;
m_currentPath->campStartY = dest.y;
m_currentPath->campEndX = dest.x;
m_currentPath->campEndY = dest.y;
}
if (m_reloadState == RELOAD_NONE && ammoClip () < 5 && ammo () != 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 = INVALID_WAYPOINT_INDEX;
if (task ()->data != INVALID_WAYPOINT_INDEX) {
destIndex = task ()->data;
}
else {
destIndex = getCoverPoint (usesSniper () ? 256.0f : 512.0f);
if (destIndex == INVALID_WAYPOINT_INDEX) {
m_retreatTime = engine.timebase () + rng.getFloat (5.0f, 10.0f);
m_prevGoalIndex = INVALID_WAYPOINT_INDEX;
completeTask ();
return;
}
}
m_campDirection = 0;
m_prevGoalIndex = destIndex;
task ()->data = destIndex;
if (destIndex != m_currentWaypointIndex) {
searchPath (m_currentWaypointIndex, destIndex, SEARCH_PATH_FASTEST);
}
}
}
void Bot::attackEnemy_ (void) {
m_moveToGoal = false;
m_checkTerrain = false;
if (!engine.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 = engine.timebase ();
}
void Bot::pause_ (void) {
m_moveToGoal = false;
m_checkTerrain = false;
m_navTimeset = engine.timebase ();
m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f;
m_aimFlags |= AIM_NAVPOINT;
// 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;
}
makeVectors (pev->v_angle);
m_camp = eyePos () + g_pGlobals->v_forward * 500.0f;
m_aimFlags |= AIM_OVERRIDE;
m_wantsToFire = true;
}
else {
pev->button |= m_campButtons;
}
// stop camping if time over or gets hurt by something else than bullets
if (task ()->time < engine.timebase () || m_lastDamageType > 0) {
completeTask ();
}
}
void Bot::blind_ (void) {
m_moveToGoal = false;
m_checkTerrain = false;
m_navTimeset = engine.timebase ();
// if bot remembers last enemy position
if (m_difficulty >= 2 && !m_lastEnemyOrigin.empty () && 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 < engine.timebase ()) {
completeTask ();
}
}
void Bot::camp_ (void) {
if (!yb_camping_allowed.boolean ()) {
completeTask ();
return;
}
m_aimFlags |= AIM_CAMP;
m_checkTerrain = false;
m_moveToGoal = false;
if (m_team == TEAM_COUNTER && g_bombPlanted && m_defendedBomb && !isBombDefusing (waypoints.getBombPos ()) && !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 = engine.timebase ();
m_timeCamping = engine.timebase ();
m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f;
getValidPoint ();
if (m_nextCampDirTime < engine.timebase ()) {
m_nextCampDirTime = engine.timebase () + rng.getFloat (2.0f, 5.0f);
if (m_currentPath->flags & FLAG_CAMP) {
Vector dest;
// switch from 1 direction to the other
if (m_campDirection < 1) {
dest.x = m_currentPath->campStartX;
dest.y = m_currentPath->campStartY;
m_campDirection ^= 1;
}
else {
dest.x = m_currentPath->campEndX;
dest.y = m_currentPath->campEndY;
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 < waypoints.length (); i++) {
// skip invisible waypoints or current waypoint
if (!waypoints.isVisible (m_currentWaypointIndex, i) || (i == m_currentWaypointIndex)) {
continue;
}
const Vector &dotB = (waypoints[i].origin - pev->origin).normalize2D ();
if ((dotA | dotB) > 0.9f) {
int distance = static_cast <int> ((pev->origin - waypoints[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 = waypoints[campPoints[rng.getInt (0, numFoundPoints)]].origin;
}
else {
m_camp = waypoints[searchCampDir ()].origin;
}
}
else
m_camp = waypoints[searchCampDir ()].origin;
}
// press remembered crouch button
pev->button |= m_campButtons;
// stop camping if time over or gets hurt by something else than bullets
if (task ()->time < engine.timebase () || m_lastDamageType > 0) {
completeTask ();
}
}
void Bot::hide_ (void) {
m_aimFlags |= AIM_CAMP;
m_checkTerrain = false;
m_moveToGoal = false;
// half the reaction time if camping
setIdealReactionTimers ();
m_idealReactionTime *= 0.5f;
m_navTimeset = engine.timebase ();
m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f;
getValidPoint ();
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 & STATE_SEEING_ENEMY) || m_inBombZone) {
if (!(m_currentPath->flags & FLAG_CAMP)) {
completeTask ();
m_campButtons = 0;
m_prevGoalIndex = INVALID_WAYPOINT_INDEX;
if (!engine.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 = INVALID_WAYPOINT_INDEX;
if (taskId () == TASK_HIDE) {
completeTask ();
}
return;
}
pev->button |= m_campButtons;
m_navTimeset = engine.timebase ();
// stop camping if time over or gets hurt by something else than bullets
if (task ()->time < engine.timebase () || m_lastDamageType > 0) {
completeTask ();
}
}
void Bot::moveToPos_ (void) {
m_aimFlags |= AIM_NAVPOINT;
if (isShieldDrawn ()) {
pev->button |= IN_ATTACK2;
}
// reached destination?
if (processNavigation ()) {
completeTask (); // we're done
m_prevGoalIndex = INVALID_WAYPOINT_INDEX;
m_position.nullify ();
}
// didn't choose goal waypoint yet?
else if (!hasActiveGoal ()) {
clearSearchNodes ();
int destIndex = INVALID_WAYPOINT_INDEX;
int goal = task ()->data;
if (waypoints.exists (goal)) {
destIndex = goal;
}
else {
destIndex = waypoints.getNearest (m_position);
}
if (waypoints.exists (destIndex)) {
m_prevGoalIndex = destIndex;
task ()->data = destIndex;
searchPath (m_currentWaypointIndex, destIndex, m_pathType);
}
else {
completeTask ();
}
}
}
void Bot::plantBomb_ (void) {
m_aimFlags |= AIM_CAMP;
// we're still got the C4?
if (m_hasC4) {
selectWeaponByName ("weapon_c4");
if (isAlive (m_enemy) || !m_inBombZone) {
completeTask ();
}
else {
m_moveToGoal = false;
m_checkTerrain = false;
m_navTimeset = engine.timebase ();
if (m_currentPath->flags & FLAG_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_NEED_BACKUP);
}
clearSearchNodes ();
int index = getDefendPoint (pev->origin);
float guardTime = mp_c4timer.flt () * 0.5f + mp_c4timer.flt () * 0.25f;
// push camp task on to stack
startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, engine.timebase () + guardTime, true);
// push move command
startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, index, engine.timebase () + guardTime, true);
if (waypoints[index].vis.crouch <= waypoints[index].vis.stand) {
m_campButtons |= IN_DUCK;
}
else {
m_campButtons &= ~IN_DUCK;
}
}
}
void Bot::bombDefuse_ (void) {
float fullDefuseTime = m_hasDefuser ? 7.0f : 12.0f;
float timeToBlowUp = getBombTimeleft ();
float defuseRemainingTime = fullDefuseTime;
if (m_hasProgressBar /*&& isOnFloor ()*/) {
defuseRemainingTime = fullDefuseTime - engine.timebase ();
}
bool pickupExists = !engine.isNullEntity (m_pickupItem);
const Vector &bombPos = pickupExists ? m_pickupItem->v.origin : waypoints.getBombPos ();
if (pickupExists) {
if (waypoints.getBombPos () != bombPos) {
waypoints.setBombPos (bombPos);
}
}
bool defuseError = false;
// exception: bomb has been defused
if (bombPos.empty ()) {
defuseError = true;
g_bombPlanted = false;
if (rng.getInt (0, 100) < 50 && m_numFriendsLeft != 0) {
if (timeToBlowUp <= 3.0) {
if (yb_communication_type.integer () == 2) {
instantChatter (CHATTER_BARELY_DEFUSED);
}
else if (yb_communication_type.integer () == 1) {
pushRadioMessage (RADIO_SECTOR_CLEAR);
}
}
else {
pushRadioMessage (RADIO_SECTOR_CLEAR);
}
}
}
else if (defuseRemainingTime > timeToBlowUp) {
defuseError = true;
}
else if (m_states & STATE_SEEING_ENEMY) {
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_NEED_BACKUP);
}
}
}
// one of exceptions is thrown. finish task.
if (defuseError) {
m_checkTerrain = true;
m_moveToGoal = true;
m_destOrigin.nullify ();
m_entity.nullify ();
m_pickupItem = nullptr;
m_pickupType = PICKUP_NONE;
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 || ((ammoClip () > 8 && m_reloadState == RELOAD_PRIMARY) || (ammoClip () > 5 && m_reloadState == RELOAD_SECONDARY))) {
int weaponIndex = bestWeaponCarried ();
// just select knife and then select weapon
selectWeaponByName ("weapon_knife");
if (weaponIndex > 0 && weaponIndex < NUM_WEAPONS) {
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 |= AIM_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 < engine.timebase ()) {
if (m_difficulty >= 2 && m_numEnemiesLeft != 0) {
m_duckDefuse = true;
}
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 = true; // duck
}
}
m_duckDefuseCheckTime = engine.timebase () + 1.5f;
}
// 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 = engine.timebase ();
// 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_DEFUSING_BOMB);
if (numFriendsNear (pev->origin, 512.0f) < 2) {
pushRadioMessage (RADIO_NEED_BACKUP);
}
}
}
else
completeTask ();
}
void Bot::followUser_ (void) {
if (engine.isNullEntity (m_targetEntity) || !isAlive (m_targetEntity)) {
m_targetEntity = nullptr;
completeTask ();
return;
}
if (m_targetEntity->v.button & IN_ATTACK) {
makeVectors (m_targetEntity->v.v_angle);
TraceResult tr;
engine.testLine (m_targetEntity->v.origin + m_targetEntity->v.view_ofs, g_pGlobals->v_forward * 500.0f, TRACE_IGNORE_EVERYTHING, ent (), &tr);
if (!engine.isNullEntity (tr.pHit) && isPlayer (tr.pHit) && engine.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 && ammo () != 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 = engine.timebase ();
}
else {
if (m_followWaitTime + 3.0f < engine.timebase ()) {
// stop following if we have been waiting too long
m_targetEntity = nullptr;
pushRadioMessage (RADIO_YOU_TAKE_THE_POINT);
completeTask ();
return;
}
}
}
m_aimFlags |= AIM_NAVPOINT;
if (yb_walking_allowed.boolean () && m_targetEntity->v.maxspeed < m_moveSpeed && !yb_jasonmode.boolean ()) {
m_moveSpeed = getShiftSpeed ();
}
if (isShieldDrawn ()) {
pev->button |= IN_ATTACK2;
}
// reached destination?
if (processNavigation ()) {
task ()->data = INVALID_WAYPOINT_INDEX;
}
// didn't choose goal waypoint yet?
if (!hasActiveGoal ()) {
clearSearchNodes ();
int destIndex = waypoints.getNearest (m_targetEntity->v.origin);
IntArray points = waypoints.searchRadius (200.0f, m_targetEntity->v.origin);
for (auto &newIndex : points) {
// if waypoint not yet used, assign it as dest
if (newIndex != m_currentWaypointIndex && !isOccupiedPoint (newIndex)) {
destIndex = newIndex;
}
}
if (waypoints.exists (destIndex) && waypoints.exists (m_currentWaypointIndex)) {
m_prevGoalIndex = destIndex;
task ()->data = destIndex;
// always take the shortest path
searchShortestPath (m_currentWaypointIndex, destIndex);
}
else {
m_targetEntity = nullptr;
completeTask ();
}
}
}
void Bot::throwExplosive_ (void) {
m_aimFlags |= AIM_GRENADE;
Vector dest = m_throw;
if (!(m_states & STATE_SEEING_ENEMY)) {
m_strafeSpeed = 0.0f;
m_moveSpeed = 0.0f;
m_moveToGoal = false;
}
else if (!(m_states & STATE_SUSPECT_ENEMY) && !engine.isNullEntity (m_enemy)) {
dest = m_enemy->v.origin + m_enemy->v.velocity.make2D () * 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 = engine.timebase () + MAX_GRENADE_TIMER;
selectBestWeapon ();
completeTask ();
return;
}
m_grenade = calcThrow (eyePos (), dest);
if (m_grenade.lengthSq () < 100.0f) {
m_grenade = calcToss (pev->origin, dest);
}
if (m_grenade.lengthSq () <= 100.0f) {
m_grenadeCheckTime = engine.timebase () + MAX_GRENADE_TIMER;
selectBestWeapon ();
completeTask ();
}
else {
auto grenade = correctGrenadeVelocity ("hegrenade.mdl");
if (engine.isNullEntity (grenade)) {
if (m_currentWeapon != WEAPON_EXPLOSIVE && !m_grenadeRequested) {
if (pev->weapons & (1 << 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_ (void) {
m_aimFlags |= AIM_GRENADE;
Vector dest = m_throw;
if (!(m_states & STATE_SEEING_ENEMY)) {
m_strafeSpeed = 0.0f;
m_moveSpeed = 0.0f;
m_moveToGoal = false;
}
else if (!(m_states & STATE_SUSPECT_ENEMY) && !engine.isNullEntity (m_enemy)) {
dest = m_enemy->v.origin + m_enemy->v.velocity.make2D () * 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 = engine.timebase () + MAX_GRENADE_TIMER;
selectBestWeapon ();
completeTask ();
return;
}
m_grenade = calcThrow (eyePos (), dest);
if (m_grenade.lengthSq () < 100.0f) {
m_grenade = calcToss (pev->origin, dest);
}
if (m_grenade.lengthSq () <= 100.0f) {
m_grenadeCheckTime = engine.timebase () + MAX_GRENADE_TIMER;
selectBestWeapon ();
completeTask ();
}
else {
auto grenade = correctGrenadeVelocity ("flashbang.mdl");
if (engine.isNullEntity (grenade)) {
if (m_currentWeapon != WEAPON_FLASHBANG && !m_grenadeRequested) {
if (pev->weapons & (1 << 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_ (void) {
m_aimFlags |= AIM_GRENADE;
if (!(m_states & STATE_SEEING_ENEMY)) {
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 (!engine.isNullEntity (m_enemy))
src = src + m_enemy->v.velocity * 0.5f;
m_grenade = (src - eyePos ()).normalize ();
if (task ()->time < engine.timebase ()) {
completeTask ();
return;
}
if (m_currentWeapon != WEAPON_SMOKE && !m_grenadeRequested) {
if (pev->weapons & (1 << WEAPON_SMOKE)) {
m_grenadeRequested = true;
selectWeaponByName ("weapon_smokegrenade");
task ()->time = engine.timebase () + 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_ (void) {
if (!isAlive (m_doubleJumpEntity) || (m_aimFlags & AIM_ENEMY) || (m_travelStartIndex != INVALID_WAYPOINT_INDEX && task ()->time + (waypoints.calculateTravelTime (pev->maxspeed, waypoints[m_travelStartIndex].origin, m_doubleJumpOrigin) + 11.0f) < engine.timebase ())) {
resetDoubleJump ();
return;
}
m_aimFlags |= AIM_NAVPOINT;
if (m_jumpReady) {
m_moveToGoal = false;
m_checkTerrain = false;
m_navTimeset = engine.timebase ();
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 < engine.timebase ()) {
pev->button |= IN_DUCK;
}
else if (inJump && !(m_oldButtons & IN_JUMP)) {
pev->button |= IN_JUMP;
}
makeVectors (Vector (0.0f, pev->angles.y, 0.0f));
Vector src = pev->origin + Vector (0.0f, 0.0f, 45.0f);
Vector dest = src + g_pGlobals->v_up * 256.0f;
TraceResult tr;
engine.testLine (src, dest, TRACE_IGNORE_NONE, ent (), &tr);
if (tr.flFraction < 1.0f && tr.pHit == m_doubleJumpEntity && inJump) {
m_duckForJump = engine.timebase () + rng.getFloat (3.0f, 5.0f);
task ()->time = engine.timebase ();
}
return;
}
if (m_currentWaypointIndex == m_prevGoalIndex) {
m_waypointOrigin = m_doubleJumpOrigin;
m_destOrigin = m_doubleJumpOrigin;
}
if (processNavigation ()) {
task ()->data = INVALID_WAYPOINT_INDEX;
}
// didn't choose goal waypoint yet?
if (!hasActiveGoal ()) {
clearSearchNodes ();
int destIndex = waypoints.getNearest (m_doubleJumpOrigin);
if (waypoints.exists (destIndex)) {
m_prevGoalIndex = destIndex;
task ()->data = destIndex;
m_travelStartIndex = m_currentWaypointIndex;
// always take the shortest path
searchShortestPath (m_currentWaypointIndex, destIndex);
if (m_currentWaypointIndex == destIndex) {
m_jumpReady = true;
}
}
else {
resetDoubleJump ();
}
}
}
void Bot::escapeFromBomb_ (void) {
m_aimFlags |= AIM_NAVPOINT;
if (!g_bombPlanted) {
completeTask ();
}
if (isShieldDrawn ()) {
pev->button |= IN_ATTACK2;
}
if (m_currentWeapon != WEAPON_KNIFE && m_numEnemiesLeft == 0) {
selectWeaponByName ("weapon_knife");
}
// reached destination?
if (processNavigation ()) {
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, INVALID_WAYPOINT_INDEX, engine.timebase () + 10.0f, true);
}
// didn't choose goal waypoint yet?
else if (!hasActiveGoal ()) {
clearSearchNodes ();
int lastSelectedGoal = INVALID_WAYPOINT_INDEX, minPathDistance = 99999;
float safeRadius = rng.getFloat (1248.0f, 2048.0f);
for (int i = 0; i < waypoints.length (); i++) {
if ((waypoints[i].origin - waypoints.getBombPos ()).length () < safeRadius || isOccupiedPoint (i)) {
continue;
}
int pathDistance = waypoints.getPathDist (m_currentWaypointIndex, i);
if (minPathDistance > pathDistance) {
minPathDistance = pathDistance;
lastSelectedGoal = i;
}
}
if (lastSelectedGoal < 0) {
lastSelectedGoal = waypoints.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, INVALID_WAYPOINT_INDEX, engine.timebase () + 10.0f, true);
return;
}
m_prevGoalIndex = lastSelectedGoal;
task ()->data = lastSelectedGoal;
searchShortestPath (m_currentWaypointIndex, lastSelectedGoal);
}
}
void Bot::shootBreakable_ (void) {
m_aimFlags |= AIM_OVERRIDE;
// Breakable destroyed?
if (engine.isNullEntity (lookupBreakable ())) {
completeTask ();
return;
}
pev->button |= m_campButtons;
m_checkTerrain = false;
m_moveToGoal = false;
m_navTimeset = engine.timebase ();
Vector src = m_breakableOrigin;
m_camp = src;
// is bot facing the breakable?
if (getShootingConeDeviation (ent (), src) >= 0.90f) {
m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f;
if (m_currentWeapon == WEAPON_KNIFE) {
selectBestWeapon ();
}
m_wantsToFire = true;
}
else {
m_checkTerrain = true;
m_moveToGoal = true;
}
}
void Bot::pickupItem_ () {
if (engine.isNullEntity (m_pickupItem)) {
m_pickupItem = nullptr;
completeTask ();
return;
}
Vector dest = engine.getAbsPos (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_DROPPED_C4:
case PICKUP_NONE:
break;
case PICKUP_WEAPON:
m_aimFlags |= AIM_NAVPOINT;
// near to weapon?
if (itemDistance < 50.0f) {
int id = 0;
for (id = 0; id < 7; id++) {
if (strcmp (g_weaponSelect[id].modelName, STRING (m_pickupItem->v.model) + 9) == 0) {
break;
}
}
if (id < 7) {
// secondary weapon. i.e., pistol
int wid = 0;
for (id = 0; id < 7; id++) {
if (pev->weapons & (1 << g_weaponSelect[id].id)) {
wid = id;
}
}
if (wid > 0) {
selectWeaponById (wid);
engine.execBotCmd (ent (), "drop");
if (hasShield ()) {
engine.execBotCmd (ent (), "drop"); // discard both shield and pistol
}
}
processBuyzoneEntering (BUYSTATE_PRIMARY_WEAPON);
}
else {
// primary weapon
int wid = bestWeaponCarried ();
if (wid == WEAPON_SHIELD || wid > 6 || hasShield ()) {
selectWeaponById (wid);
engine.execBotCmd (ent (), "drop");
}
if (!wid) {
m_itemIgnore = m_pickupItem;
m_pickupItem = nullptr;
m_pickupType = PICKUP_NONE;
break;
}
processBuyzoneEntering (BUYSTATE_PRIMARY_WEAPON);
}
checkSilencer (); // check the silencer
}
break;
case PICKUP_SHIELD:
m_aimFlags |= AIM_NAVPOINT;
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);
engine.execBotCmd (ent (), "drop");
}
}
break;
case PICKUP_PLANTED_C4:
m_aimFlags |= AIM_ENTITY;
if (m_team == TEAM_COUNTER && itemDistance < 80.0f) {
pushChatterMessage (CHATTER_DEFUSING_BOMB);
// notify team of defusing
if (m_numFriendsLeft < 3) {
pushRadioMessage (RADIO_NEED_BACKUP);
}
m_moveToGoal = false;
m_checkTerrain = false;
m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f;
startTask (TASK_DEFUSEBOMB, TASKPRI_DEFUSEBOMB, INVALID_WAYPOINT_INDEX, 0.0f, false);
}
break;
case PICKUP_HOSTAGE:
m_aimFlags |= AIM_ENTITY;
if (!isAlive (m_pickupItem)) {
// don't pickup dead hostages
m_pickupItem = nullptr;
completeTask ();
break;
}
if (itemDistance < 50.0f) {
float angleToEntity = isInFOV (dest - eyePos ());
// 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 (rng.getInt (0, 100) < 80) {
pushChatterMessage (CHATTER_USING_HOSTAGES);
}
m_hostages.push (m_pickupItem);
m_pickupItem = nullptr;
}
ignoreCollision (); // also don't consider being stuck
}
break;
case PICKUP_DEFUSEKIT:
m_aimFlags |= AIM_NAVPOINT;
if (m_hasDefuser) {
m_pickupItem = nullptr;
m_pickupType = PICKUP_NONE;
}
break;
case PICKUP_BUTTON:
m_aimFlags |= AIM_ENTITY;
if (engine.isNullEntity (m_pickupItem) || m_buttonPushTime < engine.timebase ()) {
completeTask ();
m_pickupType = PICKUP_NONE;
break;
}
// find angles from bot origin to entity...
float angleToEntity = isInFOV (dest - eyePos ());
// 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 = engine.timebase () + 3.0f;
completeTask ();
}
}
break;
}
}
void Bot::processTasks (void) {
// this is core function that handle task execution
switch (taskId ()) {
// normal task
default:
case TASK_NORMAL:
normal_ ();
break;
// bot sprays messy logos all over the place...
case TASK_SPRAY:
spraypaint_ ();
break;
// hunt down enemy
case TASK_HUNTENEMY:
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_BLINDED:
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_THROWHEGRENADE:
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 (void) {
// 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_checkWeaponSwitch && m_buyingFinished && m_spawnTime + rng.getFloat (4.0f, 6.5f) < engine.timebase ()) {
if (rng.getInt (1, 100) < 2 && yb_spraypaints.boolean ()) {
startTask (TASK_SPRAY, TASKPRI_SPRAYLOGO, INVALID_WAYPOINT_INDEX, engine.timebase () + 1.0f, false);
}
if (m_difficulty >= 2 && rng.getInt (0, 100) < (m_personality == PERSONALITY_RUSHER ? 99 : 50) && !m_isReloading && (g_mapFlags & (MAP_CS | MAP_DE | MAP_ES | MAP_AS))) {
if (yb_jasonmode.boolean ()) {
selectSecondary ();
engine.execBotCmd (ent (), "drop");
}
else {
selectWeaponByName ("weapon_knife");
}
}
m_checkKnifeSwitch = false;
if (rng.getInt (0, 100) < yb_user_follow_percent.integer () && engine.isNullEntity (m_targetEntity) && !m_isLeader && !m_hasC4 && rng.getInt (0, 100) > 50) {
decideFollowUser ();
}
}
// check if we already switched weapon mode
if (m_checkWeaponSwitch && m_buyingFinished && m_spawnTime + rng.getFloat (2.0f, 3.5f) < engine.timebase ()) {
if (hasShield () && isShieldDrawn ()) {
pev->button |= IN_ATTACK2;
}
else {
switch (m_currentWeapon) {
case WEAPON_M4A1:
case WEAPON_USP:
checkSilencer ();
break;
case WEAPON_FAMAS:
case WEAPON_GLOCK:
if (rng.getInt (0, 100) < 50) {
pev->button |= IN_ATTACK2;
}
break;
}
}
m_checkWeaponSwitch = false;
}
}
void Bot::ai (void) {
// 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 > engine.timebase ()) {
m_maxViewDistance = 4096.0f;
}
m_moveSpeed = pev->maxspeed;
if (m_prevTime <= engine.timebase ()) {
// 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 = engine.timebase () + 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_communication_type.integer () == 2) {
if ((m_states & STATE_SEEING_ENEMY) && !engine.isNullEntity (m_enemy)) {
int hasFriendNearby = numFriendsNear (pev->origin, 512.0f);
if (!hasFriendNearby && rng.getInt (0, 100) < 45 && (m_enemy->v.weapons & (1 << WEAPON_C4))) {
pushChatterMessage (CHATTER_SPOT_THE_BOMBER);
}
else if (!hasFriendNearby && rng.getInt (0, 100) < 45 && m_team == TEAM_TERRORIST && isPlayerVIP (m_enemy)) {
pushChatterMessage (CHATTER_VIP_SPOTTED);
}
else if (!hasFriendNearby && rng.getInt (0, 100) < 50 && engine.getTeam (m_enemy) != m_team && isGroupOfEnemies (m_enemy->v.origin, 2, 384)) {
pushChatterMessage (CHATTER_SCARED_EMOTE);
}
else if (!hasFriendNearby && rng.getInt (0, 100) < 40 && ((m_enemy->v.weapons & (1 << WEAPON_AWP)) || (m_enemy->v.weapons & (1 << WEAPON_SCOUT)) || (m_enemy->v.weapons & (1 << WEAPON_G3SG1)) || (m_enemy->v.weapons & (1 << WEAPON_SG550)))) {
pushChatterMessage (CHATTER_SNIPER_WARNING);
}
// if bot is trapped under shield yell for help !
if (taskId () == TASK_CAMP && hasShield () && isShieldDrawn () && hasFriendNearby >= 2 && seesEnemy (m_enemy)) {
instantChatter (CHATTER_PINNED_DOWN);
}
}
// if bomb planted warn teammates !
if (g_canSayBombPlanted && g_bombPlanted && m_team == TEAM_COUNTER) {
g_canSayBombPlanted = false;
pushChatterMessage (CHATTER_GOTTA_FIND_BOMB);
}
}
Vector src, destination;
m_checkTerrain = true;
m_moveToGoal = true;
m_wantsToFire = false;
avoidGrenades (); // avoid flyings grenades
m_isUsingGrenade = false;
processTasks (); // execute current task
updateAimDir (); // choose aim direction
processLookAngles (); // and turn to chosen aim direction
// the bots wants to fire at something?
if (m_wantsToFire && !m_isUsingGrenade && m_shootTime <= engine.timebase ()) {
fireWeapons (); // if bot didn't fire a bullet try again next frame
}
// check for reloading
if (m_reloadCheckTime <= engine.timebase ()) {
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 * calcThinkInterval ());
const Vector &dirNormal = dirOld.normalize2D ();
m_moveAngles = dirOld.toAngles ();
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) {
getValidPoint ();
// press duck button if we need to
if ((m_currentPath->flags & FLAG_CROUCH) && !(m_currentPath->flags & (FLAG_CAMP | FLAG_GOAL))) {
pev->button |= IN_DUCK;
}
m_timeWaypointMove = engine.timebase ();
// special movement for swimming here
if (isInWater ()) {
// check if we need to go forward or back press the correct buttons
if (isInFOV (m_destOrigin - eyePos ()) > 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);
}
// 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 () < engine.timebase () && engine.isNullEntity (m_enemy)) {
getValidPoint ();
// clear these pointers, bot mingh be stuck getting to them
if (!engine.isNullEntity (m_pickupItem) && !m_hasProgressBar) {
m_itemIgnore = m_pickupItem;
}
m_pickupItem = nullptr;
m_breakableEntity = nullptr;
m_itemCheckTime = engine.timebase () + 5.0f;
m_pickupType = PICKUP_NONE;
}
if (m_duckTime >= engine.timebase ()) {
pev->button |= IN_DUCK;
}
if (pev->button & IN_JUMP) {
m_jumpTime = engine.timebase ();
}
if (m_jumpTime + 0.85f > engine.timebase ()) {
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;
}
}
// display some debugging thingy to host entity
if (!engine.isNullEntity (g_hostEntity) && yb_debug.integer () >= 1) {
showDebugOverlay ();
}
// save the previous speed (for checking if stuck)
m_prevSpeed = cr::abs (m_moveSpeed);
m_lastDamageType = -1; // reset damage
}
void Bot::showDebugOverlay (void) {
bool displayDebugOverlay = false;
if (g_hostEntity->v.iuser2 == index ()) {
displayDebugOverlay = true;
}
if (!displayDebugOverlay && yb_debug.integer () >= 2) {
Bot *nearest = nullptr;
if (findNearestPlayer (reinterpret_cast <void **> (&nearest), g_hostEntity, 128.0f, false, true, true, true) && nearest == this) {
displayDebugOverlay = true;
}
}
if (displayDebugOverlay) {
static bool s_mapsFilled = false;
static float timeDebugUpdate = 0.0f;
static int index, goal, taskID;
static HashMap <int, String, IntHash <int>> tasks;
static HashMap <int, String, IntHash <int>> personalities;
static HashMap <int, String, IntHash <int>> flags;
if (!s_mapsFilled) {
tasks.put (TASK_NORMAL, "Normal");
tasks.put (TASK_PAUSE, "Pause");
tasks.put (TASK_MOVETOPOSITION, "Move");
tasks.put (TASK_FOLLOWUSER, "Follow");
tasks.put (TASK_PICKUPITEM, "Pickup");
tasks.put (TASK_CAMP, "Camp");
tasks.put (TASK_PLANTBOMB, "PlantBomb");
tasks.put (TASK_DEFUSEBOMB, "DefuseBomb");
tasks.put (TASK_ATTACK, "Attack");
tasks.put (TASK_HUNTENEMY, "Hunt");
tasks.put (TASK_SEEKCOVER, "SeekCover");
tasks.put (TASK_THROWHEGRENADE, "ThrowHE");
tasks.put (TASK_THROWFLASHBANG, "ThrowFL");
tasks.put (TASK_THROWSMOKE, "ThrowSG");
tasks.put (TASK_DOUBLEJUMP, "DoubleJump");
tasks.put (TASK_ESCAPEFROMBOMB, "EscapeFromBomb");
tasks.put (TASK_SHOOTBREAKABLE, "DestroyBreakable");
tasks.put (TASK_HIDE, "Hide");
tasks.put (TASK_BLINDED, "Blind");
tasks.put (TASK_SPRAY, "Spray");
personalities.put (PERSONALITY_RUSHER, "Rusher");
personalities.put (PERSONALITY_NORMAL, "Normal");
personalities.put (PERSONALITY_CAREFUL, "Careful");
flags.put (AIM_NAVPOINT, "Nav");
flags.put (AIM_CAMP, "Camp");
flags.put (AIM_PREDICT_PATH, "Predict");
flags.put (AIM_LAST_ENEMY, "LastEnemy");
flags.put (AIM_ENTITY, "Entity");
flags.put (AIM_ENEMY, "Enemy");
flags.put (AIM_GRENADE, "Grenade");
flags.put (AIM_OVERRIDE, "Override");
s_mapsFilled = true;
}
if (!m_tasks.empty ()) {
if (taskID != taskId () || index != m_currentWaypointIndex || goal != task ()->data || timeDebugUpdate < engine.timebase ()) {
taskID = taskId ();
index = m_currentWaypointIndex;
goal = task ()->data;
String enemy = "(none)";
if (!engine.isNullEntity (m_enemy)) {
enemy = STRING (m_enemy->v.netname);
}
else if (!engine.isNullEntity (m_lastEnemy)) {
enemy.format ("%s (L)", STRING (m_lastEnemy->v.netname));
}
String pickup = "(none)";
if (!engine.isNullEntity (m_pickupItem)) {
pickup = STRING (m_pickupItem->v.netname);
}
String aimFlags;
for (int i = 0; i < 8; i++) {
bool hasFlag = m_aimFlags & (1 << i);
if (hasFlag) {
aimFlags.formatAppend (" %s", flags[1 << i].chars ());
}
}
String weapon = STRING (getWeaponData (true, nullptr, m_currentWeapon));
String debugData;
debugData.format ("\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 (), task ()->desire, weapon.chars (), ammoClip (), ammo (), m_isReloading ? " (R)" : "", m_moneyAmount, aimFlags.trim ().chars (), m_moveSpeed, m_strafeSpeed, index, m_prevGoalIndex, goal, m_navTimeset - engine.timebase (), pev->movetype, enemy.chars (), pickup.chars (), personalities[m_personality].chars ());
MessageWriter (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, Vector::null (), g_hostEntity)
.writeByte (TE_TEXTMESSAGE)
.writeByte (1)
.writeShort (MessageWriter::fs16 (-1, 1 << 13))
.writeShort (MessageWriter::fs16 (0, 1 << 13))
.writeByte (0)
.writeByte (m_team == TEAM_COUNTER ? 0 : 255)
.writeByte (100)
.writeByte (m_team != TEAM_COUNTER ? 0 : 255)
.writeByte (0)
.writeByte (255)
.writeByte (255)
.writeByte (255)
.writeByte (0)
.writeShort (MessageWriter::fu16 (0, 1 << 8))
.writeShort (MessageWriter::fu16 (0, 1 << 8))
.writeShort (MessageWriter::fu16 (1.0, 1 << 8))
.writeString (debugData.chars ());
timeDebugUpdate = engine.timebase () + 1.0f;
}
// green = destination origin
// blue = ideal angles
// red = view angles
engine.drawLine (g_hostEntity, eyePos (), m_destOrigin, 10, 0, 0, 255, 0, 250, 5, 1, DRAW_ARROW);
makeVectors (m_idealAngles);
engine.drawLine (g_hostEntity, eyePos () - Vector (0.0f, 0.0f, 16.0f), eyePos () + g_pGlobals->v_forward * 300.0f, 10, 0, 0, 0, 255, 250, 5, 1, DRAW_ARROW);
makeVectors (pev->v_angle);
engine.drawLine (g_hostEntity, eyePos () - Vector (0.0f, 0.0f, 32.0f), eyePos () + g_pGlobals->v_forward * 300.0f, 10, 0, 255, 0, 0, 250, 5, 1, DRAW_ARROW);
// now draw line from source to destination
for (size_t i = 0; i < m_path.length () && i + 1 < m_path.length (); i++) {
engine.drawLine (g_hostEntity, waypoints[m_path[i]].origin, waypoints[m_path[i + 1]].origin, 15, 0, 255, 100, 55, 200, 5, 1, DRAW_ARROW);
}
}
}
}
bool Bot::hasHostage (void) {
for (auto hostage : m_hostages) {
if (!engine.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::ammo (void) {
if (g_weaponDefs[m_currentWeapon].ammo1 == -1 || g_weaponDefs[m_currentWeapon].ammo1 > MAX_WEAPONS - 1) {
return 0;
}
return m_ammo[g_weaponDefs[m_currentWeapon].ammo1];
}
void Bot::processDamage (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;
collectGoalExperience (damage, m_team);
if (isPlayer (inflictor)) {
if (yb_tkpunish.boolean () && engine.getTeam (inflictor) == m_team && !isFakeClient (inflictor)) {
// alright, die you teamkiller!!!
m_actualReactionTime = 0.0f;
m_seeEnemyTime = engine.timebase ();
m_enemy = inflictor;
m_lastEnemy = m_enemy;
m_lastEnemyOrigin = m_enemy->v.origin;
m_enemyOrigin = m_enemy->v.origin;
pushChatMessage (CHAT_TEAMATTACK);
processChatterMessage ("#Bot_TeamAttack");
pushChatterMessage (CHATTER_FRIENDLY_FIRE);
}
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 (engine.isNullEntity (m_enemy) && m_team != engine.getTeam (inflictor)) {
m_lastEnemy = inflictor;
m_lastEnemyOrigin = inflictor->v.origin;
// FIXME - Bot doesn't necessary sees this enemy
m_seeEnemyTime = engine.timebase ();
}
if (!(g_gameFlags & GAME_CSDM)) {
collectDataExperience (inflictor, armor + damage);
}
}
}
// hurt by unusual damage like drowning or gas
else {
// leave the camping/hiding position
if (!waypoints.isReachable (this, waypoints.getNearest (m_destOrigin))) {
clearSearchNodes ();
searchOptimalPoint ();
}
}
}
void Bot::processBlind (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 = rng.getFloat (10.0f, 20.0f);
m_blindTime = engine.timebase () + static_cast <float> (alpha - 200) / 16.0f;
if (m_blindTime < engine.timebase ()) {
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 (rng.getInt (0, 100) > 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::collectGoalExperience (int damage, int team) {
// 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 (waypoints.length () < 1 || waypoints.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) {
if (team == TEAM_TERRORIST) {
int value = (g_experienceData + (m_chosenGoalIndex * waypoints.length ()) + m_prevGoalIndex)->team0Value;
value -= static_cast <int> (pev->health / 20);
if (value < -MAX_GOAL_VALUE) {
value = -MAX_GOAL_VALUE;
}
else if (value > MAX_GOAL_VALUE) {
value = MAX_GOAL_VALUE;
}
(g_experienceData + (m_chosenGoalIndex * waypoints.length ()) + m_prevGoalIndex)->team0Value = static_cast <int16> (value);
}
else {
int value = (g_experienceData + (m_chosenGoalIndex * waypoints.length ()) + m_prevGoalIndex)->team1Value;
value -= static_cast <int> (pev->health / 20);
if (value < -MAX_GOAL_VALUE) {
value = -MAX_GOAL_VALUE;
}
else if (value > MAX_GOAL_VALUE) {
value = MAX_GOAL_VALUE;
}
(g_experienceData + (m_chosenGoalIndex * waypoints.length ()) + m_prevGoalIndex)->team1Value = static_cast <int16> (value);
}
}
}
void Bot::collectDataExperience (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 (!isPlayer (attacker)) {
return;
}
int attackerTeam = engine.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.getBot (attacker) != nullptr) {
bots.getBot (attacker)->m_goalValue += static_cast <float> (damage);
}
if (damage < 20) {
return; // do not collect damage less than 20
}
int attackerIndex = waypoints.getNearest (attacker->v.origin);
int victimIndex = m_currentWaypointIndex;
if (victimIndex == INVALID_WAYPOINT_INDEX) {
victimIndex = getNearestPoint ();
}
if (pev->health > 20.0f) {
if (victimTeam == TEAM_TERRORIST) {
(g_experienceData + (victimIndex * waypoints.length ()) + victimIndex)->team0Damage++;
}
else {
(g_experienceData + (victimIndex * waypoints.length ()) + victimIndex)->team1Damage++;
}
if ((g_experienceData + (victimIndex * waypoints.length ()) + victimIndex)->team0Damage > MAX_DAMAGE_VALUE) {
(g_experienceData + (victimIndex * waypoints.length ()) + victimIndex)->team0Damage = MAX_DAMAGE_VALUE;
}
if ((g_experienceData + (victimIndex * waypoints.length ()) + victimIndex)->team1Damage > MAX_DAMAGE_VALUE) {
(g_experienceData + (victimIndex * waypoints.length ()) + victimIndex)->team1Damage = MAX_DAMAGE_VALUE;
}
}
float updateDamage = isFakeClient (attacker) ? 10.0f : 7.0f;
// store away the damage done
if (victimTeam == TEAM_TERRORIST) {
int value = (g_experienceData + (victimIndex * waypoints.length ()) + attackerIndex)->team0Damage;
value += static_cast <int> (damage / updateDamage);
if (value > MAX_DAMAGE_VALUE) {
value = MAX_DAMAGE_VALUE;
}
if (value > g_highestDamageT) {
g_highestDamageT = value;
}
(g_experienceData + (victimIndex * waypoints.length ()) + attackerIndex)->team0Damage = static_cast <uint16> (value);
}
else {
int value = (g_experienceData + (victimIndex * waypoints.length ()) + attackerIndex)->team1Damage;
value += static_cast <int> (damage / updateDamage);
if (value > MAX_DAMAGE_VALUE) {
value = MAX_DAMAGE_VALUE;
}
if (value > g_highestDamageCT) {
g_highestDamageCT = value;
}
(g_experienceData + (victimIndex * waypoints.length ()) + attackerIndex)->team1Damage = static_cast <uint16> (value);
}
}
void Bot::processChatterMessage (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_COUNTER && strcmp (tempMessage, "#CTs_Win") == 0) || (m_team == TEAM_TERRORIST && strcmp (tempMessage, "#Terrorists_Win") == 0)) {
if (g_timeRoundMid > engine.timebase ()) {
pushChatterMessage (CHATTER_QUICK_WON_ROUND);
}
else {
pushChatterMessage (CHATTER_WON_THE_ROUND);
}
}
else if (strcmp (tempMessage, "#Bot_TeamAttack") == 0) {
pushChatterMessage (CHATTER_FRIENDLY_FIRE);
}
else if (strcmp (tempMessage, "#Bot_NiceShotCommander") == 0) {
pushChatterMessage (CHATTER_NICESHOT_COMMANDER);
}
else if (strcmp (tempMessage, "#Bot_NiceShotPall") == 0) {
pushChatterMessage (CHATTER_NICESHOT_PALL);
}
}
void Bot::pushChatMessage (int type, bool isTeamSay) {
extern ConVar yb_chat;
if (g_chatFactory[type].empty () || !yb_chat.boolean ()) {
return;
}
const char *pickedPhrase = g_chatFactory[type].random ().chars ();
if (isEmptyStr (pickedPhrase)) {
return;
}
prepareChatMessage (const_cast <char *> (pickedPhrase));
pushMsgQueue (isTeamSay ? GAME_MSG_SAY_TEAM_MSG : GAME_MSG_SAY_CMD);
}
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 (isAlive (user) && m_moneyAmount >= 2000 && hasPrimaryWeapon () && (user->v.origin - pev->origin).length () <= 450.0f) {
m_aimFlags |= AIM_ENTITY;
m_lookAt = user->v.origin;
if (discardC4) {
selectWeaponByName ("weapon_c4");
engine.execBotCmd (ent (), "drop");
}
else {
selectBestWeapon ();
engine.execBotCmd (ent (), "drop");
}
m_pickupItem = nullptr;
m_pickupType = PICKUP_NONE;
m_itemCheckTime = engine.timebase () + 5.0f;
if (m_inBuyZone) {
m_ignoreBuyDelay = true;
m_buyingFinished = false;
m_buyState = BUYSTATE_PRIMARY_WEAPON;
pushMsgQueue (GAME_MSG_PURCHASE);
m_nextBuyTime = engine.timebase ();
}
}
}
void Bot::startDoubleJump (edict_t *ent) {
resetDoubleJump ();
m_doubleJumpOrigin = ent->v.origin;
m_doubleJumpEntity = ent;
startTask (TASK_DOUBLEJUMP, TASKPRI_DOUBLEJUMP, INVALID_WAYPOINT_INDEX, engine.timebase (), true);
sayTeam (format ("Ok %s, i will help you!", STRING (ent->v.netname)));
}
void Bot::resetDoubleJump (void) {
completeTask ();
m_doubleJumpEntity = nullptr;
m_duckForJump = 0.0f;
m_doubleJumpOrigin.nullify ();
m_travelStartIndex = INVALID_WAYPOINT_INDEX;
m_jumpReady = false;
}
void Bot::sayDebug (const char *format, ...) {
int level = yb_debug.integer ();
if (level <= 2) {
return;
}
va_list ap;
char buffer[MAX_PRINT_BUFFER];
va_start (ap, format);
vsnprintf (buffer, cr::bufsize (buffer), format, ap);
va_end (ap);
String printBuf;
printBuf.format ("%s: %s", STRING (pev->netname), buffer);
bool playMessage = false;
if (level == 3 && !engine.isNullEntity (g_hostEntity) && g_hostEntity->v.iuser2 == index ()) {
playMessage = true;
}
else if (level != 3) {
playMessage = true;
}
if (playMessage && level > 3) {
logEntry (false, LL_DEFAULT, printBuf.chars ());
}
if (playMessage) {
engine.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.flt () * 0.55f;
Vector end = stop - pev->velocity;
end.z -= 15.0f;
if (cr::abs (end.z - start.z) > 500.0f) {
return Vector::null ();
}
Vector midPoint = start + (end - start) * 0.5f;
engine.testHull (midPoint, midPoint + Vector (0.0f, 0.0f, 500.0f), TRACE_IGNORE_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 Vector::null ();
}
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 Vector::null ();
}
Vector velocity = (end - start) / (timeOne + timeTwo);
velocity.z = gravity * timeOne;
Vector apex = start + velocity * timeOne;
apex.z = midPoint.z;
engine.testHull (start, apex, TRACE_IGNORE_NONE, head_hull, ent (), &tr);
if (tr.flFraction < 1.0f || tr.fAllSolid) {
return Vector::null ();
}
engine.testHull (end, apex, TRACE_IGNORE_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 Vector::null ();
}
}
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.flt () * 0.55f;
float time = velocity.length () / 195.0f;
if (time < 0.01f) {
return Vector::null ();
}
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);
engine.testHull (start, apex, TRACE_IGNORE_NONE, head_hull, ent (), &tr);
if (tr.flFraction != 1.0f) {
return Vector::null ();
}
engine.testHull (stop, apex, TRACE_IGNORE_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 Vector::null ();
}
}
return velocity * 0.7793f;
}
edict_t *Bot::correctGrenadeVelocity (const char *model) {
edict_t *pent = nullptr;
while (!engine.isNullEntity (pent = g_engfuncs.pfnFindEntityByString (pent, "classname", "grenade"))) {
if (pent->v.owner == ent () && strcmp (STRING (pent->v.model) + 9, model) == 0) {
// set the correct velocity for the grenade
if (m_grenade.lengthSq () > 100.0f) {
pent->v.velocity = m_grenade;
}
m_grenadeCheckTime = engine.timebase () + MAX_GRENADE_TIMER;
selectBestWeapon ();
completeTask ();
break;
}
}
return pent;
}
Vector Bot::isBombAudible (void) {
// this function checks if bomb is can be heard by the bot, calculations done by manual testing.
if (!g_bombPlanted || taskId () == TASK_ESCAPEFROMBOMB) {
return Vector::null (); // reliability check
}
if (m_difficulty > 2) {
return waypoints.getBombPos ();
}
const Vector &bombOrigin = waypoints.getBombPos ();
float timeElapsed = ((engine.timebase () - g_timeBombPlanted) / mp_c4timer.flt ()) * 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 Vector::null ();
}
uint8 Bot::computeMsec (void) {
// estimate msec to use for this command based on time passed from the previous command
return static_cast <uint8> ((engine.timebase () - m_lastCommandTime) * 1000.0f);
}
void Bot::runMovement (void) {
// 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 = engine.timebase () - m_lastCommandTime;
uint8 msecVal = computeMsec ();
m_lastCommandTime = engine.timebase ();
g_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_GLOCK && distance < 300.0f && m_weaponBurstMode == BM_OFF) {
pev->button |= IN_ATTACK2;
}
else if (m_currentWeapon == WEAPON_GLOCK && distance >= 300.0f && m_weaponBurstMode == BM_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 == BM_OFF) {
pev->button |= IN_ATTACK2;
}
else if (m_currentWeapon == WEAPON_FAMAS && distance <= 400.0f && m_weaponBurstMode == BM_ON) {
pev->button |= IN_ATTACK2;
}
}
void Bot::checkSilencer (void) {
if (((m_currentWeapon == WEAPON_USP && m_difficulty < 2) || m_currentWeapon == WEAPON_M4A1) && !hasShield ()) {
int prob = (m_personality == PERSONALITY_RUSHER ? 35 : 65);
// aggressive bots don't like the silencer
if (rng.getInt (1, 100) <= (m_currentWeapon == WEAPON_USP ? prob / 3 : 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 (void) {
if (!g_bombPlanted) {
return 0.0f;
}
float timeLeft = ((g_timeBombPlanted + mp_c4timer.flt ()) - engine.timebase ());
if (timeLeft < 0.0f) {
return 0.0f;
}
return timeLeft;
}
bool Bot::isOutOfBombTimer (void) {
if (!(g_mapFlags & MAP_DE)) {
return false;
}
if (m_currentWaypointIndex == INVALID_WAYPOINT_INDEX || (m_hasProgressBar || taskId () == 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 = waypoints.getBombPos ();
// 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 (int i = 0; i < engine.maxClients (); i++) {
auto *bot = bots.getBot (i);
// search players with defuse kit
if (bot != nullptr && bot != this && bot->m_team == TEAM_COUNTER && bot->m_hasDefuser && (bombOrigin - bot->pev->origin).lengthSq () < cr::square (512.0f)) {
hasTeammatesWithDefuserKit = true;
break;
}
}
// add reach time to left time
float reachTime = waypoints.calculateTravelTime (pev->maxspeed, m_currentPath->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::processHearing (void) {
int hearEnemyIndex = INVALID_WAYPOINT_INDEX;
float minDistance = 99999.0f;
// loop through all enemy clients to check for hearable stuff
for (int i = 0; i < engine.maxClients (); i++) {
const Client &client = g_clients[i];
if (!(client.flags & CF_USED) || !(client.flags & CF_ALIVE) || client.ent == ent () || client.team == m_team || client.timeSoundLasting < engine.timebase ()) {
continue;
}
float distance = (client.soundPos - pev->origin).length ();
if (distance > client.hearingDistance) {
continue;
}
if (distance < minDistance) {
hearEnemyIndex = i;
minDistance = distance;
}
}
edict_t *player = nullptr;
if (hearEnemyIndex >= 0 && g_clients[hearEnemyIndex].team != m_team && !(g_gameFlags & GAME_CSDM_FFA)) {
player = g_clients[hearEnemyIndex].ent;
}
// did the bot hear someone ?
if (player != nullptr && isPlayer (player)) {
// change to best weapon if heard something
if (m_shootTime < engine.timebase () - 5.0f && isOnFloor () && m_currentWeapon != WEAPON_C4 && m_currentWeapon != WEAPON_EXPLOSIVE && m_currentWeapon != WEAPON_SMOKE && m_currentWeapon != WEAPON_FLASHBANG && !yb_jasonmode.boolean ()) {
selectBestWeapon ();
}
m_heardSoundTime = engine.timebase ();
m_states |= STATE_HEARING_ENEMY;
if (rng.getInt (0, 100) < 15 && engine.isNullEntity (m_enemy) && engine.isNullEntity (m_lastEnemy) && m_seeEnemyTime + 7.0f < engine.timebase ()) {
pushChatterMessage (CHATTER_HEARD_ENEMY);
}
// 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 & STATE_SEEING_ENEMY) {
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 < engine.timebase ()) {
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_enemyOrigin, &m_visibility)) {
m_enemy = player;
m_lastEnemy = player;
m_lastEnemyOrigin = m_enemyOrigin;
m_states |= STATE_SEEING_ENEMY;
m_seeEnemyTime = engine.timebase ();
}
// check if heard enemy can be shoot through some obstacle
else {
if (m_difficulty > 2 && m_lastEnemy == player && m_seeEnemyTime + 3.0f > engine.timebase () && yb_shoots_thru_walls.boolean () && isPenetrableObstacle (player->v.origin)) {
m_enemy = player;
m_lastEnemy = player;
m_enemyOrigin = player->v.origin;
m_lastEnemyOrigin = player->v.origin;
m_states |= (STATE_SEEING_ENEMY | STATE_SUSPECT_ENEMY);
m_seeEnemyTime = engine.timebase ();
}
}
}
}
bool Bot::isShootableBreakable (edict_t *ent) {
// this function is checking that pointed by ent pointer obstacle, can be destroyed.
auto classname = STRING (ent->v.classname);
if (strcmp (classname, "func_breakable") == 0 || (strcmp (classname, "func_pushable") == 0 && (ent->v.spawnflags & SF_PUSH_BREAKABLE))) {
return ent->v.takedamage != DAMAGE_NO && ent->v.impulse <= 0 && !(ent->v.flags & FL_WORLDBRUSH) && !(ent->v.spawnflags & SF_BREAK_TRIGGER_ONLY) && ent->v.health < 500.0f;
}
return false;
}
void Bot::processBuyzoneEntering (int buyState) {
// this function is gets called when bot enters a buyzone, to allow bot to buy some stuff
// if bot is in buy zone, try to buy ammo for this weapon...
if (m_seeEnemyTime + 12.0f < engine.timebase () && m_lastEquipTime + 15.0f < engine.timebase () && m_inBuyZone && (g_timeRoundStart + rng.getFloat (10.0f, 20.0f) + mp_buytime.flt () < engine.timebase ()) && !g_bombPlanted && m_moneyAmount > g_botBuyEconomyTable[0]) {
m_ignoreBuyDelay = true;
m_buyingFinished = false;
m_buyState = buyState;
// push buy message
pushMsgQueue (GAME_MSG_PURCHASE);
m_nextBuyTime = engine.timebase ();
m_lastEquipTime = engine.timebase ();
}
}
bool Bot::isBombDefusing (const Vector &bombOrigin) {
// this function finds if somebody currently defusing the bomb.
if (!g_bombPlanted)
return false;
bool defusingInProgress = false;
for (int i = 0; i < engine.maxClients (); i++) {
Bot *bot = bots.getBot (i);
if (bot == nullptr || bot == this) {
continue; // skip invalid bots
}
if (m_team != bot->m_team || bot->taskId () == TASK_ESCAPEFROMBOMB) {
continue; // skip other mess
}
if ((bot->pev->origin - bombOrigin).length () < 140.0f && (bot->taskId () == TASK_DEFUSEBOMB || bot->m_hasProgressBar)) {
defusingInProgress = true;
break;
}
const Client &client = g_clients[i];
// take in account peoples too
if (defusingInProgress || !(client.flags & CF_USED) || !(client.flags & CF_ALIVE) || client.team != m_team || isFakeClient (client.ent)) {
continue;
}
if ((client.ent->v.origin - bombOrigin).length () < 140.0f && ((client.ent->v.button | client.ent->v.oldbuttons) & IN_USE)) {
defusingInProgress = true;
break;
}
}
return defusingInProgress;
}
float Bot::getShiftSpeed (void) {
if (taskId () == TASK_SEEKCOVER || (pev->flags & FL_DUCKING) || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK) || (m_currentTravelFlags & PATHFLAG_JUMP) || (m_currentPath != nullptr && m_currentPath->flags & FLAG_LADDER) || isOnLadder () || isInWater () || m_isStuck) {
return pev->maxspeed;
}
return static_cast <float> (pev->maxspeed * 0.4f);
}