diff --git a/inc/graph.h b/inc/graph.h index ddc0fe6..168fa94 100644 --- a/inc/graph.h +++ b/inc/graph.h @@ -307,6 +307,11 @@ public: return m_paths.length (); } + // get the random node on map + int32_t random () const { + return rg.get (0, length () - 1); + } + // check if has editor bool hasEditor () const { return !!m_editor; diff --git a/inc/manager.h b/inc/manager.h index 78f41f6..1c9c0c8 100644 --- a/inc/manager.h +++ b/inc/manager.h @@ -151,6 +151,7 @@ public: void erase (Bot *bot); void handleDeath (edict_t *killer, edict_t *victim); void setLastWinner (int winner); + void checkBotModel (edict_t *ent, char *infobuffer); bool isTeamStacked (int team); bool kickRandom (bool decQuota = true, Team fromTeam = Team::Unassigned); diff --git a/inc/yapb.h b/inc/yapb.h index d245bb4..e09f082 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -771,6 +771,7 @@ private: bool m_grenadeRequested {}; // bot requested change to grenade bool m_needToSendWelcomeChat {}; // bot needs to greet people on server? bool m_switchedToKnifeDuringJump {}; // bot needs to revert weapon after jump? + bool m_isCreature {}; // bot is not a player, but something else ? zombie ? Pickup m_pickupType {}; // type of entity which needs to be used/picked up PathWalk m_pathWalk {}; // pointer to current node from path @@ -779,6 +780,7 @@ private: CollisionState m_collisionState {}; // collision State FindPath m_pathType {}; // which pathfinder to use uint8_t m_enemyParts {}; // visibility flags + uint16_t m_modelMask {}; // model mask bits TraceResult m_lastTrace[TraceChannel::Num] {}; // last trace result UniquePtr m_planner; @@ -894,6 +896,7 @@ private: bool canRunHeavyWeight (); bool isEnemyInSight (Vector &endPos); bool isEnemyNoticeable (float range); + bool isCreature (); void doPlayerAvoidance (const Vector &normal); void selectCampButtons (int index); @@ -953,6 +956,7 @@ private: void refreshEnemyPredict (); void syncUpdatePredictedIndex (); void updatePredictedIndex (); + void refreshModelName (char *infobuffer); void completeTask (); void executeTasks (); diff --git a/src/botlib.cpp b/src/botlib.cpp index 69f4197..5265748 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -325,7 +325,7 @@ void Bot::updatePickups () { // this function finds Items to collect or use in the near of a bot // don't try to pickup anything while on ladder or trying to escape from bomb... - if ((m_states & Sense::SeeingEnemy) || isOnLadder () || getCurrentTaskId () == Task::EscapeFromBomb || !cv_pickup_best.bool_ () || cv_jasonmode.bool_ () || !bots.hasInterestingEntities ()) { + if (m_isCreature || (m_states & Sense::SeeingEnemy) || isOnLadder () || getCurrentTaskId () == Task::EscapeFromBomb || !cv_pickup_best.bool_ () || cv_jasonmode.bool_ () || !bots.hasInterestingEntities ()) { m_pickupItem = nullptr; m_pickupType = Pickup::None; @@ -903,7 +903,7 @@ void Bot::checkMsgQueue () { return; } - if (!m_inBuyZone || game.is (GameFlags::CSDM)) { + if (!m_inBuyZone || game.is (GameFlags::CSDM) || m_isCreature) { m_buyPending = true; m_buyingFinished = true; @@ -1767,7 +1767,7 @@ void Bot::setConditions () { refreshEnemyPredict (); // check for grenades depending on difficulty - if (rg.chance (cr::max (25, m_difficulty * 25))) { + if (rg.chance (cr::max (25, m_difficulty * 25)) && !m_isCreature) { checkGrenadesThrow (); } @@ -1841,7 +1841,6 @@ void Bot::filterTasks () { float retreatLevel = (100.0f - (m_healthValue > 70.0f ? 100.0f : m_healthValue)) * tempFear; // retreat level depends on bot health if (m_numEnemiesLeft > m_numFriendsLeft / 2 && m_retreatTime < game.time () && m_seeEnemyTime - rg.get (2.0f, 4.0f) < game.time ()) { - float timeSeen = m_seeEnemyTime - game.time (); float timeHeard = m_heardSoundTime - game.time (); float ratio = 0.0f; @@ -1865,6 +1864,9 @@ void Bot::filterTasks () { else if (m_isVIP || m_isReloading || (sniping && usesSniper ())) { ratio *= 3.0f; // triple the seek cover desire if bot is VIP or reloading } + else if (m_isCreature) { + ratio = 0.0f; + } else { ratio /= 2.0f; // reduce seek cover otherwise } @@ -2553,7 +2555,7 @@ void Bot::checkRadioQueue () { break; case Radio::GetInPositionAndWaitForGo: - if ((game.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 1024.0f) { + if (!m_isCreature && ((game.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 1024.0f)) { pushRadioMessage (Radio::RogerThat); if (getCurrentTaskId () == Task::Camp) { @@ -2927,6 +2929,7 @@ void Bot::update () { else if (m_team == Team::CT && game.mapIs (MapFlags::HostageRescue)) { m_hasHostage = hasHostage (); } + m_isCreature = isCreature (); // is bot movement enabled bool botMovement = false; @@ -3537,7 +3540,7 @@ void Bot::blind_ () { } void Bot::camp_ () { - if (!cv_camping_allowed.bool_ ()) { + if (!cv_camping_allowed.bool_ () || m_isCreature) { completeTask (); return; } @@ -3627,6 +3630,11 @@ void Bot::camp_ () { } void Bot::hide_ () { + if (m_isCreature) { + completeTask (); + return; + }; + m_aimFlags |= AimFlags::Camp; m_checkTerrain = false; m_moveToGoal = false; @@ -3747,7 +3755,7 @@ void Bot::plantBomb_ () { m_aimFlags |= AimFlags::Camp; // we're still got the C4? - if (m_hasC4) { + if (m_hasC4 && !isKnifeMode ()) { if (m_currentWeapon != Weapon::C4) { selectWeaponById (Weapon::C4); } @@ -4821,7 +4829,7 @@ void Bot::logic () { m_wantsToFire = false; // avoid flyings grenades, if needed - if (cv_avoid_grenades.bool_ ()) { + if (cv_avoid_grenades.bool_ () && !m_isCreature) { avoidGrenades (); } m_isUsingGrenade = false; @@ -5660,6 +5668,9 @@ void Bot::updateHearing () { void Bot::enteredBuyZone (int buyState) { // this function is gets called when bot enters a buyzone, to allow bot to buy some stuff + if (m_isCreature) { + return; // creatures can't buy anything + } const int *econLimit = conf.getEconLimit (); // if bot is in buy zone, try to buy ammo for this weapon... @@ -5795,3 +5806,32 @@ bool Bot::isEnemyInFrustum (edict_t *enemy) { } return true; } + +void Bot::refreshModelName (char *infobuffer) { + if (infobuffer == nullptr) { + infobuffer = engfuncs.pfnGetInfoKeyBuffer (ent ()); + } + String modelName = engfuncs.pfnInfoKeyValue (infobuffer, "model"); + + // need at least two characters to test model mask + if (modelName.length () < 2) { + m_modelMask = 0; + return; + } + union ModelTest { + char model[2]; + uint16_t mask; + ModelTest (StringRef m) : model { m[0], m[1] } {}; + } modelTest { modelName }; + + // assign our model mask (tests against model done every bot update) + m_modelMask = modelTest.mask; +} + +bool Bot::isCreature () { + // current creature models are: zombie, chicken + constexpr auto modelMaskZombie = (('o' << 8) + 'z'); + constexpr auto modelMaskChicken = (('h' << 8) + 'c'); + + return m_modelMask == modelMaskZombie || m_modelMask == modelMaskChicken; +} diff --git a/src/combat.cpp b/src/combat.cpp index 1880848..e0f6f01 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -132,19 +132,22 @@ bool Bot::checkBodyParts (edict_t *target) { auto spot = target->v.origin; auto self = pev->pContainingEntity; - m_enemyParts = Visibility::None; - game.testLine (eyes, spot, TraceIgnore::Everything, self, &result); + // creatures can't hurt behind anything + auto ignoreFlags = m_isCreature ? TraceIgnore::None : TraceIgnore::Everything; - if (result.flFraction >= 1.0f) { + m_enemyParts = Visibility::None; + game.testLine (eyes, spot, ignoreFlags, self, &result); + + if (result.flFraction >= 1.0f && result.pHit == target) { m_enemyParts |= Visibility::Body; m_enemyOrigin = result.vecEndPos; } // check top of head spot.z += 25.0f; - game.testLine (eyes, spot, TraceIgnore::Everything, self, &result); + game.testLine (eyes, spot, ignoreFlags, self, &result); - if (result.flFraction >= 1.0f) { + if (result.flFraction >= 1.0f && result.pHit == target) { m_enemyParts |= Visibility::Head; m_enemyOrigin = result.vecEndPos; } @@ -162,9 +165,9 @@ bool Bot::checkBodyParts (edict_t *target) { else { spot.z = target->v.origin.z - standFeet; } - game.testLineChannel (TraceChannel::Enemy, eyes, spot, TraceIgnore::Everything, self, result); + game.testLineChannel (TraceChannel::Enemy, eyes, spot, ignoreFlags, self, result); - if (result.flFraction >= 1.0f) { + if (result.flFraction >= 1.0f && result.pHit == target) { m_enemyParts |= Visibility::Other; m_enemyOrigin = result.vecEndPos; @@ -177,9 +180,9 @@ bool Bot::checkBodyParts (edict_t *target) { Vector perp (-dir.y, dir.x, 0.0f); spot = target->v.origin + Vector (perp.x * edgeOffset, perp.y * edgeOffset, 0); - game.testLineChannel (TraceChannel::Enemy, eyes, spot, TraceIgnore::Everything, self, result); + game.testLineChannel (TraceChannel::Enemy, eyes, spot, ignoreFlags, self, result); - if (result.flFraction >= 1.0f) { + if (result.flFraction >= 1.0f && result.pHit == target) { m_enemyParts |= Visibility::Other; m_enemyOrigin = result.vecEndPos; @@ -187,9 +190,9 @@ bool Bot::checkBodyParts (edict_t *target) { } spot = target->v.origin - Vector (perp.x * edgeOffset, perp.y * edgeOffset, 0); - game.testLineChannel (TraceChannel::Enemy, eyes, spot, TraceIgnore::Everything, self, result); + game.testLineChannel (TraceChannel::Enemy, eyes, spot, ignoreFlags, self, result); - if (result.flFraction >= 1.0f) { + if (result.flFraction >= 1.0f && result.pHit == target) { m_enemyParts |= Visibility::Other; m_enemyOrigin = result.vecEndPos; @@ -1494,7 +1497,7 @@ bool Bot::hasAnyWeapons () { } bool Bot::isKnifeMode () { - return cv_jasonmode.bool_ () || (usesKnife () && !hasAnyWeapons ()); + return cv_jasonmode.bool_ () || (usesKnife () && !hasAnyWeapons ()) || m_isCreature; } void Bot::selectBestWeapon () { diff --git a/src/linkage.cpp b/src/linkage.cpp index 1a48e9c..9e6a511 100644 --- a/src/linkage.cpp +++ b/src/linkage.cpp @@ -260,6 +260,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) { // team changes, recounting the teams population, etc... ctrl.assignAdminRights (ent, infobuffer); + bots.checkBotModel (ent, infobuffer); if (game.is (GameFlags::Metamod)) { RETURN_META (MRES_IGNORED); diff --git a/src/manager.cpp b/src/manager.cpp index 2cdcb7a..f73086c 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -740,6 +740,15 @@ void BotManager::setLastWinner (int winner) { } } +void BotManager::checkBotModel (edict_t *ent, char *infobuffer) { + for (const auto &bot : bots) { + if (bot->ent () == ent) { + bot->refreshModelName (infobuffer); + break; + } + } +} + void BotManager::setWeaponMode (int selection) { // this function sets bots weapon mode @@ -1367,6 +1376,7 @@ void Bot::newRound () { for (auto &timer : m_chatterTimes) { timer = kMaxChatterRepeatInterval; } + refreshModelName (nullptr); m_isReloading = false; m_reloadState = Reload::None; diff --git a/src/navigate.cpp b/src/navigate.cpp index 01402b3..65a0323 100644 --- a/src/navigate.cpp +++ b/src/navigate.cpp @@ -13,6 +13,17 @@ int Bot::findBestGoal () { return goal; }; + if (m_isCreature) { + if (!graph.m_terrorPoints.empty ()) { + return graph.m_terrorPoints.random (); + } + + if (!graph.m_goalPoints.empty ()) { + return graph.m_goalPoints.random (); + } + return graph.random (); + } + // chooses a destination (goal) node for a bot if (m_team == Team::Terrorist && game.mapIs (MapFlags::Demolition)) { auto result = findBestGoalWhenBombAction (); @@ -282,7 +293,7 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offsensive) { } if (goalChoices[0] == kInvalidNodeIndex) { - return m_chosenGoalIndex = rg.get (0, graph.length () - 1); + return m_chosenGoalIndex = graph.random (); } bool sorting = false; @@ -1785,7 +1796,7 @@ int Bot::findDefendNode (const Vector &origin) { // some of points not found, return random one if (srcIndex == kInvalidNodeIndex || posIndex == kInvalidNodeIndex) { - return rg.get (0, graph.length () - 1); + return graph.random (); } // find the best node now @@ -1885,7 +1896,7 @@ int Bot::findDefendNode (const Vector &origin) { } if (found.empty ()) { - return rg.get (0, graph.length () - 1); // most worst case, since there a evil error in nodes + return graph.random (); // most worst case, since there a evil error in nodes } return found.random (); } @@ -2129,7 +2140,7 @@ bool Bot::advanceMovement () { Task taskID = getCurrentTaskId (); // only if we in normal task and bomb is not planted - if (taskID == Task::Normal && bots.getRoundMidTime () + 5.0f < game.time () && m_timeCamping + 5.0f < game.time () && !bots.isBombPlanted () && m_personality != Personality::Rusher && !m_hasC4 && !m_isVIP && m_loosedBombNodeIndex == kInvalidNodeIndex && !m_hasHostage) { + if (taskID == Task::Normal && bots.getRoundMidTime () + 5.0f < game.time () && m_timeCamping + 5.0f < game.time () && !bots.isBombPlanted () && m_personality != Personality::Rusher && !m_hasC4 && !m_isVIP && m_loosedBombNodeIndex == kInvalidNodeIndex && !m_hasHostage && !m_isCreature) { m_campButtons = 0; const int nextIndex = m_pathWalk.next (); @@ -2861,7 +2872,7 @@ int Bot::getRandomCampDir () { if (count >= 0) { return indices[rg.get (0, count)]; } - return rg.get (0, graph.length () - 1); + return graph.random (); } void Bot::updateBodyAngles () { @@ -3215,7 +3226,7 @@ void Bot::syncFindPath (int srcIndex, int destIndex, FindPath pathType) { destIndex = graph.getNearestNoBuckets (pev->origin, kInfiniteDistance, NodeFlag::Goal); if (!graph.exists (destIndex) || srcIndex == destIndex) { - destIndex = rg.get (0, graph.length () - 1); + destIndex = graph.random (); if (!graph.exists (destIndex)) { printf ("%s dest path index not valid (%d).", __func__, destIndex);