// // 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 ConVar yb_whose_your_daddy ("yb_whose_your_daddy", "0"); ConVar yb_debug_heuristic_type ("yb_debug_heuristic_type", "0"); int Bot::searchGoal (void) { // chooses a destination (goal) waypoint for a bot if (!g_bombPlanted && m_team == TEAM_TERRORIST && (g_mapFlags & MAP_DE)) { edict_t *pent = nullptr; while (!engine.isNullEntity (pent = g_engfuncs.pfnFindEntityByString (pent, "classname", "weaponbox"))) { if (strcmp (STRING (pent->v.model), "models/w_backpack.mdl") == 0) { int index = waypoints.getNearest (engine.getAbsPos (pent)); if (waypoints.exists (index)) { return m_loosedBombWptIndex = index; } break; } } // forcing terrorist bot to not move to another bomb spot if (m_inBombZone && !m_hasProgressBar && m_hasC4) { return waypoints.getNearest (pev->origin, 768.0f, FLAG_GOAL); } } int tactic = 0; // path finding behavior depending on map type float offensive = 0.0f; float defensive = 0.0f; float goalDesire = 0.0f; float forwardDesire = 0.0f; float campDesire = 0.0f; float backoffDesire = 0.0f; float tacticChoice = 0.0f; IntArray *offensiveWpts = nullptr; IntArray *defensiveWpts = nullptr; switch (m_team) { case TEAM_TERRORIST: offensiveWpts = &waypoints.m_ctPoints; defensiveWpts = &waypoints.m_terrorPoints; break; case TEAM_COUNTER: default: offensiveWpts = &waypoints.m_terrorPoints; defensiveWpts = &waypoints.m_ctPoints; break; } // terrorist carrying the C4? if (m_hasC4 || m_isVIP) { tactic = 3; return getGoalProcess (tactic, defensiveWpts, offensiveWpts); } else if (m_team == TEAM_COUNTER && hasHostage ()) { tactic = 2; offensiveWpts = &waypoints.m_rescuePoints; return getGoalProcess (tactic, defensiveWpts, offensiveWpts); } offensive = m_agressionLevel * 100.0f; defensive = m_fearLevel * 100.0f; if (g_mapFlags & (MAP_AS | MAP_CS)) { if (m_team == TEAM_TERRORIST) { defensive += 25.0f; offensive -= 25.0f; } else if (m_team == TEAM_COUNTER) { // on hostage maps force more bots to save hostages if (g_mapFlags & MAP_CS) { defensive -= 25.0f - m_difficulty * 0.5f; offensive += 25.0f + m_difficulty * 5.0f; } else { defensive -= 25.0f; offensive += 25.0f; } } } else if ((g_mapFlags & MAP_DE) && m_team == TEAM_COUNTER) { if (g_bombPlanted && taskId () != TASK_ESCAPEFROMBOMB && !waypoints.getBombPos ().empty ()) { if (g_bombSayString) { pushChatMessage (CHAT_BOMBPLANT); g_bombSayString = false; } return m_chosenGoalIndex = getBombPoint (); } defensive += 25.0f + m_difficulty * 4.0f; offensive -= 25.0f - m_difficulty * 0.5f; if (m_personality != PERSONALITY_RUSHER) { defensive += 10.0f; } } else if ((g_mapFlags & MAP_DE) && m_team == TEAM_TERRORIST && g_timeRoundStart + 10.0f < engine.timebase ()) { // send some terrorists to guard planted bomb if (!m_defendedBomb && g_bombPlanted && taskId () != TASK_ESCAPEFROMBOMB && getBombTimeleft () >= 15.0) { return m_chosenGoalIndex = getDefendPoint (waypoints.getBombPos ()); } } goalDesire = rng.getFloat (0.0f, 100.0f) + offensive; forwardDesire = rng.getFloat (0.0f, 100.0f) + offensive; campDesire = rng.getFloat (0.0f, 100.0f) + defensive; backoffDesire = rng.getFloat (0.0f, 100.0f) + defensive; if (!usesCampGun ()) { campDesire *= 0.5f; } tacticChoice = backoffDesire; tactic = 0; if (campDesire > tacticChoice) { tacticChoice = campDesire; tactic = 1; } if (forwardDesire > tacticChoice) { tacticChoice = forwardDesire; tactic = 2; } if (goalDesire > tacticChoice) { tactic = 3; } return getGoalProcess (tactic, defensiveWpts, offensiveWpts); } int Bot::getGoalProcess (int tactic, IntArray *defensive, IntArray *offsensive) { int goalChoices[4] = { INVALID_WAYPOINT_INDEX, INVALID_WAYPOINT_INDEX, INVALID_WAYPOINT_INDEX, INVALID_WAYPOINT_INDEX }; if (tactic == 0 && !(*defensive).empty ()) { // careful goal filterGoals (*defensive, goalChoices); } else if (tactic == 1 && !waypoints.m_campPoints.empty ()) // camp waypoint goal { // pickup sniper points if possible for sniping bots if (!waypoints.m_sniperPoints.empty () && usesSniper ()) { filterGoals (waypoints.m_sniperPoints, goalChoices); } else { filterGoals (waypoints.m_campPoints, goalChoices); } } else if (tactic == 2 && !(*offsensive).empty ()) { // offensive goal filterGoals (*offsensive, goalChoices); } else if (tactic == 3 && !waypoints.m_goalPoints.empty ()) // map goal waypoint { // force bomber to select closest goal, if round-start goal was reset by something if (m_hasC4 && g_timeRoundStart + 20.0f < engine.timebase ()) { float minDist = 99999.0f; int count = 0; for (auto &point : waypoints.m_goalPoints) { float distance = (waypoints[point].origin - pev->origin).lengthSq (); if (distance > 1024.0f) { continue; } if (distance < minDist) { goalChoices[count] = point; if (++count > 3) { count = 0; } minDist = distance; } } for (int i = 0; i < 4; i++) { if (goalChoices[i] == INVALID_WAYPOINT_INDEX) { goalChoices[i] = waypoints.m_goalPoints.random (); } } } else { filterGoals (waypoints.m_goalPoints, goalChoices); } } if (!waypoints.exists (m_currentWaypointIndex)) { m_currentWaypointIndex = changePointIndex (getNearestPoint ()); } if (goalChoices[0] == INVALID_WAYPOINT_INDEX) { return m_chosenGoalIndex = rng.getInt (0, waypoints.length () - 1); } bool isSorting = false; do { isSorting = false; for (int i = 0; i < 3; i++) { int testIndex = goalChoices[i + 1]; if (testIndex < 0) { break; } int goal1 = m_team == TEAM_TERRORIST ? (g_experienceData + (m_currentWaypointIndex * waypoints.length ()) + goalChoices[i])->team0Value : (g_experienceData + (m_currentWaypointIndex * waypoints.length ()) + goalChoices[i])->team1Value; int goal2 = m_team == TEAM_TERRORIST ? (g_experienceData + (m_currentWaypointIndex * waypoints.length ()) + goalChoices[i + 1])->team0Value : (g_experienceData + (m_currentWaypointIndex * waypoints.length ()) + goalChoices[i + 1])->team1Value; if (goal1 < goal2) { goalChoices[i + 1] = goalChoices[i]; goalChoices[i] = testIndex; isSorting = true; } } } while (isSorting); return m_chosenGoalIndex = goalChoices[0]; // return and store goal } void Bot::filterGoals (const IntArray &goals, int *result) { // this function filters the goals, so new goal is not bot's old goal, and array of goals doesn't contains duplicate goals int searchCount = 0; for (int index = 0; index < 4; index++) { int rand = goals.random (); if (searchCount <= 8 && (m_prevGoalIndex == rand || ((result[0] == rand || result[1] == rand || result[2] == rand || result[3] == rand) && goals.length () > 4)) && !isOccupiedPoint (rand)) { if (index > 0) { index--; } searchCount++; continue; } result[index] = rand; } } bool Bot::hasActiveGoal (void) { int goal = task ()->data; if (goal == INVALID_WAYPOINT_INDEX) { // not decided about a goal return false; } else if (goal == m_currentWaypointIndex) { // no nodes needed return true; } else if (m_path.empty ()) { // no path calculated return false; } return goal == m_path.back (); // got path - check if still valid } void Bot::resetCollision (void) { m_collideTime = 0.0f; m_probeTime = 0.0f; m_collisionProbeBits = 0; m_collisionState = COLLISION_NOTDECICED; m_collStateIndex = 0; for (int i = 0; i < MAX_COLLIDE_MOVES; i++) { m_collideMoves[i] = 0; } } void Bot::ignoreCollision (void) { resetCollision (); m_prevTime = engine.timebase () + 1.2f; m_lastCollTime = engine.timebase () + 1.5f; m_isStuck = false; m_checkTerrain = false; m_prevSpeed = m_moveSpeed; m_prevOrigin = pev->origin; } void Bot::avoidIncomingPlayers (edict_t *touch) { auto task = taskId (); if (task == TASK_PLANTBOMB || task == TASK_DEFUSEBOMB || task == TASK_CAMP || m_moveSpeed <= 100.0f) { return; } int ownId = engine.indexOfEntity (ent ()); int otherId = engine.indexOfEntity (touch); if (ownId < otherId) { return; } if (m_avoid) { int currentId = engine.indexOfEntity (m_avoid); if (currentId < otherId) { return; } } m_avoid = touch; m_avoidTime = engine.timebase () + 0.33f + calcThinkInterval (); } bool Bot::doPlayerAvoidance (const Vector &normal) { // avoid collision entity, got it form official csbot if (m_avoidTime > engine.timebase () && isAlive (m_avoid)) { Vector dir (cr::cosf (pev->v_angle.y), cr::sinf (pev->v_angle.y), 0.0f); Vector lat (-dir.y, dir.x, 0.0f); Vector to = Vector (m_avoid->v.origin.x - pev->origin.x, m_avoid->v.origin.y - pev->origin.y, 0.0f).normalize (); float toProj = to.x * dir.x + to.y * dir.y; float latProj = to.x * lat.x + to.y * lat.y; const float c = 0.5f; if (toProj > c) { m_moveSpeed = -pev->maxspeed; return true; } else if (toProj < -c) { m_moveSpeed = pev->maxspeed; return true; } if (latProj >= c) { pev->button |= IN_MOVELEFT; setStrafeSpeed (normal, pev->maxspeed); return true; } else if (latProj <= -c) { pev->button |= IN_MOVERIGHT; setStrafeSpeed (normal, -pev->maxspeed); return true; } return false; } else { m_avoid = nullptr; } return false; } void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) { m_isStuck = false; TraceResult tr; // Standing still, no need to check? // FIXME: doesn't care for ladder movement (handled separately) should be included in some way if ((m_moveSpeed >= 10.0f || m_strafeSpeed >= 10.0f) && m_lastCollTime < engine.timebase () && m_seeEnemyTime + 0.8f < engine.timebase () && taskId () != TASK_ATTACK) { // didn't we move enough previously? if (movedDistance < 2.0f && m_prevSpeed >= 20.0f) { m_prevTime = engine.timebase (); // then consider being stuck m_isStuck = true; if (cr::fzero (m_firstCollideTime)) { m_firstCollideTime = engine.timebase () + 0.2f; } } // not stuck yet else { // test if there's something ahead blocking the way if (cantMoveForward (dirNormal, &tr) && !isOnLadder ()) { if (m_firstCollideTime == 0.0f) { m_firstCollideTime = engine.timebase () + 0.2f; } else if (m_firstCollideTime <= engine.timebase ()) { m_isStuck = true; } } else { m_firstCollideTime = 0.0f; } } // not stuck? if (!m_isStuck) { if (m_probeTime + 0.5f < engine.timebase ()) { resetCollision (); // reset collision memory if not being stuck for 0.5 secs } else { // remember to keep pressing duck if it was necessary ago if (m_collideMoves[m_collStateIndex] == COLLISION_DUCK && (isOnFloor () || isInWater ())) { pev->button |= IN_DUCK; } } return; } // bot is stuck! Vector src; Vector dst; // not yet decided what to do? if (m_collisionState == COLLISION_NOTDECICED) { int bits = 0; if (isOnLadder ()) { bits |= PROBE_STRAFE; } else if (isInWater ()) { bits |= (PROBE_JUMP | PROBE_STRAFE); } else { bits |= (PROBE_STRAFE | (m_jumpStateTimer < engine.timebase () ? PROBE_JUMP : 0)); } // collision check allowed if not flying through the air if (isOnFloor () || isOnLadder () || isInWater ()) { int state[MAX_COLLIDE_MOVES * 2 + 1]; int i = 0; // first 4 entries hold the possible collision states state[i++] = COLLISION_STRAFELEFT; state[i++] = COLLISION_STRAFERIGHT; state[i++] = COLLISION_JUMP; state[i++] = COLLISION_DUCK; if (bits & PROBE_STRAFE) { state[i] = 0; state[i + 1] = 0; // to start strafing, we have to first figure out if the target is on the left side or right side makeVectors (m_moveAngles); Vector dirToPoint = (pev->origin - m_destOrigin).normalize2D (); Vector rightSide = g_pGlobals->v_right.normalize2D (); bool dirRight = false; bool dirLeft = false; bool blockedLeft = false; bool blockedRight = false; if ((dirToPoint | rightSide) > 0.0f) { dirRight = true; } else { dirLeft = true; } const Vector &testDir = m_moveSpeed > 0.0f ? g_pGlobals->v_forward : -g_pGlobals->v_forward; // now check which side is blocked src = pev->origin + g_pGlobals->v_right * 32.0f; dst = src + testDir * 32.0f; engine.testHull (src, dst, TRACE_IGNORE_MONSTERS, head_hull, ent (), &tr); if (tr.flFraction != 1.0f) blockedRight = true; src = pev->origin - g_pGlobals->v_right * 32.0f; dst = src + testDir * 32.0f; engine.testHull (src, dst, TRACE_IGNORE_MONSTERS, head_hull, ent (), &tr); if (tr.flFraction != 1.0f) { blockedLeft = true; } if (dirLeft) { state[i] += 5; } else { state[i] -= 5; } if (blockedLeft) { state[i] -= 5; } i++; if (dirRight) { state[i] += 5; } else { state[i] -= 5; } if (blockedRight) { state[i] -= 5; } } // now weight all possible states if (bits & PROBE_JUMP) { state[i] = 0; if (canJumpUp (dirNormal)) { state[i] += 10; } if (m_destOrigin.z >= pev->origin.z + 18.0f) { state[i] += 5; } if (seesEntity (m_destOrigin)) { makeVectors (m_moveAngles); src = eyePos (); src = src + g_pGlobals->v_right * 15.0f; engine.testLine (src, m_destOrigin, TRACE_IGNORE_EVERYTHING, ent (), &tr); if (tr.flFraction >= 1.0f) { src = eyePos (); src = src - g_pGlobals->v_right * 15.0f; engine.testLine (src, m_destOrigin, TRACE_IGNORE_EVERYTHING, ent (), &tr); if (tr.flFraction >= 1.0f) { state[i] += 5; } } } if (pev->flags & FL_DUCKING) { src = pev->origin; } else { src = pev->origin + Vector (0.0f, 0.0f, -17.0f); } dst = src + dirNormal * 30.0f; engine.testLine (src, dst, TRACE_IGNORE_EVERYTHING, ent (), &tr); if (tr.flFraction != 1.0f) { state[i] += 10; } } else { state[i] = 0; } i++; #if 0 if (bits & PROBE_DUCK) { state[i] = 0; if (canDuckUnder (dirNormal)) { state[i] += 10; } if ((m_destOrigin.z + 36.0f <= pev->origin.z) && seesEntity (m_destOrigin)) { state[i] += 5; } } else #endif state[i] = 0; i++; // weighted all possible moves, now sort them to start with most probable bool isSorting = false; do { isSorting = false; for (i = 0; i < 3; i++) { if (state[i + MAX_COLLIDE_MOVES] < state[i + MAX_COLLIDE_MOVES + 1]) { int temp = state[i]; state[i] = state[i + 1]; state[i + 1] = temp; temp = state[i + MAX_COLLIDE_MOVES]; state[i + MAX_COLLIDE_MOVES] = state[i + MAX_COLLIDE_MOVES + 1]; state[i + MAX_COLLIDE_MOVES + 1] = temp; isSorting = true; } } } while (isSorting); for (i = 0; i < MAX_COLLIDE_MOVES; i++) { m_collideMoves[i] = state[i]; } m_collideTime = engine.timebase (); m_probeTime = engine.timebase () + 0.5f; m_collisionProbeBits = bits; m_collisionState = COLLISION_PROBING; m_collStateIndex = 0; } } if (m_collisionState == COLLISION_PROBING) { if (m_probeTime < engine.timebase ()) { m_collStateIndex++; m_probeTime = engine.timebase () + 0.5f; if (m_collStateIndex > MAX_COLLIDE_MOVES) { m_navTimeset = engine.timebase () - 5.0f; resetCollision (); } } if (m_collStateIndex < MAX_COLLIDE_MOVES) { switch (m_collideMoves[m_collStateIndex]) { case COLLISION_JUMP: if (isOnFloor () || isInWater ()) { pev->button |= IN_JUMP; m_jumpStateTimer = engine.timebase () + rng.getFloat (0.7f, 1.5f); } break; case COLLISION_DUCK: if (isOnFloor () || isInWater ()) { pev->button |= IN_DUCK; } break; case COLLISION_STRAFELEFT: pev->button |= IN_MOVELEFT; setStrafeSpeed (dirNormal, -pev->maxspeed); break; case COLLISION_STRAFERIGHT: pev->button |= IN_MOVERIGHT; setStrafeSpeed (dirNormal, pev->maxspeed); break; } } } } doPlayerAvoidance (dirNormal); } bool Bot::processNavigation (void) { // this function is a main path navigation TraceResult tr, tr2; // check if we need to find a waypoint... if (m_currentWaypointIndex == INVALID_WAYPOINT_INDEX) { getValidPoint (); m_waypointOrigin = m_currentPath->origin; // if wayzone radios non zero vary origin a bit depending on the body angles if (m_currentPath->radius > 0) { makeVectors (Vector (pev->angles.x, cr::angleNorm (pev->angles.y + rng.getFloat (-90.0f, 90.0f)), 0.0f)); m_waypointOrigin = m_waypointOrigin + g_pGlobals->v_forward * rng.getFloat (0, m_currentPath->radius); } m_navTimeset = engine.timebase (); } m_destOrigin = m_waypointOrigin + pev->view_ofs; float waypointDistance = (pev->origin - m_waypointOrigin).length (); // this waypoint has additional travel flags - care about them if (m_currentTravelFlags & PATHFLAG_JUMP) { // bot is not jumped yet? if (!m_jumpFinished) { // if bot's on the ground or on the ladder we're free to jump. actually setting the correct velocity is cheating. // pressing the jump button gives the illusion of the bot actual jumping. if (isOnFloor () || isOnLadder ()) { pev->velocity = m_desiredVelocity; pev->button |= IN_JUMP; m_jumpFinished = true; m_checkTerrain = false; m_desiredVelocity.nullify (); } } else if (!yb_jasonmode.boolean () && m_currentWeapon == WEAPON_KNIFE && isOnFloor ()) { selectBestWeapon (); } } if (m_currentPath->flags & FLAG_LADDER) { if (m_waypointOrigin.z >= (pev->origin.z + 16.0f)) { m_waypointOrigin = m_currentPath->origin + Vector (0.0f, 0.0f, 16.0f); } else if (m_waypointOrigin.z < pev->origin.z + 16.0f && !isOnLadder () && isOnFloor () && !(pev->flags & FL_DUCKING)) { m_moveSpeed = waypointDistance; if (m_moveSpeed < 150.0f) { m_moveSpeed = 150.0f; } else if (m_moveSpeed > pev->maxspeed) { m_moveSpeed = pev->maxspeed; } } } // special lift handling (code merged from podbotmm) if (m_currentPath->flags & FLAG_LIFT) { bool liftClosedDoorExists = false; // update waypoint time set m_navTimeset = engine.timebase (); // trace line to door engine.testLine (pev->origin, m_currentPath->origin, TRACE_IGNORE_EVERYTHING, ent (), &tr2); if (tr2.flFraction < 1.0f && strcmp (STRING (tr2.pHit->v.classname), "func_door") == 0 && (m_liftState == LIFT_NO_NEARBY || m_liftState == LIFT_WAITING_FOR || m_liftState == LIFT_LOOKING_BUTTON_OUTSIDE) && pev->groundentity != tr2.pHit) { if (m_liftState == LIFT_NO_NEARBY) { m_liftState = LIFT_LOOKING_BUTTON_OUTSIDE; m_liftUsageTime = engine.timebase () + 7.0f; } liftClosedDoorExists = true; } // trace line down engine.testLine (m_currentPath->origin, m_currentPath->origin + Vector (0.0f, 0.0f, -50.0f), TRACE_IGNORE_EVERYTHING, ent (), &tr); // if trace result shows us that it is a lift if (!engine.isNullEntity (tr.pHit) && !m_path.empty () && (strcmp (STRING (tr.pHit->v.classname), "func_door") == 0 || strcmp (STRING (tr.pHit->v.classname), "func_plat") == 0 || strcmp (STRING (tr.pHit->v.classname), "func_train") == 0) && !liftClosedDoorExists) { if ((m_liftState == LIFT_NO_NEARBY || m_liftState == LIFT_WAITING_FOR || m_liftState == LIFT_LOOKING_BUTTON_OUTSIDE) && tr.pHit->v.velocity.z == 0.0f) { if (cr::abs (pev->origin.z - tr.vecEndPos.z) < 70.0f) { m_liftEntity = tr.pHit; m_liftState = LIFT_ENTERING_IN; m_liftTravelPos = m_currentPath->origin; m_liftUsageTime = engine.timebase () + 5.0f; } } else if (m_liftState == LIFT_TRAVELING_BY) { m_liftState = LIFT_LEAVING; m_liftUsageTime = engine.timebase () + 7.0f; } } else if (!m_path.empty ()) // no lift found at waypoint { if ((m_liftState == LIFT_NO_NEARBY || m_liftState == LIFT_WAITING_FOR) && m_path.hasNext ()) { int nextNode = m_path.next (); if (waypoints.exists (nextNode) && (waypoints[nextNode].flags & FLAG_LIFT)) { engine.testLine (m_currentPath->origin, waypoints[nextNode].origin, TRACE_IGNORE_EVERYTHING, ent (), &tr); if (!engine.isNullEntity (tr.pHit) && (strcmp (STRING (tr.pHit->v.classname), "func_door") == 0 || strcmp (STRING (tr.pHit->v.classname), "func_plat") == 0 || strcmp (STRING (tr.pHit->v.classname), "func_train") == 0)) { m_liftEntity = tr.pHit; } } m_liftState = LIFT_LOOKING_BUTTON_OUTSIDE; m_liftUsageTime = engine.timebase () + 15.0f; } } // bot is going to enter the lift if (m_liftState == LIFT_ENTERING_IN) { m_destOrigin = m_liftTravelPos; // check if we enough to destination if ((pev->origin - m_destOrigin).lengthSq () < 225.0f) { m_moveSpeed = 0.0f; m_strafeSpeed = 0.0f; m_navTimeset = engine.timebase (); m_aimFlags |= AIM_NAVPOINT; resetCollision (); // need to wait our following teammate ? bool needWaitForTeammate = false; // if some bot is following a bot going into lift - he should take the same lift to go for (int i = 0; i < engine.maxClients (); i++) { Bot *bot = bots.getBot (i); if (bot == nullptr || bot == this) { continue; } if (!bot->m_notKilled || bot->m_team != m_team || bot->m_targetEntity != ent () || bot->taskId () != TASK_FOLLOWUSER) { continue; } if (bot->pev->groundentity == m_liftEntity && bot->isOnFloor ()) { break; } bot->m_liftEntity = m_liftEntity; bot->m_liftState = LIFT_ENTERING_IN; bot->m_liftTravelPos = m_liftTravelPos; needWaitForTeammate = true; } if (needWaitForTeammate) { m_liftState = LIFT_WAIT_FOR_TEAMMATES; m_liftUsageTime = engine.timebase () + 8.0f; } else { m_liftState = LIFT_LOOKING_BUTTON_INSIDE; m_liftUsageTime = engine.timebase () + 10.0f; } } } // bot is waiting for his teammates if (m_liftState == LIFT_WAIT_FOR_TEAMMATES) { // need to wait our following teammate ? bool needWaitForTeammate = false; for (int i = 0; i < engine.maxClients (); i++) { Bot *bot = bots.getBot (i); if (bot == nullptr) { continue; // skip invalid bots } if (!bot->m_notKilled || bot->m_team != m_team || bot->m_targetEntity != ent () || bot->taskId () != TASK_FOLLOWUSER || bot->m_liftEntity != m_liftEntity) { continue; } if (bot->pev->groundentity == m_liftEntity || !bot->isOnFloor ()) { needWaitForTeammate = true; break; } } // need to wait for teammate if (needWaitForTeammate) { m_destOrigin = m_liftTravelPos; if ((pev->origin - m_destOrigin).lengthSq () < 225.0f) { m_moveSpeed = 0.0f; m_strafeSpeed = 0.0f; m_navTimeset = engine.timebase (); m_aimFlags |= AIM_NAVPOINT; resetCollision (); } } // else we need to look for button if (!needWaitForTeammate || m_liftUsageTime < engine.timebase ()) { m_liftState = LIFT_LOOKING_BUTTON_INSIDE; m_liftUsageTime = engine.timebase () + 10.0f; } } // bot is trying to find button inside a lift if (m_liftState == LIFT_LOOKING_BUTTON_INSIDE) { edict_t *button = getNearestButton (STRING (m_liftEntity->v.targetname)); // got a valid button entity ? if (!engine.isNullEntity (button) && pev->groundentity == m_liftEntity && m_buttonPushTime + 1.0f < engine.timebase () && m_liftEntity->v.velocity.z == 0.0f && isOnFloor ()) { m_pickupItem = button; m_pickupType = PICKUP_BUTTON; m_navTimeset = engine.timebase (); } } // is lift activated and bot is standing on it and lift is moving ? if (m_liftState == LIFT_LOOKING_BUTTON_INSIDE || m_liftState == LIFT_ENTERING_IN || m_liftState == LIFT_WAIT_FOR_TEAMMATES || m_liftState == LIFT_WAITING_FOR) { if (pev->groundentity == m_liftEntity && m_liftEntity->v.velocity.z != 0.0f && isOnFloor () && ((waypoints[m_prevWptIndex[0]].flags & FLAG_LIFT) || !engine.isNullEntity (m_targetEntity))) { m_liftState = LIFT_TRAVELING_BY; m_liftUsageTime = engine.timebase () + 14.0f; if ((pev->origin - m_destOrigin).lengthSq () < 225.0f) { m_moveSpeed = 0.0f; m_strafeSpeed = 0.0f; m_navTimeset = engine.timebase (); m_aimFlags |= AIM_NAVPOINT; resetCollision (); } } } // bots is currently moving on lift if (m_liftState == LIFT_TRAVELING_BY) { m_destOrigin = Vector (m_liftTravelPos.x, m_liftTravelPos.y, pev->origin.z); if ((pev->origin - m_destOrigin).lengthSq () < 225.0f) { m_moveSpeed = 0.0f; m_strafeSpeed = 0.0f; m_navTimeset = engine.timebase (); m_aimFlags |= AIM_NAVPOINT; resetCollision (); } } // need to find a button outside the lift if (m_liftState == LIFT_LOOKING_BUTTON_OUTSIDE) { // button has been pressed, lift should come if (m_buttonPushTime + 8.0f >= engine.timebase ()) { if (waypoints.exists (m_prevWptIndex[0])) { m_destOrigin = waypoints[m_prevWptIndex[0]].origin; } else { m_destOrigin = pev->origin; } if ((pev->origin - m_destOrigin).lengthSq () < 225.0f) { m_moveSpeed = 0.0f; m_strafeSpeed = 0.0f; m_navTimeset = engine.timebase (); m_aimFlags |= AIM_NAVPOINT; resetCollision (); } } else if (!engine.isNullEntity (m_liftEntity)) { edict_t *button = getNearestButton (STRING (m_liftEntity->v.targetname)); // if we got a valid button entity if (!engine.isNullEntity (button)) { // lift is already used ? bool liftUsed = false; // iterate though clients, and find if lift already used 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 || client.ent == ent () || engine.isNullEntity (client.ent->v.groundentity)) { continue; } if (client.ent->v.groundentity == m_liftEntity) { liftUsed = true; break; } } // lift is currently used if (liftUsed) { if (waypoints.exists (m_prevWptIndex[0])) { m_destOrigin = waypoints[m_prevWptIndex[0]].origin; } else { m_destOrigin = button->v.origin; } if ((pev->origin - m_destOrigin).lengthSq () < 225.0f) { m_moveSpeed = 0.0f; m_strafeSpeed = 0.0f; } } else { m_pickupItem = button; m_pickupType = PICKUP_BUTTON; m_liftState = LIFT_WAITING_FOR; m_navTimeset = engine.timebase (); m_liftUsageTime = engine.timebase () + 20.0f; } } else { m_liftState = LIFT_WAITING_FOR; m_liftUsageTime = engine.timebase () + 15.0f; } } } // bot is waiting for lift if (m_liftState == LIFT_WAITING_FOR) { if (waypoints.exists (m_prevWptIndex[0])) { if (!(waypoints[m_prevWptIndex[0]].flags & FLAG_LIFT)) { m_destOrigin = waypoints[m_prevWptIndex[0]].origin; } else if (waypoints.exists (m_prevWptIndex[1])) { m_destOrigin = waypoints[m_prevWptIndex[1]].origin; } } if ((pev->origin - m_destOrigin).lengthSq () < 100.0f) { m_moveSpeed = 0.0f; m_strafeSpeed = 0.0f; m_navTimeset = engine.timebase (); m_aimFlags |= AIM_NAVPOINT; resetCollision (); } } // if bot is waiting for lift, or going to it if (m_liftState == LIFT_WAITING_FOR || m_liftState == LIFT_ENTERING_IN) { // bot fall down somewhere inside the lift's groove :) if (pev->groundentity != m_liftEntity && waypoints.exists (m_prevWptIndex[0])) { if ((waypoints[m_prevWptIndex[0]].flags & FLAG_LIFT) && (m_currentPath->origin.z - pev->origin.z) > 50.0f && (waypoints[m_prevWptIndex[0]].origin.z - pev->origin.z) > 50.0f) { m_liftState = LIFT_NO_NEARBY; m_liftEntity = nullptr; m_liftUsageTime = 0.0f; clearSearchNodes (); searchOptimalPoint (); if (waypoints.exists (m_prevWptIndex[2])) { searchShortestPath (m_currentWaypointIndex, m_prevWptIndex[2]); } return false; } } } } if (!engine.isNullEntity (m_liftEntity) && !(m_currentPath->flags & FLAG_LIFT)) { if (m_liftState == LIFT_TRAVELING_BY) { m_liftState = LIFT_LEAVING; m_liftUsageTime = engine.timebase () + 10.0f; } if (m_liftState == LIFT_LEAVING && m_liftUsageTime < engine.timebase () && pev->groundentity != m_liftEntity) { m_liftState = LIFT_NO_NEARBY; m_liftUsageTime = 0.0f; m_liftEntity = nullptr; } } if (m_liftUsageTime < engine.timebase () && m_liftUsageTime != 0.0f) { m_liftEntity = nullptr; m_liftState = LIFT_NO_NEARBY; m_liftUsageTime = 0.0f; clearSearchNodes (); if (waypoints.exists (m_prevWptIndex[0])) { if (!(waypoints[m_prevWptIndex[0]].flags & FLAG_LIFT)) { changePointIndex (m_prevWptIndex[0]); } else { searchOptimalPoint (); } } else { searchOptimalPoint (); } return false; } // check if we are going through a door... if (g_mapFlags & MAP_HAS_DOORS) { engine.testLine (pev->origin, m_waypointOrigin, TRACE_IGNORE_MONSTERS, ent (), &tr); if (!engine.isNullEntity (tr.pHit) && engine.isNullEntity (m_liftEntity) && strncmp (STRING (tr.pHit->v.classname), "func_door", 9) == 0) { // if the door is near enough... if ((engine.getAbsPos (tr.pHit) - pev->origin).lengthSq () < 2500.0f) { ignoreCollision (); // don't consider being stuck if (rng.getInt (1, 100) < 50) { // do not use door directrly under xash, or we will get failed assert in gamedll code if (g_gameFlags & GAME_XASH_ENGINE) { pev->button |= IN_USE; } else { MDLL_Use (tr.pHit, ent ()); // also 'use' the door randomly } } } // make sure we are always facing the door when going through it m_aimFlags &= ~(AIM_LAST_ENEMY | AIM_PREDICT_PATH); m_canChooseAimDirection = false; edict_t *button = getNearestButton (STRING (tr.pHit->v.targetname)); // check if we got valid button if (!engine.isNullEntity (button)) { m_pickupItem = button; m_pickupType = PICKUP_BUTTON; m_navTimeset = engine.timebase (); } // if bot hits the door, then it opens, so wait a bit to let it open safely if (pev->velocity.length2D () < 2 && m_timeDoorOpen < engine.timebase ()) { startTask (TASK_PAUSE, TASKPRI_PAUSE, INVALID_WAYPOINT_INDEX, engine.timebase () + 0.5f, false); m_timeDoorOpen = engine.timebase () + 1.0f; // retry in 1 sec until door is open edict_t *pent = nullptr; if (m_tryOpenDoor++ > 2 && findNearestPlayer (reinterpret_cast (&pent), ent (), 256.0f, false, false, true, true, false)) { m_seeEnemyTime = engine.timebase () - 0.5f; m_states |= STATE_SEEING_ENEMY; m_aimFlags |= AIM_ENEMY; m_lastEnemy = pent; m_enemy = pent; m_lastEnemyOrigin = pent->v.origin; m_tryOpenDoor = 0; } else m_tryOpenDoor = 0; } } } float desiredDistance = 0.0f; // initialize the radius for a special waypoint type, where the wpt is considered to be reached if (m_currentPath->flags & FLAG_LIFT) { desiredDistance = 50.0f; } else if ((pev->flags & FL_DUCKING) || (m_currentPath->flags & FLAG_GOAL)) { desiredDistance = 25.0f; } else if (m_currentPath->flags & FLAG_LADDER) { desiredDistance = 15.0f; } else if (m_currentTravelFlags & PATHFLAG_JUMP) { desiredDistance = 0.0f; } else if (isOccupiedPoint (m_currentWaypointIndex)) { desiredDistance = 120.0f; } else { desiredDistance = m_currentPath->radius; } // check if waypoint has a special travelflag, so they need to be reached more precisely for (int i = 0; i < MAX_PATH_INDEX; i++) { if (m_currentPath->connectionFlags[i] != 0) { desiredDistance = 0.0f; break; } } // needs precise placement - check if we get past the point if (desiredDistance < 22.0f && waypointDistance < 30.0f && (pev->origin + (pev->velocity * calcThinkInterval ()) - m_waypointOrigin).lengthSq () > cr::square (waypointDistance)) { desiredDistance = waypointDistance + 1.0f; } if (waypointDistance < desiredDistance) { // did we reach a destination waypoint? if (task ()->data == m_currentWaypointIndex) { // add goal values if (m_chosenGoalIndex != INVALID_WAYPOINT_INDEX) { int16 waypointValue; int startIndex = m_chosenGoalIndex; int goalIndex = m_currentWaypointIndex; if (m_team == TEAM_TERRORIST) { waypointValue = (g_experienceData + (startIndex * waypoints.length ()) + goalIndex)->team0Value; waypointValue += static_cast (pev->health * 0.5f); waypointValue += static_cast (m_goalValue * 0.5f); if (waypointValue < -MAX_GOAL_VALUE) { waypointValue = -MAX_GOAL_VALUE; } else if (waypointValue > MAX_GOAL_VALUE) { waypointValue = MAX_GOAL_VALUE; } (g_experienceData + (startIndex * waypoints.length ()) + goalIndex)->team0Value = waypointValue; } else { waypointValue = (g_experienceData + (startIndex * waypoints.length ()) + goalIndex)->team1Value; waypointValue += static_cast (pev->health * 0.5f); waypointValue += static_cast (m_goalValue * 0.5f); if (waypointValue < -MAX_GOAL_VALUE) { waypointValue = -MAX_GOAL_VALUE; } else if (waypointValue > MAX_GOAL_VALUE) { waypointValue = MAX_GOAL_VALUE; } (g_experienceData + (startIndex * waypoints.length ()) + goalIndex)->team1Value = waypointValue; } } return true; } else if (m_path.empty ()) { return false; } int taskTarget = task ()->data; if ((g_mapFlags & MAP_DE) && g_bombPlanted && m_team == TEAM_COUNTER && taskId () != TASK_ESCAPEFROMBOMB && taskTarget != INVALID_WAYPOINT_INDEX) { const Vector &bombOrigin = isBombAudible (); // bot within 'hearable' bomb tick noises? if (!bombOrigin.empty ()) { float distance = (bombOrigin - waypoints[taskTarget].origin).length (); if (distance > 512.0f) { if (rng.getInt (0, 100) < 50 && !waypoints.isVisited (taskTarget)) { pushRadioMessage (RADIO_SECTOR_CLEAR); } waypoints.setVisited (taskTarget); // doesn't hear so not a good goal } } else { if (rng.getInt (0, 100) < 50 && !waypoints.isVisited (taskTarget)) { pushRadioMessage (RADIO_SECTOR_CLEAR); } waypoints.setVisited (taskTarget); // doesn't hear so not a good goal } } advanceMovement (); // do the actual movement checking } return false; } void Bot::searchShortestPath (int srcIndex, int destIndex) { // this function finds the shortest path from source index to destination index if (!waypoints.exists (srcIndex)){ logEntry (false, LL_ERROR, "Pathfinder source path index not valid (%d)", srcIndex); return; } else if (!waypoints.exists (destIndex)) { logEntry (false, LL_ERROR, "Pathfinder destination path index not valid (%d)", destIndex); return; } clearSearchNodes (); m_chosenGoalIndex = srcIndex; m_goalValue = 0.0f; m_path.push (srcIndex); while (srcIndex != destIndex) { srcIndex = *(waypoints.m_pathMatrix + (srcIndex * waypoints.length ()) + destIndex); if (srcIndex < 0) { m_prevGoalIndex = INVALID_WAYPOINT_INDEX; task ()->data = INVALID_WAYPOINT_INDEX; return; } m_path.push (srcIndex); } } float gfunctionKillsDistT (int currentIndex, int parentIndex) { // least kills and number of nodes to goal for a team if (parentIndex == INVALID_WAYPOINT_INDEX) { return 0.0f; } float cost = static_cast ((g_experienceData + (currentIndex * waypoints.length ()) + currentIndex)->team0Damage + g_highestDamageT); Path ¤t = waypoints[currentIndex]; for (int i = 0; i < MAX_PATH_INDEX; i++) { int neighbour = current.index[i]; if (neighbour != INVALID_WAYPOINT_INDEX) { cost += (g_experienceData + (neighbour * waypoints.length ()) + neighbour)->team0Damage; } } if (current.flags & FLAG_CROUCH) { cost *= 1.5f; } return cost; } float gfunctionKillsDistCT (int currentIndex, int parentIndex) { // least kills and number of nodes to goal for a team if (parentIndex == INVALID_WAYPOINT_INDEX) { return 0.0f; } float cost = static_cast ((g_experienceData + (currentIndex * waypoints.length ()) + currentIndex)->team1Damage + g_highestDamageCT); Path ¤t = waypoints[currentIndex]; for (int i = 0; i < MAX_PATH_INDEX; i++) { int neighbour = current.index[i]; if (neighbour != INVALID_WAYPOINT_INDEX) { cost += static_cast ((g_experienceData + (neighbour * waypoints.length ()) + neighbour)->team1Damage); } } if (current.flags & FLAG_CROUCH) { cost *= 1.5f; } return cost; } float gfunctionKillsDistCTWithHostage (int currentIndex, int parentIndex) { // least kills and number of nodes to goal for a team Path ¤t = waypoints[currentIndex]; if (current.flags & FLAG_NOHOSTAGE) { return 65355.0f; } else if (current.flags & (FLAG_CROUCH | FLAG_LADDER)) { return gfunctionKillsDistCT (currentIndex, parentIndex) * 500.0f; } return gfunctionKillsDistCT (currentIndex, parentIndex); } float gfunctionKillsT (int currentIndex, int) { // least kills to goal for a team float cost = (g_experienceData + (currentIndex * waypoints.length ()) + currentIndex)->team0Damage; Path ¤t = waypoints[currentIndex]; for (int i = 0; i < MAX_PATH_INDEX; i++) { int neighbour = current.index[i]; if (neighbour != INVALID_WAYPOINT_INDEX) { cost += (g_experienceData + (neighbour * waypoints.length ()) + neighbour)->team0Damage; } } if (current.flags & FLAG_CROUCH) { cost *= 1.5f; } return cost + 0.5f; } float gfunctionKillsCT (int currentIndex, int parentIndex) { // least kills to goal for a team if (parentIndex == INVALID_WAYPOINT_INDEX) { return 0.0f; } float cost = (g_experienceData + (currentIndex * waypoints.length ()) + currentIndex)->team1Damage; Path ¤t = waypoints[currentIndex]; for (int i = 0; i < MAX_PATH_INDEX; i++) { int neighbour = current.index[i]; if (neighbour != INVALID_WAYPOINT_INDEX) { cost += (g_experienceData + (neighbour * waypoints.length ()) + neighbour)->team1Damage; } } if (current.flags & FLAG_CROUCH) { cost *= 1.5f; } return cost + 0.5f; } float gfunctionKillsCTWithHostage (int currentIndex, int parentIndex) { // least kills to goal for a team if (parentIndex == INVALID_WAYPOINT_INDEX) { return 0.0f; } Path ¤t = waypoints[currentIndex]; if (current.flags & FLAG_NOHOSTAGE) { return 65355.0f; } else if (current.flags & (FLAG_CROUCH | FLAG_LADDER)) { return gfunctionKillsDistCT (currentIndex, parentIndex) * 500.0f; } return gfunctionKillsCT (currentIndex, parentIndex); } float gfunctionPathDist (int currentIndex, int parentIndex) { if (parentIndex == INVALID_WAYPOINT_INDEX) { return 0.0f; } Path &parent = waypoints[parentIndex]; Path ¤t = waypoints[currentIndex]; for (int i = 0; i < MAX_PATH_INDEX; i++) { if (parent.index[i] == currentIndex) { // we don't like ladder or crouch point if (current.flags & (FLAG_CROUCH | FLAG_LADDER)) { return parent.distances[i] * 1.5f; } return static_cast (parent.distances[i]); } } return 65355.0f; } float gfunctionPathDistWithHostage (int currentIndex, int parentIndex) { Path ¤t = waypoints[currentIndex]; if (current.flags & FLAG_NOHOSTAGE) { return 65355.0f; } else if (current.flags & (FLAG_CROUCH | FLAG_LADDER)) { return gfunctionPathDist (currentIndex, parentIndex) * 500.0f; } return gfunctionPathDist (currentIndex, parentIndex); } float hfunctionPathDist (int index, int, int goalIndex) { // square distance heuristic Path &start = waypoints[index]; Path &goal = waypoints[goalIndex]; float x = cr::abs (start.origin.x - goal.origin.x); float y = cr::abs (start.origin.y - goal.origin.y); float z = cr::abs (start.origin.z - goal.origin.z); switch (yb_debug_heuristic_type.integer ()) { case 0: default: return cr::max (cr::max (x, y), z); // chebyshev distance case 1: return x + y + z; // manhattan distance case 2: return 0.0f; // no heuristic case 3: case 4: // euclidean based distance float euclidean = cr::powf (cr::powf (x, 2.0f) + cr::powf (y, 2.0f) + cr::powf (z, 2.0f), 0.5f); if (yb_debug_heuristic_type.integer () == 4) { return 1000.0f * (cr::ceilf (euclidean) - euclidean); } return euclidean; } } float hfunctionPathDistWithHostage (int index, int startIndex, int goalIndex) { // square distance heuristic with hostages if (waypoints[startIndex].flags & FLAG_NOHOSTAGE) { return 65355.0f; } return hfunctionPathDist (index, startIndex, goalIndex); } float hfunctionNone (int index, int startIndex, int goalIndex) { return hfunctionPathDist (index, startIndex, goalIndex) / 128.0f * 10.0f; } void Bot::searchPath (int srcIndex, int destIndex, SearchPathType pathType /*= SEARCH_PATH_FASTEST */) { // this function finds a path from srcIndex to destIndex if (!waypoints.exists (srcIndex)) { logEntry (false, LL_ERROR, "Pathfinder source path index not valid (%d)", srcIndex); return; } else if (!waypoints.exists (destIndex)) { logEntry (false, LL_ERROR, "Pathfinder destination path index not valid (%d)", destIndex); return; } clearSearchNodes (); m_chosenGoalIndex = srcIndex; m_goalValue = 0.0f; clearRoute (); float (*gcalc) (int, int) = nullptr; float (*hcalc) (int, int, int) = nullptr; switch (pathType) { default: case SEARCH_PATH_FASTEST: if ((g_mapFlags & MAP_CS) && hasHostage ()) { gcalc = gfunctionPathDistWithHostage; hcalc = hfunctionPathDistWithHostage; } else { gcalc = gfunctionPathDist; hcalc = hfunctionPathDist; } break; case SEARCH_PATH_SAFEST_FASTER: if (m_team == TEAM_TERRORIST) { gcalc = gfunctionKillsDistT; hcalc = hfunctionPathDist; } else if ((g_mapFlags & MAP_CS) && hasHostage ()) { gcalc = gfunctionKillsDistCTWithHostage; hcalc = hfunctionPathDistWithHostage; } else { gcalc = gfunctionKillsDistCT; hcalc = hfunctionPathDist; } break; case SEARCH_PATH_SAFEST: if (m_team == TEAM_TERRORIST) { gcalc = gfunctionKillsT; hcalc = hfunctionNone; } else if ((g_mapFlags & MAP_CS) && hasHostage ()) { gcalc = gfunctionKillsCTWithHostage; hcalc = hfunctionNone; } else { gcalc = gfunctionKillsCT; hcalc = hfunctionNone; } break; } auto srcRoute = &m_routes[srcIndex]; // put start node into open list srcRoute->g = gcalc (srcIndex, INVALID_WAYPOINT_INDEX); srcRoute->f = srcRoute->g + hcalc (srcIndex, srcIndex, destIndex); srcRoute->state = ROUTE_OPEN; m_routeQue.clear (); m_routeQue.push (srcIndex, srcRoute->g); while (!m_routeQue.empty ()) { // remove the first node from the open list int currentIndex = m_routeQue.pop (); // safes us from bad waypoints... if (m_routeQue.length () >= MAX_ROUTE_LENGTH - 1) { logEntry (true, LL_ERROR, "A* Search for bots \"%s\" has tried to build path with at least %d nodes. Seems to be waypoints are broken.", STRING (pev->netname), m_routeQue.length ()); // bail out to shortest path searchShortestPath (srcIndex, destIndex); return; } // is the current node the goal node? if (currentIndex == destIndex) { // build the complete path do { m_path.push (currentIndex); currentIndex = m_routes[currentIndex].parent; } while (currentIndex != INVALID_WAYPOINT_INDEX); m_path.reverse (); return; } auto curRoute = &m_routes[currentIndex]; if (curRoute->state != ROUTE_OPEN) { continue; } // put current node into CLOSED list curRoute->state = ROUTE_CLOSED; // now expand the current node for (int i = 0; i < MAX_PATH_INDEX; i++) { int currentChild = waypoints[currentIndex].index[i]; if (currentChild == INVALID_WAYPOINT_INDEX) { continue; } auto childRoute = &m_routes[currentChild]; // calculate the F value as F = G + H float g = curRoute->g + gcalc (currentChild, currentIndex); float h = hcalc (currentChild, srcIndex, destIndex); float f = g + h; if (childRoute->state == ROUTE_NEW || childRoute->f > f) { // put the current child into open list childRoute->parent = currentIndex; childRoute->state = ROUTE_OPEN; childRoute->g = g; childRoute->f = f; m_routeQue.push (currentChild, g); } } } searchShortestPath (srcIndex, destIndex); // A* found no path, try floyd pathfinder instead } void Bot::clearSearchNodes (void) { m_path.clear (); m_chosenGoalIndex = INVALID_WAYPOINT_INDEX; } void Bot::clearRoute (void) { int maxWaypoints = waypoints.length (); if (m_routes.capacity () < static_cast (waypoints.length ())) { m_routes.reserve (maxWaypoints); } for (int i = 0; i < maxWaypoints; i++) { auto route = &m_routes[i]; route->g = route->f = 0.0f; route->parent = INVALID_WAYPOINT_INDEX; route->state = ROUTE_NEW; } m_routes.clear (); } int Bot::searchAimingPoint (const Vector &to) { // return the most distant waypoint which is seen from the bot to the target and is within count if (m_currentWaypointIndex == INVALID_WAYPOINT_INDEX) { m_currentWaypointIndex = changePointIndex (getNearestPoint ()); } int destIndex = waypoints.getNearest (to); int bestIndex = m_currentWaypointIndex; if (destIndex == INVALID_WAYPOINT_INDEX) { return INVALID_WAYPOINT_INDEX; } while (destIndex != m_currentWaypointIndex) { destIndex = *(waypoints.m_pathMatrix + (destIndex * waypoints.length ()) + m_currentWaypointIndex); if (destIndex < 0) { break; } if (waypoints.isVisible (m_currentWaypointIndex, destIndex) && waypoints.isVisible (destIndex, m_currentWaypointIndex)) { bestIndex = destIndex; break; } } if (bestIndex == m_currentWaypointIndex) { return INVALID_WAYPOINT_INDEX; } return bestIndex; } bool Bot::searchOptimalPoint (void) { // this function find a waypoint in the near of the bot if bot had lost his path of pathfinder needs // to be restarted over again. int busy = INVALID_WAYPOINT_INDEX; float lessDist[3] = { 99999.0f, 99999.0f , 99999.0f }; int lessIndex[3] = { INVALID_WAYPOINT_INDEX, INVALID_WAYPOINT_INDEX , INVALID_WAYPOINT_INDEX }; auto &bucket = waypoints.getWaypointsInBucket (pev->origin); int numToSkip = cr::clamp (rng.getInt (0, static_cast (bucket.length () - 1)), 0, 5); for (const int at : bucket) { bool skip = !!(at == m_currentWaypointIndex); // skip the current waypoint, if any if (skip) { continue; } // skip current and recent previous waypoints for (int j = 0; j < numToSkip; j++) { if (at == m_prevWptIndex[j]) { skip = true; break; } } // skip waypoint from recent list if (skip) { continue; } // cts with hostages should not pick waypoints with no hostage flag if ((g_mapFlags & MAP_CS) && m_team == TEAM_COUNTER && (waypoints[at].flags & FLAG_NOHOSTAGE) && hasHostage ()) { continue; } // ignore non-reacheable waypoints... if (!waypoints.isReachable (this, at)) { continue; } // check if waypoint is already used by another bot... if (g_timeRoundStart + 5.0f > engine.timebase () && isOccupiedPoint (at)) { busy = at; continue; } // if we're still here, find some close waypoints float distance = (pev->origin - waypoints[at].origin).lengthSq (); if (distance < lessDist[0]) { lessDist[2] = lessDist[1]; lessIndex[2] = lessIndex[1]; lessDist[1] = lessDist[0]; lessIndex[1] = lessIndex[0]; lessDist[0] = distance; lessIndex[0] = at; } else if (distance < lessDist[1]) { lessDist[2] = lessDist[1]; lessIndex[2] = lessIndex[1]; lessDist[1] = distance; lessIndex[1] = at; } else if (distance < lessDist[2]) { lessDist[2] = distance; lessIndex[2] = at; } } int selected = INVALID_WAYPOINT_INDEX; // now pick random one from choosen int index = 0; // choice from found if (lessIndex[2] != INVALID_WAYPOINT_INDEX) { index = rng.getInt (0, 2); } else if (lessIndex[1] != INVALID_WAYPOINT_INDEX) { index = rng.getInt (0, 1); } else if (lessIndex[0] != INVALID_WAYPOINT_INDEX) { index = 0; } selected = lessIndex[index]; // if we're still have no waypoint and have busy one (by other bot) pick it up if (selected == INVALID_WAYPOINT_INDEX && busy != INVALID_WAYPOINT_INDEX) { selected = busy; } // worst case... find atleast something if (selected == INVALID_WAYPOINT_INDEX) { selected = getNearestPoint (); } ignoreCollision (); changePointIndex (selected); return true; } float Bot::getReachTime (void) { auto task = taskId (); float estimatedTime = 0.0f; switch (task) { case TASK_PAUSE: case TASK_CAMP: case TASK_HIDE: return 0.0f; default: estimatedTime = 2.8f; // time to reach next waypoint } // calculate 'real' time that we need to get from one waypoint to another if (waypoints.exists (m_currentWaypointIndex) && waypoints.exists (m_prevWptIndex[0])) { float distance = (waypoints[m_prevWptIndex[0]].origin - waypoints[m_currentWaypointIndex].origin).length (); // caclulate estimated time if (pev->maxspeed <= 0.0f) { estimatedTime = 3.0f * distance / 240.0f; } else { estimatedTime = 3.0f * distance / pev->maxspeed; } bool longTermReachability = (m_currentPath->flags & FLAG_CROUCH) || (m_currentPath->flags & FLAG_LADDER) || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK); // check for special waypoints, that can slowdown our movement if (longTermReachability) { estimatedTime *= 2.0f; } estimatedTime = cr::clamp (estimatedTime, 1.2f, longTermReachability ? 8.0f : 5.0f); } return estimatedTime; } void Bot::getValidPoint (void) { // checks if the last waypoint the bot was heading for is still valid auto rechoiceGoal = [&] (void) { if (m_rechoiceGoalCount > 1) { int newGoal = searchGoal (); m_prevGoalIndex = newGoal; m_chosenGoalIndex = newGoal; task ()->data = newGoal; // do path finding if it's not the current waypoint if (newGoal != m_currentWaypointIndex) { searchPath (m_currentWaypointIndex, newGoal, m_pathType); } m_rechoiceGoalCount = 0; } else { searchOptimalPoint (); m_rechoiceGoalCount++; } }; // if bot hasn't got a waypoint we need a new one anyway or if time to get there expired get new one as well if (m_currentWaypointIndex == INVALID_WAYPOINT_INDEX) { clearSearchNodes (); rechoiceGoal (); m_waypointOrigin = m_currentPath->origin; } else if (m_navTimeset + getReachTime () < engine.timebase () && engine.isNullEntity (m_enemy)) { if (m_team == TEAM_TERRORIST) { int value = (g_experienceData + (m_currentWaypointIndex * waypoints.length ()) + m_currentWaypointIndex)->team0Damage; value += 100; if (value > MAX_DAMAGE_VALUE) { value = MAX_DAMAGE_VALUE; } (g_experienceData + (m_currentWaypointIndex * waypoints.length ()) + m_currentWaypointIndex)->team0Damage = static_cast (value); // affect nearby connected with victim waypoints for (int i = 0; i < MAX_PATH_INDEX; i++) { if (waypoints.exists (m_currentPath->index[i])) { value = (g_experienceData + (m_currentPath->index[i] * waypoints.length ()) + m_currentPath->index[i])->team0Damage; value += 2; if (value > MAX_DAMAGE_VALUE) { value = MAX_DAMAGE_VALUE; } (g_experienceData + (m_currentPath->index[i] * waypoints.length ()) + m_currentPath->index[i])->team0Damage = static_cast (value); } } } else { int value = (g_experienceData + (m_currentWaypointIndex * waypoints.length ()) + m_currentWaypointIndex)->team1Damage; value += 100; if (value > MAX_DAMAGE_VALUE) { value = MAX_DAMAGE_VALUE; } (g_experienceData + (m_currentWaypointIndex * waypoints.length ()) + m_currentWaypointIndex)->team1Damage = static_cast (value); // affect nearby connected with victim waypoints for (int i = 0; i < MAX_PATH_INDEX; i++) { if (waypoints.exists (m_currentPath->index[i])) { value = (g_experienceData + (m_currentPath->index[i] * waypoints.length ()) + m_currentPath->index[i])->team1Damage; value += 2; if (value > MAX_DAMAGE_VALUE) { value = MAX_DAMAGE_VALUE; } (g_experienceData + (m_currentPath->index[i] * waypoints.length ()) + m_currentPath->index[i])->team1Damage = static_cast (value); } } } clearSearchNodes (); rechoiceGoal (); m_waypointOrigin = m_currentPath->origin; } } int Bot::changePointIndex (int index) { if (index == INVALID_WAYPOINT_INDEX) { return 0; } m_prevWptIndex[4] = m_prevWptIndex[3]; m_prevWptIndex[3] = m_prevWptIndex[2]; m_prevWptIndex[2] = m_prevWptIndex[1]; m_prevWptIndex[0] = m_currentWaypointIndex; m_currentWaypointIndex = index; m_navTimeset = engine.timebase (); m_currentPath = &waypoints[index]; m_waypointFlags = m_currentPath->flags; return m_currentWaypointIndex; // to satisfy static-code analyzers } int Bot::getNearestPoint (void) { // get the current nearest waypoint to bot with visibility checks int index = INVALID_WAYPOINT_INDEX; float minimum = cr::square (1024.0f); auto &bucket = waypoints.getWaypointsInBucket (pev->origin); for (const auto at : bucket) { if (at == m_currentWaypointIndex) { continue; } float distance = (waypoints[at].origin - pev->origin).lengthSq (); if (distance < minimum) { // if bot doing navigation, make sure waypoint really visible and not too high if ((m_currentWaypointIndex != INVALID_WAYPOINT_INDEX && waypoints.isVisible (m_currentWaypointIndex, at)) || waypoints.isReachable (this, at)) { index = at; minimum = distance; } } } // worst case, take any waypoint... if (index == INVALID_WAYPOINT_INDEX) { index = waypoints.getNearestNoBuckets (pev->origin); } return index; } int Bot::getBombPoint (void) { // this function finds the best goal (bomb) waypoint for CTs when searching for a planted bomb. auto &goals = waypoints.m_goalPoints; auto bomb = waypoints.getBombPos (); auto audible = isBombAudible (); if (!audible.empty ()) { m_bombSearchOverridden = true; return waypoints.getNearest (audible, 240.0f); } else if (goals.empty ()) { return waypoints.getNearest (bomb, 240.0f, FLAG_GOAL); // reliability check } // take the nearest to bomb waypoints instead of goal if close enough else if ((pev->origin - bomb).lengthSq () < cr::square (512.0f)) { int waypoint = waypoints.getNearest (bomb, 240.0f); m_bombSearchOverridden = true; if (waypoint != INVALID_WAYPOINT_INDEX) { return waypoint; } } int goal = 0, count = 0; float lastDistance = 999999.0f; // find nearest goal waypoint either to bomb (if "heard" or player) for (auto &point : goals) { float distance = (waypoints[point].origin - bomb).lengthSq (); // check if we got more close distance if (distance < lastDistance) { goal = point; lastDistance = distance; } } while (waypoints.isVisited (goal)) { goal = goals.random (); if (count++ >= static_cast (goals.length ())) { break; } } return goal; } int Bot::getDefendPoint (const Vector &origin) { // this function tries to find a good position which has a line of sight to a position, // provides enough cover point, and is far away from the defending position if (m_currentWaypointIndex == INVALID_WAYPOINT_INDEX) { m_currentWaypointIndex = changePointIndex (getNearestPoint ()); } TraceResult tr; int waypointIndex[MAX_PATH_INDEX]; int minDistance[MAX_PATH_INDEX]; for (int i = 0; i < MAX_PATH_INDEX; i++) { waypointIndex[i] = INVALID_WAYPOINT_INDEX; minDistance[i] = 128; } int posIndex = waypoints.getNearest (origin); int srcIndex = m_currentWaypointIndex; // some of points not found, return random one if (srcIndex == INVALID_WAYPOINT_INDEX || posIndex == INVALID_WAYPOINT_INDEX) { return rng.getInt (0, waypoints.length () - 1); } // find the best waypoint now for (int i = 0; i < waypoints.length (); i++) { // exclude ladder & current waypoints if ((waypoints[i].flags & FLAG_LADDER) || i == srcIndex || !waypoints.isVisible (i, posIndex) || isOccupiedPoint (i)) { continue; } // use the 'real' pathfinding distances int distance = waypoints.getPathDist (srcIndex, i); // skip wayponts with distance more than 512 units if (distance > 512) { continue; } engine.testLine (waypoints[i].origin, waypoints[posIndex].origin, TRACE_IGNORE_EVERYTHING, ent (), &tr); // check if line not hit anything if (tr.flFraction != 1.0f) { continue; } for (int j = 0; j < MAX_PATH_INDEX; j++) { if (distance > minDistance[j]) { waypointIndex[j] = i; minDistance[j] = distance; } } } // use statistic if we have them for (int i = 0; i < MAX_PATH_INDEX; i++) { if (waypointIndex[i] != INVALID_WAYPOINT_INDEX) { Experience *exp = (g_experienceData + (waypointIndex[i] * waypoints.length ()) + waypointIndex[i]); int experience = INVALID_WAYPOINT_INDEX; if (m_team == TEAM_TERRORIST) { experience = exp->team0Damage; } else { experience = exp->team1Damage; } experience = (experience * 100) / (m_team == TEAM_TERRORIST ? g_highestDamageT : g_highestDamageCT); minDistance[i] = (experience * 100) / 8192; minDistance[i] += experience; } } bool isOrderChanged = false; // sort results waypoints for farest distance do { isOrderChanged = false; // completely sort the data for (int i = 0; i < 3 && waypointIndex[i] != INVALID_WAYPOINT_INDEX && waypointIndex[i + 1] != INVALID_WAYPOINT_INDEX && minDistance[i] > minDistance[i + 1]; i++) { int index = waypointIndex[i]; waypointIndex[i] = waypointIndex[i + 1]; waypointIndex[i + 1] = index; index = minDistance[i]; minDistance[i] = minDistance[i + 1]; minDistance[i + 1] = index; isOrderChanged = true; } } while (isOrderChanged); if (waypointIndex[0] == INVALID_WAYPOINT_INDEX) { IntArray found; for (int i = 0; i < waypoints.length (); i++) { if ((waypoints[i].origin - origin).lengthSq () <= cr::square (1248.0f) && !waypoints.isVisible (i, posIndex) && !isOccupiedPoint (i)) { found.push (i); } } if (found.empty ()) { return rng.getInt (0, waypoints.length () - 1); // most worst case, since there a evil error in waypoints } return found.random (); } int index = 0; for (; index < MAX_PATH_INDEX; index++) { if (waypointIndex[index] == INVALID_WAYPOINT_INDEX) { break; } } return waypointIndex[rng.getInt (0, static_cast ((index - 1) * 0.5f))]; } int Bot::getCoverPoint (float maxDistance) { // this function tries to find a good cover waypoint if bot wants to hide const float enemyMax = (m_lastEnemyOrigin - pev->origin).length (); // do not move to a position near to the enemy if (maxDistance > enemyMax) { maxDistance = enemyMax; } if (maxDistance < 300.0f) { maxDistance = 300.0f; } int srcIndex = m_currentWaypointIndex; int enemyIndex = waypoints.getNearest (m_lastEnemyOrigin); IntArray enemies; enemies.reserve (MAX_ENGINE_PLAYERS); int waypointIndex[MAX_PATH_INDEX]; int minDistance[MAX_PATH_INDEX]; for (int i = 0; i < MAX_PATH_INDEX; i++) { waypointIndex[i] = INVALID_WAYPOINT_INDEX; minDistance[i] = static_cast (maxDistance); } if (enemyIndex == INVALID_WAYPOINT_INDEX) { return INVALID_WAYPOINT_INDEX; } // now get enemies neigbouring points for (int i = 0; i < MAX_PATH_INDEX; i++) { if (waypoints[enemyIndex].index[i] != INVALID_WAYPOINT_INDEX) { enemies.push (waypoints[enemyIndex].index[i]); } } // ensure we're on valid point changePointIndex (srcIndex); // find the best waypoint now for (int i = 0; i < waypoints.length (); i++) { // exclude ladder, current waypoints and waypoints seen by the enemy if ((waypoints[i].flags & FLAG_LADDER) || i == srcIndex || waypoints.isVisible (enemyIndex, i)) { continue; } bool neighbourVisible = false; // now check neighbour waypoints for visibility for (auto &enemy : enemies) { if (waypoints.isVisible (enemy, i)) { neighbourVisible = true; break; } } // skip visible points if (neighbourVisible) { continue; } // use the 'real' pathfinding distances int distances = waypoints.getPathDist (srcIndex, i); int enemyDistance = waypoints.getPathDist (enemyIndex, i); if (distances >= enemyDistance) { continue; } for (int j = 0; j < MAX_PATH_INDEX; j++) { if (distances < minDistance[j]) { waypointIndex[j] = i; minDistance[j] = distances; break; } } } // use statistic if we have them for (int i = 0; i < MAX_PATH_INDEX; i++) { if (waypointIndex[i] != INVALID_WAYPOINT_INDEX) { Experience *exp = (g_experienceData + (waypointIndex[i] * waypoints.length ()) + waypointIndex[i]); int experience = INVALID_WAYPOINT_INDEX; if (m_team == TEAM_TERRORIST) { experience = exp->team0Damage; } else { experience = exp->team1Damage; } experience = (experience * 100) / MAX_DAMAGE_VALUE; minDistance[i] = (experience * 100) / 8192; minDistance[i] += experience; } } bool isOrderChanged; // sort resulting waypoints for nearest distance do { isOrderChanged = false; for (int i = 0; i < 3 && waypointIndex[i] != INVALID_WAYPOINT_INDEX && waypointIndex[i + 1] != INVALID_WAYPOINT_INDEX && minDistance[i] > minDistance[i + 1]; i++) { int index = waypointIndex[i]; waypointIndex[i] = waypointIndex[i + 1]; waypointIndex[i + 1] = index; index = minDistance[i]; minDistance[i] = minDistance[i + 1]; minDistance[i + 1] = index; isOrderChanged = true; } } while (isOrderChanged); TraceResult tr; // take the first one which isn't spotted by the enemy for (int i = 0; i < MAX_PATH_INDEX; i++) { if (waypointIndex[i] != INVALID_WAYPOINT_INDEX) { engine.testLine (m_lastEnemyOrigin + Vector (0.0f, 0.0f, 36.0f), waypoints[waypointIndex[i]].origin, TRACE_IGNORE_EVERYTHING, ent (), &tr); if (tr.flFraction < 1.0f) { return waypointIndex[i]; } } } // if all are seen by the enemy, take the first one if (waypointIndex[0] != INVALID_WAYPOINT_INDEX) { return waypointIndex[0]; } return INVALID_WAYPOINT_INDEX; // do not use random points } bool Bot::getNextBestPoint (void) { // this function does a realtime post processing of waypoints return from the // pathfinder, to vary paths and find the best waypoint on our way assert (!m_path.empty ()); assert (m_path.hasNext ()); if (!isOccupiedPoint (m_path.first ())) { return false; } for (int i = 0; i < MAX_PATH_INDEX; i++) { int id = m_currentPath->index[i]; if (id != INVALID_WAYPOINT_INDEX && waypoints.isConnected (id, m_path.next ()) && waypoints.isConnected (m_currentWaypointIndex, id)) { // don't use ladder waypoints as alternative if (waypoints[id].flags & FLAG_LADDER) { continue; } if (!isOccupiedPoint (id)) { m_path.first () = id; return true; } } } return false; } bool Bot::advanceMovement (void) { // advances in our pathfinding list and sets the appropiate destination origins for this bot getValidPoint (); // check if old waypoints is still reliable // no waypoints from pathfinding? if (m_path.empty ()) { return false; } TraceResult tr; m_path.shift (); // advance in list m_currentTravelFlags = 0; // reset travel flags (jumping etc) // we're not at the end of the list? if (!m_path.empty ()) { // if in between a route, postprocess the waypoint (find better alternatives)... if (m_path.hasNext () && m_path.first () != m_path.back ()) { getNextBestPoint (); m_minSpeed = pev->maxspeed; TaskID taskID = taskId (); // only if we in normal task and bomb is not planted if (taskID == TASK_NORMAL && g_timeRoundMid + 5.0f < engine.timebase () && m_timeCamping + 5.0f < engine.timebase () && !g_bombPlanted && m_personality != PERSONALITY_RUSHER && !m_hasC4 && !m_isVIP && m_loosedBombWptIndex == INVALID_WAYPOINT_INDEX && !hasHostage ()) { m_campButtons = 0; const int nextIndex = m_path.next (); float kills = 0; if (m_team == TEAM_TERRORIST) { kills = (g_experienceData + (nextIndex * waypoints.length ()) + nextIndex)->team0Damage; } else { kills = (g_experienceData + (nextIndex * waypoints.length ()) + nextIndex)->team1Damage; } // if damage done higher than one if (kills > 1.0f && g_timeRoundMid > engine.timebase ()) { switch (m_personality) { case PERSONALITY_NORMAL: kills *= 0.33f; break; default: kills *= 0.5f; break; } if (m_baseAgressionLevel < kills && hasPrimaryWeapon ()) { startTask (TASK_CAMP, TASKPRI_CAMP, INVALID_WAYPOINT_INDEX, engine.timebase () + rng.getFloat (m_difficulty * 0.5f, static_cast (m_difficulty)) * 5.0f, true); startTask (TASK_MOVETOPOSITION, TASKPRI_MOVETOPOSITION, getDefendPoint (waypoints[nextIndex].origin), engine.timebase () + rng.getFloat (3.0f, 10.0f), true); } } else if (g_botsCanPause && !isOnLadder () && !isInWater () && !m_currentTravelFlags && isOnFloor ()) { if (static_cast (kills) == m_baseAgressionLevel) { m_campButtons |= IN_DUCK; } else if (rng.getInt (1, 100) > m_difficulty * 25) { m_minSpeed = getShiftSpeed (); } } // force terrorist bot to plant bomb if (m_inBombZone && !m_hasProgressBar && m_hasC4) { int newGoal = searchGoal (); m_prevGoalIndex = newGoal; m_chosenGoalIndex = newGoal; // remember index task ()->data = newGoal; // do path finding if it's not the current waypoint if (newGoal != m_currentWaypointIndex) { searchPath (m_currentWaypointIndex, newGoal, m_pathType); } return false; } } } if (!m_path.empty ()) { const int destIndex = m_path.first (); // find out about connection flags if (m_currentWaypointIndex != INVALID_WAYPOINT_INDEX) { for (int i = 0; i < MAX_PATH_INDEX; i++) { if (m_currentPath->index[i] == destIndex) { m_currentTravelFlags = m_currentPath->connectionFlags[i]; m_desiredVelocity = m_currentPath->connectionVelocity[i]; m_jumpFinished = false; break; } } // check if bot is going to jump bool willJump = false; float jumpDistance = 0.0f; Vector src; Vector dst; // try to find out about future connection flags if (m_path.hasNext ()) { for (int i = 0; i < MAX_PATH_INDEX; i++) { const int nextIndex = m_path.next (); Path &path = waypoints[destIndex]; Path &next = waypoints[nextIndex]; if (path.index[i] == nextIndex && (path.connectionFlags[i] & PATHFLAG_JUMP)) { src = path.origin; dst = next.origin; jumpDistance = (path.origin - next.origin).length (); willJump = true; break; } } } // is there a jump waypoint right ahead and do we need to draw out the light weapon ? if (willJump && m_currentWeapon != WEAPON_KNIFE && m_currentWeapon != WEAPON_SCOUT && !m_isReloading && !usesPistol () && (jumpDistance > 210.0f || (dst.z - 32.0f > src.z && jumpDistance > 150.0f)) && !(m_states & (STATE_SEEING_ENEMY | STATE_SUSPECT_ENEMY))) { selectWeaponByName ("weapon_knife"); // draw out the knife if we needed } // bot not already on ladder but will be soon? if ((waypoints[destIndex].flags & FLAG_LADDER) && !isOnLadder ()) { // get ladder waypoints used by other (first moving) bots for (int c = 0; c < engine.maxClients (); c++) { Bot *otherBot = bots.getBot (c); // if another bot uses this ladder, wait 3 secs if (otherBot != nullptr && otherBot != this && otherBot->m_notKilled && otherBot->m_currentWaypointIndex == destIndex) { startTask (TASK_PAUSE, TASKPRI_PAUSE, INVALID_WAYPOINT_INDEX, engine.timebase () + 3.0f, false); return true; } } } } changePointIndex (destIndex); } } m_waypointOrigin = m_currentPath->origin; // if wayzone radius non zero vary origin a bit depending on the body angles if (m_currentPath->radius > 0.0f) { makeVectors (Vector (pev->angles.x, cr::angleNorm (pev->angles.y + rng.getFloat (-90.0f, 90.0f)), 0.0f)); m_waypointOrigin = m_waypointOrigin + g_pGlobals->v_forward * rng.getFloat (0.0f, m_currentPath->radius); } if (isOnLadder ()) { engine.testLine (Vector (pev->origin.x, pev->origin.y, pev->absmin.z), m_waypointOrigin, TRACE_IGNORE_EVERYTHING, ent (), &tr); if (tr.flFraction < 1.0f) { m_waypointOrigin = m_waypointOrigin + (pev->origin - m_waypointOrigin) * 0.5f + Vector (0.0f, 0.0f, 32.0f); } } m_navTimeset = engine.timebase (); return true; } bool Bot::cantMoveForward (const Vector &normal, TraceResult *tr) { // checks if bot is blocked in his movement direction (excluding doors) // use some TraceLines to determine if anything is blocking the current path of the bot. // first do a trace from the bot's eyes forward... Vector src = eyePos (); Vector forward = src + normal * 24.0f; makeVectors (Vector (0.0f, pev->angles.y, 0.0f)); auto checkDoor = [] (TraceResult *tr) { if (!(g_mapFlags & MAP_HAS_DOORS)) { return false; } return tr->flFraction < 1.0f && strncmp ("func_door", STRING (tr->pHit->v.classname), 9) != 0; }; // trace from the bot's eyes straight forward... engine.testLine (src, forward, TRACE_IGNORE_MONSTERS, ent (), tr); // check if the trace hit something... if (tr->flFraction < 1.0f) { if ((g_mapFlags & MAP_HAS_DOORS) && strncmp ("func_door", STRING (tr->pHit->v.classname), 9) == 0) { return false; } return true; // bot's head will hit something } // bot's head is clear, check at shoulder level... // trace from the bot's shoulder left diagonal forward to the right shoulder... src = eyePos () + Vector (0.0f, 0.0f, -16.0f) - g_pGlobals->v_right * -16.0f; forward = eyePos () + Vector (0.0f, 0.0f, -16.0f) + g_pGlobals->v_right * 16.0f + normal * 24.0f; engine.testLine (src, forward, TRACE_IGNORE_MONSTERS, ent (), tr); // check if the trace hit something... if (checkDoor (tr)) { return true; // bot's body will hit something } // bot's head is clear, check at shoulder level... // trace from the bot's shoulder right diagonal forward to the left shoulder... src = eyePos () + Vector (0.0f, 0.0f, -16.0f) + g_pGlobals->v_right * 16.0f; forward = eyePos () + Vector (0.0f, 0.0f, -16.0f) - g_pGlobals->v_right * -16.0f + normal * 24.0f; engine.testLine (src, forward, TRACE_IGNORE_MONSTERS, ent (), tr); // check if the trace hit something... if (checkDoor (tr)) { return true; // bot's body will hit something } // now check below waist if (pev->flags & FL_DUCKING) { src = pev->origin + Vector (0.0f, 0.0f, -19.0f + 19.0f); forward = src + Vector (0.0f, 0.0f, 10.0f) + normal * 24.0f; engine.testLine (src, forward, TRACE_IGNORE_MONSTERS, ent (), tr); // check if the trace hit something... if (checkDoor (tr)) { return true; // bot's body will hit something } src = pev->origin; forward = src + normal * 24.0f; engine.testLine (src, forward, TRACE_IGNORE_MONSTERS, ent (), tr); // check if the trace hit something... if (checkDoor (tr)) { return true; // bot's body will hit something } } else { // trace from the left waist to the right forward waist pos src = pev->origin + Vector (0.0f, 0.0f, -17.0f) - g_pGlobals->v_right * -16.0f; forward = pev->origin + Vector (0.0f, 0.0f, -17.0f) + g_pGlobals->v_right * 16.0f + normal * 24.0f; // trace from the bot's waist straight forward... engine.testLine (src, forward, TRACE_IGNORE_MONSTERS, ent (), tr); // check if the trace hit something... if (checkDoor (tr)) { return true; // bot's body will hit something } // trace from the left waist to the right forward waist pos src = pev->origin + Vector (0.0f, 0.0f, -17.0f) + g_pGlobals->v_right * 16.0f; forward = pev->origin + Vector (0.0f, 0.0f, -17.0f) - g_pGlobals->v_right * -16.0f + normal * 24.0f; engine.testLine (src, forward, TRACE_IGNORE_MONSTERS, ent (), tr); // check if the trace hit something... if (checkDoor (tr)) { return true; // bot's body will hit something } } return false; // bot can move forward, return false } #ifdef DEAD_CODE bool Bot::canStrafeLeft (TraceResult *tr) { // this function checks if bot can move sideways makeVectors (pev->v_angle); Vector src = pev->origin; Vector left = src - g_pGlobals->v_right * -40.0f; // trace from the bot's waist straight left... TraceLine (src, left, true, ent (), tr); // check if the trace hit something... if (tr->flFraction < 1.0f) { return false; // bot's body will hit something } src = left; left = left + g_pGlobals->v_forward * 40.0f; // trace from the strafe pos straight forward... TraceLine (src, left, true, ent (), tr); // check if the trace hit something... if (tr->flFraction < 1.0f) { return false; // bot's body will hit something } return true; } bool Bot::canStrafeRight (TraceResult *tr) { // this function checks if bot can move sideways makeVectors (pev->v_angle); Vector src = pev->origin; Vector right = src + g_pGlobals->v_right * 40.0f; // trace from the bot's waist straight right... TraceLine (src, right, true, ent (), tr); // check if the trace hit something... if (tr->flFraction < 1.0f) { return false; // bot's body will hit something } src = right; right = right + g_pGlobals->v_forward * 40.0f; // trace from the strafe pos straight forward... TraceLine (src, right, true, ent (), tr); // check if the trace hit something... if (tr->flFraction < 1.0f) { return false; // bot's body will hit something } return true; } #endif bool Bot::canJumpUp (const Vector &normal) { // this function check if bot can jump over some obstacle TraceResult tr; // can't jump if not on ground and not on ladder/swimming if (!isOnFloor () && (isOnLadder () || !isInWater ())) { return false; } // convert current view angle to vectors for traceline math... makeVectors (Vector (0.0f, pev->angles.y, 0.0f)); // check for normal jump height first... Vector src = pev->origin + Vector (0.0f, 0.0f, -36.0f + 45.0f); Vector dest = src + normal * 32.0f; // trace a line forward at maximum jump height... engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); if (tr.flFraction < 1.0f) { return doneCanJumpUp (normal); } else { // now trace from jump height upward to check for obstructions... src = dest; dest.z = dest.z + 37.0f; engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); if (tr.flFraction < 1.0f) { return false; } } // now check same height to one side of the bot... src = pev->origin + g_pGlobals->v_right * 16.0f + Vector (0.0f, 0.0f, -36.0f + 45.0f); dest = src + normal * 32.0f; // trace a line forward at maximum jump height... engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false if (tr.flFraction < 1.0f) { return doneCanJumpUp (normal); } // now trace from jump height upward to check for obstructions... src = dest; dest.z = dest.z + 37.0f; engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false if (tr.flFraction < 1.0f) { return false; } // now check same height on the other side of the bot... src = pev->origin + (-g_pGlobals->v_right * 16.0f) + Vector (0.0f, 0.0f, -36.0f + 45.0f); dest = src + normal * 32.0f; // trace a line forward at maximum jump height... engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false if (tr.flFraction < 1.0f) { return doneCanJumpUp (normal); } // now trace from jump height upward to check for obstructions... src = dest; dest.z = dest.z + 37.0f; engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false return tr.flFraction > 1.0f; } bool Bot::doneCanJumpUp (const Vector &normal) { // use center of the body first... maximum duck jump height is 62, so check one unit above that (63) Vector src = pev->origin + Vector (0.0f, 0.0f, -36.0f + 63.0f); Vector dest = src + normal * 32.0f; TraceResult tr; // trace a line forward at maximum jump height... engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); if (tr.flFraction < 1.0f) { return false; } else { // now trace from jump height upward to check for obstructions... src = dest; dest.z = dest.z + 37.0f; engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, check duckjump if (tr.flFraction < 1.0f) { return false; } } // now check same height to one side of the bot... src = pev->origin + g_pGlobals->v_right * 16.0f + Vector (0.0f, 0.0f, -36.0f + 63.0f); dest = src + normal * 32.0f; // trace a line forward at maximum jump height... engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false if (tr.flFraction < 1.0f) { return false; } // now trace from jump height upward to check for obstructions... src = dest; dest.z = dest.z + 37.0f; engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false if (tr.flFraction < 1.0f) { return false; } // now check same height on the other side of the bot... src = pev->origin + (-g_pGlobals->v_right * 16.0f) + Vector (0.0f, 0.0f, -36.0f + 63.0f); dest = src + normal * 32.0f; // trace a line forward at maximum jump height... engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false if (tr.flFraction < 1.0f) { return false; } // now trace from jump height upward to check for obstructions... src = dest; dest.z = dest.z + 37.0f; engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false return tr.flFraction > 1.0f; } bool Bot::canDuckUnder (const Vector &normal) { // this function check if bot can duck under obstacle TraceResult tr; Vector baseHeight; // convert current view angle to vectors for TraceLine math... makeVectors (Vector (0.0f, pev->angles.y, 0.0f)); // use center of the body first... if (pev->flags & FL_DUCKING) { baseHeight = pev->origin + Vector (0.0f, 0.0f, -17.0f); } else { baseHeight = pev->origin; } Vector src = baseHeight; Vector dest = src + normal * 32.0f; // trace a line forward at duck height... engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false if (tr.flFraction < 1.0f) { return false; } // now check same height to one side of the bot... src = baseHeight + g_pGlobals->v_right * 16.0f; dest = src + normal * 32.0f; // trace a line forward at duck height... engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false if (tr.flFraction < 1.0f) { return false; } // now check same height on the other side of the bot... src = baseHeight + (-g_pGlobals->v_right * 16.0f); dest = src + normal * 32.0f; // trace a line forward at duck height... engine.testLine (src, dest, TRACE_IGNORE_MONSTERS, ent (), &tr); // if trace hit something, return false return tr.flFraction > 1.0f; } #ifdef DEAD_CODE bool Bot::isBlockedLeft (void) { TraceResult tr; int direction = 48; if (m_moveSpeed < 0.0f) { direction = -48; } makeVectors (pev->angles); // do a trace to the left... engine.TestLine (pev->origin, g_pGlobals->v_forward * direction - g_pGlobals->v_right * 48.0f, TRACE_IGNORE_MONSTERS, ent (), &tr); // check if the trace hit something... if ((g_mapFlags & MAP_HAS_DOORS) && tr.flFraction < 1.0f && strncmp ("func_door", STRING (tr.pHit->v.classname), 9) != 0) { return true; // bot's body will hit something } return false; } bool Bot::isBlockedRight (void) { TraceResult tr; int direction = 48; if (m_moveSpeed < 0) { direction = -48; } makeVectors (pev->angles); // do a trace to the right... engine.TestLine (pev->origin, pev->origin + g_pGlobals->v_forward * direction + g_pGlobals->v_right * 48.0f, TRACE_IGNORE_MONSTERS, ent (), &tr); // check if the trace hit something... if ((g_mapFlags & MAP_HAS_DOORS) && tr.flFraction < 1.0f && (strncmp ("func_door", STRING (tr.pHit->v.classname), 9) != 0)) { return true; // bot's body will hit something } return false; } #endif bool Bot::checkWallOnLeft (void) { TraceResult tr; makeVectors (pev->angles); engine.testLine (pev->origin, pev->origin - g_pGlobals->v_right * 40.0f, TRACE_IGNORE_MONSTERS, ent (), &tr); // check if the trace hit something... if (tr.flFraction < 1.0f) { return true; } return false; } bool Bot::checkWallOnRight (void) { TraceResult tr; makeVectors (pev->angles); // do a trace to the right... engine.testLine (pev->origin, pev->origin + g_pGlobals->v_right * 40.0f, TRACE_IGNORE_MONSTERS, ent (), &tr); // check if the trace hit something... if (tr.flFraction < 1.0f) { return true; } return false; } bool Bot::isDeadlyMove (const Vector &to) { // this function eturns if given location would hurt Bot with falling damage Vector botPos = pev->origin; TraceResult tr; Vector move ((to - botPos).toYaw (), 0.0f, 0.0f); makeVectors (move); Vector direction = (to - botPos).normalize (); // 1 unit long Vector check = botPos; Vector down = botPos; down.z = down.z - 1000.0f; // straight down 1000 units engine.testHull (check, down, TRACE_IGNORE_MONSTERS, head_hull, ent (), &tr); if (tr.flFraction > 0.036f) { // we're not on ground anymore? tr.flFraction = 0.036f; } float lastHeight = tr.flFraction * 1000.0f; // height from ground float distance = (to - check).length (); // distance from goal while (distance > 16.0f) { check = check + direction * 16.0f; // move 10 units closer to the goal... down = check; down.z = down.z - 1000.0f; // straight down 1000 units engine.testHull (check, down, TRACE_IGNORE_MONSTERS, head_hull, ent (), &tr); if (tr.fStartSolid) { // Wall blocking? return false; } float height = tr.flFraction * 1000.0f; // height from ground if (lastHeight < height - 100.0f) {// Drops more than 100 Units? return true; } lastHeight = height; distance = (to - check).length (); // distance from goal } return false; } #ifdef DEAD_CODE void Bot::changePitch (float speed) { // this function turns a bot towards its ideal_pitch float idealPitch = AngleNormalize (pev->idealpitch); float curent = AngleNormalize (pev->v_angle.x); // turn from the current v_angle pitch to the idealpitch by selecting // the quickest way to turn to face that direction // find the difference in the curent and ideal angle float normalizePitch = AngleNormalize (idealPitch - curent); if (normalizePitch > 0.0f) { if (normalizePitch > speed) { normalizePitch = speed; } } else { if (normalizePitch < -speed) { normalizePitch = -speed; } } pev->v_angle.x = AngleNormalize (curent + normalizePitch); if (pev->v_angle.x > 89.9f) { pev->v_angle.x = 89.9f; } if (pev->v_angle.x < -89.9f) { pev->v_angle.x = -89.9f; } pev->angles.x = -pev->v_angle.x / 3; } void Bot::changeYaw (float speed) { // this function turns a bot towards its ideal_yaw float idealPitch = AngleNormalize (pev->ideal_yaw); float curent = AngleNormalize (pev->v_angle.y); // turn from the current v_angle yaw to the ideal_yaw by selecting // the quickest way to turn to face that direction // find the difference in the curent and ideal angle float normalizePitch = AngleNormalize (idealPitch - curent); if (normalizePitch > 0.0f) { if (normalizePitch > speed) { normalizePitch = speed; } } else { if (normalizePitch < -speed) { normalizePitch = -speed; } } pev->v_angle.y = AngleNormalize (curent + normalizePitch); pev->angles.y = pev->v_angle.y; } #endif int Bot::searchCampDir (void) { // find a good waypoint to look at when camping if (m_currentWaypointIndex == INVALID_WAYPOINT_INDEX) { m_currentWaypointIndex = changePointIndex (getNearestPoint ()); } int count = 0, indices[3]; float distTab[3]; uint16 visibility[3]; int currentWaypoint = m_currentWaypointIndex; for (int i = 0; i < waypoints.length (); i++) { if (currentWaypoint == i || !waypoints.isVisible (currentWaypoint, i)) { continue; } Path &path = waypoints[i]; if (count < 3) { indices[count] = i; distTab[count] = (pev->origin - path.origin).lengthSq (); visibility[count] = path.vis.crouch + path.vis.stand; count++; } else { float distance = (pev->origin - path.origin).lengthSq (); uint16 visBits = path.vis.crouch + path.vis.stand; for (int j = 0; j < 3; j++) { if (visBits >= visibility[j] && distance > distTab[j]) { indices[j] = i; distTab[j] = distance; visibility[j] = visBits; break; } } } } count--; if (count >= 0) { return indices[rng.getInt (0, count)]; } return rng.getInt (0, waypoints.length () - 1); } void Bot::processBodyAngles (void) { // set the body angles to point the gun correctly pev->angles.x = -pev->v_angle.x * (1.0f / 3.0f); pev->angles.y = pev->v_angle.y; pev->angles.clampAngles (); } void Bot::processLookAngles (void) { const float delta = cr::clamp (engine.timebase () - m_lookUpdateTime, cr::EQEPSILON, 0.05f); m_lookUpdateTime = engine.timebase (); // adjust all body and view angles to face an absolute vector Vector direction = (m_lookAt - eyePos ()).toAngles (); direction.x *= -1.0f; // invert for engine direction.clampAngles (); // lower skilled bot's have lower aiming if (m_difficulty < 2) { updateLookAnglesNewbie (direction, delta); processBodyAngles (); return; } if (m_difficulty > 3 && (m_aimFlags & AIM_ENEMY) && (m_wantsToFire || usesSniper ()) && yb_whose_your_daddy.boolean ()) { pev->v_angle = direction; processBodyAngles (); return; } float accelerate = 3000.0f; float stiffness = 200.0f; float damping = 25.0f; if ((m_aimFlags & (AIM_ENEMY | AIM_ENTITY | AIM_GRENADE)) && m_difficulty > 3) { accelerate += 800.0f; stiffness += 320.0f; damping -= 8.0f; } m_idealAngles = pev->v_angle; float angleDiffYaw = cr::angleDiff (direction.y, m_idealAngles.y); float angleDiffPitch = cr::angleDiff (direction.x, m_idealAngles.x); if (angleDiffYaw < 1.0f && angleDiffYaw > -1.0f) { m_lookYawVel = 0.0f; m_idealAngles.y = direction.y; } else { float accel = cr::clamp (stiffness * angleDiffYaw - damping * m_lookYawVel, -accelerate, accelerate); m_lookYawVel += delta * accel; m_idealAngles.y += delta * m_lookYawVel; } float accel = cr::clamp (2.0f * stiffness * angleDiffPitch - damping * m_lookPitchVel, -accelerate, accelerate); m_lookPitchVel += delta * accel; m_idealAngles.x += cr::clamp (delta * m_lookPitchVel, -89.0f, 89.0f); pev->v_angle = m_idealAngles; pev->v_angle.clampAngles (); processBodyAngles (); } void Bot::updateLookAnglesNewbie (const Vector &direction, const float delta) { Vector spring (13.0f, 13.0f, 0.0f); Vector damperCoefficient (0.22f, 0.22f, 0.0f); const float offset = cr::clamp (m_difficulty, 1, 4) * 25.0f; Vector influence = Vector (0.25f, 0.17f, 0.0f) * (100.0f - offset) / 100.f; Vector randomization = Vector (2.0f, 0.18f, 0.0f) * (100.0f - offset) / 100.f; const float noTargetRatio = 0.3f; const float offsetDelay = 1.2f; Vector stiffness; Vector randomize; m_idealAngles = direction.make2D (); m_idealAngles.clampAngles (); if (m_aimFlags & (AIM_ENEMY | AIM_ENTITY)) { m_playerTargetTime = engine.timebase (); m_randomizedIdealAngles = m_idealAngles; stiffness = spring * (0.2f + offset / 125.0f); } else { // is it time for bot to randomize the aim direction again (more often where moving) ? if (m_randomizeAnglesTime < engine.timebase () && ((pev->velocity.length () > 1.0f && m_angularDeviation.length () < 5.0f) || m_angularDeviation.length () < 1.0f)) { // is the bot standing still ? if (pev->velocity.length () < 1.0f) { randomize = randomization * 0.2f; // randomize less } else { randomize = randomization; } // randomize targeted location a bit (slightly towards the ground) m_randomizedIdealAngles = m_idealAngles + Vector (rng.getFloat (-randomize.x * 0.5f, randomize.x * 1.5f), rng.getFloat (-randomize.y, randomize.y), 0.0f); // set next time to do this m_randomizeAnglesTime = engine.timebase () + rng.getFloat (0.4f, offsetDelay); } float stiffnessMultiplier = noTargetRatio; // take in account whether the bot was targeting someone in the last N seconds if (engine.timebase () - (m_playerTargetTime + offsetDelay) < noTargetRatio * 10.0f) { stiffnessMultiplier = 1.0f - (engine.timebase () - m_timeLastFired) * 0.1f; // don't allow that stiffness multiplier less than zero if (stiffnessMultiplier < 0.0f) { stiffnessMultiplier = 0.5f; } } // also take in account the remaining deviation (slow down the aiming in the last 10°) stiffnessMultiplier *= m_angularDeviation.length () * 0.1f * 0.5f; // but don't allow getting below a certain value if (stiffnessMultiplier < 0.35f) { stiffnessMultiplier = 0.35f; } stiffness = spring * stiffnessMultiplier; // increasingly slow aim } // compute randomized angle deviation this time m_angularDeviation = m_randomizedIdealAngles - pev->v_angle; m_angularDeviation.clampAngles (); // spring/damper model aiming m_aimSpeed.x = stiffness.x * m_angularDeviation.x - damperCoefficient.x * m_aimSpeed.x; m_aimSpeed.y = stiffness.y * m_angularDeviation.y - damperCoefficient.y * m_aimSpeed.y; // influence of y movement on x axis and vice versa (less influence than x on y since it's // easier and more natural for the bot to "move its mouse" horizontally than vertically) m_aimSpeed.x += cr::clamp (m_aimSpeed.y * influence.y, -50.0f, 50.0f); m_aimSpeed.y += cr::clamp (m_aimSpeed.x * influence.x, -200.0f, 200.0f); // move the aim cursor pev->v_angle = pev->v_angle + delta * Vector (m_aimSpeed.x, m_aimSpeed.y, 0.0f); pev->v_angle.clampAngles (); } void Bot::setStrafeSpeed (const Vector &moveDir, float strafeSpeed) { makeVectors (pev->angles); const Vector &los = (moveDir - pev->origin).normalize2D (); float dot = los | g_pGlobals->v_forward.make2D (); if (dot > 0.0f && !checkWallOnRight ()) { m_strafeSpeed = strafeSpeed; } else if (!checkWallOnLeft ()) { m_strafeSpeed = -strafeSpeed; } } int Bot::locatePlantedC4 (void) { // this function tries to find planted c4 on the defuse scenario map and returns nearest to it waypoint if (m_team != TEAM_TERRORIST || !(g_mapFlags & MAP_DE)) { return INVALID_WAYPOINT_INDEX; // don't search for bomb if the player is CT, or it's not defusing bomb } edict_t *bombEntity = nullptr; // temporaly pointer to bomb // search the bomb on the map while (!engine.isNullEntity (bombEntity = g_engfuncs.pfnFindEntityByString (bombEntity, "classname", "grenade"))) { if (strcmp (STRING (bombEntity->v.model) + 9, "c4.mdl") == 0) { int nearestIndex = waypoints.getNearest (engine.getAbsPos (bombEntity)); if (waypoints.exists (nearestIndex)) { return nearestIndex; } break; } } return INVALID_WAYPOINT_INDEX; } bool Bot::isOccupiedPoint (int index) { if (!waypoints.exists (index)) { return true; } for (int i = 0; i < engine.maxClients (); i++) { const Client &client = g_clients[i]; if (!(client.flags & (CF_USED | CF_ALIVE)) || client.team != m_team || client.ent == ent ()) { continue; } auto bot = bots.getBot (i); if (bot == this) { continue; } if (bot != nullptr) { int occupyId = getShootingConeDeviation (bot->ent (), pev->origin) >= 0.7f ? bot->m_prevWptIndex[0] : bot->m_currentWaypointIndex; if (bot != nullptr) { if (index == occupyId) { return true; } } } float length = (waypoints[index].origin - client.origin).lengthSq (); if (length < cr::clamp (waypoints[index].radius, cr::square (32.0f), cr::square (90.0f))) { return true; } } return false; } edict_t *Bot::getNearestButton (const char *targetName) { // this function tries to find nearest to current bot button, and returns pointer to // it's entity, also here must be specified the target, that button must open. if (isEmptyStr (targetName)) { return nullptr; } float nearestDistance = 99999.0f; edict_t *searchEntity = nullptr, *foundEntity = nullptr; // find the nearest button which can open our target while (!engine.isNullEntity (searchEntity = g_engfuncs.pfnFindEntityByString (searchEntity, "target", targetName))) { const Vector &pos = engine.getAbsPos (searchEntity); // check if this place safe if (!isDeadlyMove (pos)) { float distance = (pev->origin - pos).lengthSq (); // check if we got more close button if (distance <= nearestDistance) { nearestDistance = distance; foundEntity = searchEntity; } } } return foundEntity; }