diff --git a/src/game/client/neo/c_neo_player.cpp b/src/game/client/neo/c_neo_player.cpp index 19a63a3b6..f610ee76a 100644 --- a/src/game/client/neo/c_neo_player.cpp +++ b/src/game/client/neo/c_neo_player.cpp @@ -83,6 +83,7 @@ IMPLEMENT_CLIENTCLASS_DT(C_NEO_Player, DT_NEO_Player, CNEO_Player) RecvPropInt(RECVINFO(m_iNextSpawnClassChoice)), RecvPropInt(RECVINFO(m_bInLean)), RecvPropEHandle(RECVINFO(m_hServerRagdoll)), + RecvPropEHandle(RECVINFO(m_hCommandingPlayer)), RecvPropBool(RECVINFO(m_bInThermOpticCamo)), RecvPropBool(RECVINFO(m_bLastTickInThermOpticCamo)), @@ -100,6 +101,7 @@ IMPLEMENT_CLIENTCLASS_DT(C_NEO_Player, DT_NEO_Player, CNEO_Player) RecvPropArray(RecvPropInt(RECVINFO(m_rfAttackersScores[0])), m_rfAttackersScores), RecvPropArray(RecvPropFloat(RECVINFO(m_rfAttackersAccumlator[0])), m_rfAttackersAccumlator), RecvPropArray(RecvPropInt(RECVINFO(m_rfAttackersHits[0])), m_rfAttackersHits), + RecvPropArray(RecvPropVector(RECVINFO(m_vLastPingByStar[0])), m_vLastPingByStar), RecvPropInt(RECVINFO(m_NeoFlags)), RecvPropString(RECVINFO(m_szNeoName)), diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index 0a10b14cd..8f5292076 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -217,6 +217,8 @@ class C_NEO_Player : public C_HL2MP_Player CNetworkVar(float, m_flNextPingTime); + CNetworkArray(Vector, m_vLastPingByStar, STAR__TOTAL); + CNetworkVar(bool, m_bInThermOpticCamo); CNetworkVar(bool, m_bLastTickInThermOpticCamo); CNetworkVar(bool, m_bInVision); @@ -225,6 +227,7 @@ class C_NEO_Player : public C_HL2MP_Player CNetworkVar(bool, m_bCarryingGhost); CNetworkVar(bool, m_bIneligibleForLoadoutPick); CNetworkHandle(CBaseEntity, m_hServerRagdoll); + CNetworkHandle(CBasePlayer, m_hCommandingPlayer); CNetworkVar(int, m_iNeoClass); CNetworkVar(int, m_iNeoSkin); diff --git a/src/game/client/neo/ui/neo_hud_round_state.cpp b/src/game/client/neo/ui/neo_hud_round_state.cpp index 3649b9e23..8e98da8de 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -536,7 +536,7 @@ void CNEOHud_RoundState::DrawNeoHudElement() bool bDMRightSide = false; if (NEORules()->IsTeamplay()) { - for (int i = 0; i < (MAX_PLAYERS + 1); i++) + for (int i = 1; i <= gpGlobals->maxClients; i++) { if (g_PR->IsConnected(i)) { @@ -615,6 +615,14 @@ void CNEOHud_RoundState::DrawNeoHudElement() void CNEOHud_RoundState::DrawPlayerList() { + ConVarRef cl_neo_bot_cmdr_enable_ref("sv_neo_bot_cmdr_enable"); + Assert(cl_neo_bot_cmdr_enable_ref.IsValid()); + if (cl_neo_bot_cmdr_enable_ref.IsValid() && cl_neo_bot_cmdr_enable_ref.GetBool()) + { + DrawPlayerList_BotCmdr(); + return; + } + if (g_PR) { // Draw members of players squad in an old style list @@ -638,7 +646,7 @@ void CNEOHud_RoundState::DrawPlayerList() { bool squadMateFound = false; - for (int i = 0; i < (MAX_PLAYERS + 1); i++) + for (int i = 1; i <= gpGlobals->maxClients; i++) { if (i == localPlayerIndex) { @@ -673,7 +681,7 @@ void CNEOHud_RoundState::DrawPlayerList() m_iRightPlayersAlive = 0; // Draw other team mates - for (int i = 0; i < (MAX_PLAYERS + 1); i++) + for (int i = 1; i <= gpGlobals->maxClients; i++) { if (!g_PR->IsConnected(i)) { @@ -712,8 +720,127 @@ void CNEOHud_RoundState::DrawPlayerList() } } +void CNEOHud_RoundState::DrawPlayerList_BotCmdr() +{ + if (!g_PR) + { + return; + } + + // Draw members of players squad in an old style list + const int localPlayerTeam = GetLocalPlayerTeam(); + const int localPlayerIndex = GetLocalPlayerIndex(); + const bool localPlayerSpec = !(localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF); + const int leftTeam = cl_neo_hud_team_swap_sides.GetBool() ? (localPlayerSpec ? TEAM_JINRAI : localPlayerTeam) : TEAM_JINRAI; + + int offset = 52; + if (cl_neo_squad_hud_star_scale.GetFloat() > 0) + { + IntDim res = {}; + surface()->GetScreenSize(res.w, res.h); + offset *= cl_neo_squad_hud_star_scale.GetFloat() * res.h / 1080.0f; + } + + const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoScoreBoard->IsVisible(); + + // Draw squad mates + m_commandedList.RemoveAll(); + m_nonCommandedList.RemoveAll(); + m_nonSquadList.RemoveAll(); + + bool squadMateFound = false; + m_iLeftPlayersAlive = 0; + m_iRightPlayersAlive = 0; + const int localStar = g_PR->GetStar(localPlayerIndex); + + // Single pass to collect and categorize players + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + if (!g_PR->IsConnected(i)) + { + continue; + } + const int playerTeam = g_PR->GetTeam(i); + if (playerTeam != leftTeam) + { + if (g_PR->IsAlive(i)) + { + m_iRightPlayersAlive++; + } + } + else { + if (g_PR->IsAlive(i)) + { + m_iLeftPlayersAlive++; + } + } + if (playerTeam != localPlayerTeam) + { + continue; + } + if (i == localPlayerIndex || localPlayerSpec || hideDueToScoreboard || !ArePlayersOnSameTeam(i, localPlayerIndex)) + { + continue; + } + + // Only consider players in the same squad star as the local player + const bool isSameSquadStar = (localStar != STAR_NONE) && (g_PR->GetStar(i) == localStar); + if (isSameSquadStar) + { + C_NEO_Player* pPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); + bool isCommanded = (pPlayer && pPlayer->m_hCommandingPlayer.Get() == C_NEO_Player::GetLocalNEOPlayer()); + if (isCommanded) + { + m_commandedList.AddToTail(i); + } + else + { + m_nonCommandedList.AddToTail(i); + } + squadMateFound = true; + } + else + { + m_nonSquadList.AddToTail(i); + } + } + + // Draw commanded players first + const auto player = C_NEO_Player::GetLocalNEOPlayer(); + TeamLogoColor* pTeamLogoColor = player ? &m_teamLogoColors[player->GetTeamNumber()] : nullptr; + const Color* colorOverride = pTeamLogoColor ? &pTeamLogoColor->color : nullptr; + for (int i = 0; i < m_commandedList.Count(); ++i) + { + offset = DrawPlayerRow_BotCmdr(m_commandedList[i], offset, false, colorOverride); + } + + // Draw non-commanded players (who are also in the same squad star) + for (int i = 0; i < m_nonCommandedList.Count(); ++i) + { + offset = DrawPlayerRow_BotCmdr(m_nonCommandedList[i], offset, false); + } + + if (squadMateFound) + { + offset += 12; + } + + // Draw other team mates + for (int i = 0; i < m_nonSquadList.Count(); ++i) + { + offset = DrawPlayerRow_BotCmdr(m_nonSquadList[i], offset, true); + } +} + int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool small) { + ConVarRef cl_neo_bot_cmdr_enable_ref("sv_neo_bot_cmdr_enable"); + Assert(cl_neo_bot_cmdr_enable_ref.IsValid()); + if (cl_neo_bot_cmdr_enable_ref.IsValid() && cl_neo_bot_cmdr_enable_ref.GetBool()) + { + return DrawPlayerRow_BotCmdr(playerIndex, yOffset, small); + } + // Draw player static constexpr int SQUAD_MATE_TEXT_LENGTH = 62; // 31 characters in name without end character max plus 3 in short rank name plus 7 max in class name plus 3 max in health plus other characters char squadMateText[SQUAD_MATE_TEXT_LENGTH]; @@ -749,7 +876,7 @@ int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool s auto* pImpersonator = pNeoPlayer ? pNeoPlayer->m_hSpectatorTakeoverPlayerImpersonatingMe.Get() : nullptr; const char* pPlayerDisplayName = pImpersonator ? - pImpersonator->GetPlayerName() + pImpersonator->GetPlayerName() : g_PR->GetPlayerName(playerIndex); const char* displayClass = pImpersonator ? GetNeoClassName(pImpersonator->m_iClassBeforeTakeover) : squadMateClass; @@ -767,6 +894,62 @@ int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool s return yOffset + fontHeight; } +int CNEOHud_RoundState::DrawPlayerRow_BotCmdr(int playerIndex, const int yOffset, bool small, const Color* colorOverride) +{ + // Draw player + static constexpr int SQUAD_MATE_TEXT_LENGTH = 62; // 31 characters in name without end character max plus 3 in short rank name plus 7 max in class name plus 3 max in health plus other characters + char squadMateText[SQUAD_MATE_TEXT_LENGTH]; + wchar_t wSquadMateText[SQUAD_MATE_TEXT_LENGTH]; + const char* squadMateRankName = GetRankName(g_PR->GetXP(playerIndex), true); + const char* squadMateClass = GetNeoClassName(g_PR->GetClass(playerIndex)); + const bool isAlive = g_PR->IsAlive(playerIndex); + + C_NEO_Player* pNeoPlayer = ToNEOPlayer(UTIL_PlayerByIndex(playerIndex)); + if (isAlive) + { + const char* pPlayerDisplayName = pNeoPlayer ? + pNeoPlayer->GetPlayerNameWithTakeoverContext(playerIndex) + : g_PR->GetPlayerName(playerIndex); + + const char* displayRankName = squadMateRankName; + if (pNeoPlayer) + { + C_NEO_Player* pTakeoverTarget = ToNEOPlayer(pNeoPlayer->m_hSpectatorTakeoverPlayerTarget.Get()); + if (pTakeoverTarget) + { + displayRankName = GetRankName(g_PR->GetXP(pTakeoverTarget->entindex()), true); + } + } + + const int healthMode = cl_neo_hud_health_mode.GetInt(); + char playerHealth[7]; // 4 digits + 2 letters + V_snprintf(playerHealth, sizeof(playerHealth), healthMode ? "%dhp" : "%d%%", g_PR->GetDisplayedHealth(playerIndex, healthMode)); + V_snprintf(squadMateText, SQUAD_MATE_TEXT_LENGTH, "%s %s [%s] %s", pPlayerDisplayName, squadMateRankName, squadMateClass, playerHealth); + } + else + { + auto* pImpersonator = pNeoPlayer ? pNeoPlayer->m_hSpectatorTakeoverPlayerImpersonatingMe.Get() : nullptr; + + const char* pPlayerDisplayName = pImpersonator ? + pImpersonator->GetPlayerName() + : g_PR->GetPlayerName(playerIndex); + + const char* displayClass = pImpersonator ? GetNeoClassName(pImpersonator->m_iClassBeforeTakeover) : squadMateClass; + V_snprintf(squadMateText, SQUAD_MATE_TEXT_LENGTH, "%s [%s] DEAD", pPlayerDisplayName, displayClass); + } + int wSquadMateTextLen = g_pVGuiLocalize->ConvertANSIToUnicode(squadMateText, wSquadMateText, sizeof(wSquadMateText)); + + int fontWidth, fontHeight; + surface()->DrawSetTextFont(small ? m_hOCRSmallerFont : m_hOCRSmallFont); + surface()->GetTextSize(m_hOCRSmallFont, m_wszPlayersAliveUnicode, fontWidth, fontHeight); + + surface()->DrawSetTextColor(colorOverride ? *colorOverride : (isAlive ? COLOR_FADED_WHITE : COLOR_DARK_FADED_WHITE)); + surface()->DrawSetTextPos(8, yOffset); + surface()->DrawPrintText(wSquadMateText, wSquadMateTextLen - 1); + + return yOffset + fontHeight; +} + void CNEOHud_RoundState::DrawPlayer(int playerIndex, int teamIndex, const TeamLogoColor &teamLogoColor, const int xOffset, const bool drawHealthClass) { @@ -792,6 +975,23 @@ void CNEOHud_RoundState::DrawPlayer(int playerIndex, int teamIndex, const TeamLo surface()->DrawSetColor(COLOR_DARK); surface()->DrawTexturedRect(xOffset, Y_POS + 1, xOffset + m_ilogoSize, Y_POS + m_ilogoSize + 1); + ConVarRef cl_neo_bot_cmdr_enable_ref("sv_neo_bot_cmdr_enable"); + Assert(cl_neo_bot_cmdr_enable_ref.IsValid()); + if (cl_neo_bot_cmdr_enable_ref.IsValid() && cl_neo_bot_cmdr_enable_ref.GetBool()) + { + // Draw Command Highlight Border on top of the avatar image + C_NEO_Player* pPlayer = static_cast(UTIL_PlayerByIndex(playerIndex)); + if (pPlayer && pPlayer->m_hCommandingPlayer.Get() == C_NEO_Player::GetLocalNEOPlayer()) + { + surface()->DrawSetColor(COLOR_WHITE); + // Draw a thicker border inwards + for (int borderIndex = 0; borderIndex < 3; ++borderIndex) + { + surface()->DrawOutlinedRect(xOffset + borderIndex, Y_POS + borderIndex, xOffset + m_ilogoSize - borderIndex, Y_POS + m_ilogoSize - borderIndex); + } + } + } + // Deathmatch only: Draw XP on everyone if (!NEORules()->IsTeamplay()) { diff --git a/src/game/client/neo/ui/neo_hud_round_state.h b/src/game/client/neo/ui/neo_hud_round_state.h index b1aee9e3d..6f30c8dd3 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.h +++ b/src/game/client/neo/ui/neo_hud_round_state.h @@ -44,7 +44,9 @@ class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, publ void CheckActiveStar(); void DrawPlayerList(); + void DrawPlayerList_BotCmdr(); int DrawPlayerRow(int playerIndex, int yOffset, bool small = false); + int DrawPlayerRow_BotCmdr(int playerIndex, int yOffset, bool small = false, const Color* highlightColor = nullptr); void DrawPlayer(int playerIndex, int teamIndex, const TeamLogoColor &teamLogoColor, const int xOffset, const bool drawHealthClass); void SetTextureToAvatar(int playerIndex); @@ -98,6 +100,11 @@ class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, publ int m_iBeepSecsTotal = 0; NeoRoundStatus m_ePrevRoundStatus = NeoRoundStatus::Idle; + // Bot Commander Lists + CUtlVector m_commandedList; + CUtlVector m_nonCommandedList; + CUtlVector m_nonSquadList; + CPanelAnimationVar(Color, box_color, "box_color", "200 200 200 40"); CPanelAnimationVarAliasType(bool, health_monochrome, "health_monochrome", "1", "bool"); CPanelAnimationVarAliasType(bool, top_left_corner, "top_left_corner", "0", "bool"); diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index b7e4b3c74..386a7225e 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1436,6 +1436,8 @@ target_sources_grouped( neo/bot/behavior/neo_bot_attack.h neo/bot/behavior/neo_bot_behavior.cpp neo/bot/behavior/neo_bot_behavior.h + neo/bot/behavior/neo_bot_command_follow.cpp + neo/bot/behavior/neo_bot_command_follow.h neo/bot/behavior/neo_bot_dead.cpp neo/bot/behavior/neo_bot_dead.h neo/bot/behavior/neo_bot_jgr_capture.cpp @@ -1452,6 +1454,8 @@ target_sources_grouped( neo/bot/behavior/neo_bot_melee_attack.h neo/bot/behavior/neo_bot_move_to_vantage_point.cpp neo/bot/behavior/neo_bot_move_to_vantage_point.h + neo/bot/behavior/neo_bot_pause.cpp + neo/bot/behavior/neo_bot_pause.h neo/bot/behavior/neo_bot_retreat_to_cover.cpp neo/bot/behavior/neo_bot_retreat_to_cover.h neo/bot/behavior/neo_bot_retreat_from_grenade.cpp diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp new file mode 100644 index 000000000..adaa7545a --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -0,0 +1,369 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_command_follow.h" +#include "nav_mesh.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +ConVar sv_neo_bot_cmdr_debug_pause_uncommanded("sv_neo_bot_cmdr_debug_pause_uncommanded", "0", + FCVAR_CHEAT, "If true, uncommanded bots in behavior CNEOBotSeekAndDestroy will pause.", false, 0, false, 1); + +// NEOTODO: Figure out a clean way to represent config distances as Hammer units, without needing to square on use +ConVar sv_neo_bot_cmdr_stop_distance_sq("sv_neo_bot_cmdr_stop_distance_sq", "5000", + FCVAR_CHEAT, "Minimum distance gap between following bots", true, 3000, true, 100000); + +ConVar sv_neo_bot_cmdr_look_weights_friendly_repulsion("sv_neo_bot_cmdr_look_weights_friendly_repulsion", "2", + FCVAR_CHEAT, "Weight for friendly bot repulsion force", true, 1, true, 9999); +ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion("sv_neo_bot_cmdr_look_weights_wall_repulsion", "3", + FCVAR_CHEAT, "Weight for wall repulsion force", true, 1, true, 9999); +ConVar sv_neo_bot_cmdr_look_weights_explosives_repulsion("sv_neo_bot_cmdr_look_weights_explosives_repulsion", "4", + FCVAR_CHEAT, "Weight for explosive repulsion force", true, 1, true, 9999); + +ConVar sv_neo_bot_cmdr_look_weights_friendly_max_dist("sv_neo_bot_cmdr_look_weights_friendly_max_dist", "5000", + FCVAR_CHEAT, "Distance to compare friendly repulsion forces", true, 1, true, MAX_TRACE_LENGTH); +ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist("sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist", "500", + FCVAR_CHEAT, "Distance to extend whiskers", true, 1, true, MAX_TRACE_LENGTH); + +//--------------------------------------------------------------------------------------------- +CNEOBotCommandFollow::CNEOBotCommandFollow() : m_vGoalPos( CNEO_Player::VECTOR_INVALID_WAYPOINT ) +{ +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCommandFollow::OnStart(CNEOBot *me, Action< CNEOBot > *priorAction) +{ + m_path.SetMinLookAheadDistance(me->GetDesiredPathLookAheadRange()); + + if (!FollowCommandChain(me)) + { + return Done("No commander to follow"); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCommandFollow::Update(CNEOBot *me, float interval) +{ + if (!FollowCommandChain(me)) + { + return Done("Lost commander or released"); + } + + m_path.Update(me); + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCommandFollow::OnResume(CNEOBot *me, Action< CNEOBot > *interruptingAction) +{ + if (!FollowCommandChain(me)) + { + return Done("No commander to follow on resume"); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotCommandFollow::OnEnd(CNEOBot *me, Action< CNEOBot > *nextAction) +{ + me->m_hLeadingPlayer = nullptr; + me->m_hCommandingPlayer = nullptr; +} + + +// --------------------------------------------------------------------------------------------- +// Process commander ping waypoint commands for bots +// true - order has been processed, continue following +// false - no order, stop following +bool CNEOBotCommandFollow::FollowCommandChain(CNEOBot* me) +{ + Assert(me); + CNEO_Player* pCommander = me->m_hCommandingPlayer.Get(); + + if (!pCommander) + { + return false; + } + + // Mirror behavior of leader if we have one + if ( CNEO_Player *pPlayerToMirror = me->m_hLeadingPlayer.Get() ) + { + if (pPlayerToMirror->GetInThermOpticCamo()) + { + me->EnableCloak(4.0f); + } + else + { + me->DisableCloak(); + } + + if (pPlayerToMirror->IsDucking()) + { + me->PressCrouchButton(0.5f); + } + else + { + me->ReleaseCrouchButton(); + } + } + + // Calibrate dynamic follow distance + float follow_stop_distance_sq = sv_neo_bot_cmdr_stop_distance_sq.GetFloat(); + if (pCommander->m_flBotDynamicFollowDistanceSq > follow_stop_distance_sq) + { + follow_stop_distance_sq = pCommander->m_flBotDynamicFollowDistanceSq; + } + + if (!pCommander->IsAlive()) + { + // Commander died + return false; // This triggers OnEnd which clears vars + } + + // Follow commander if they are close enough to collect you (and ping cooldown elapsed) + if (pCommander->m_tBotPlayerPingCooldown.IsElapsed() + && (me->GetStar() == pCommander->GetStar()) // only follow when commander is focused on your squad + && (me->GetAbsOrigin().DistToSqr(pCommander->GetAbsOrigin()) < sv_neo_bot_cmdr_stop_distance_sq.GetFloat())) + { + // Use sv_neo_bot_cmdr_stop_distance_sq for consistent bot collection range + // follow_stop_distance_sq would be confusing if player doesn't know about distance tuning + me->m_hLeadingPlayer = pCommander; + m_vGoalPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + pCommander->m_vLastPingByStar.GetForModify(me->GetStar()) = CNEO_Player::VECTOR_INVALID_WAYPOINT; + } + // Go to commander's ping + else if (pCommander->m_vLastPingByStar.Get(me->GetStar()) != CNEO_Player::VECTOR_INVALID_WAYPOINT) + { + // Check if there's been an update for this star's ping waypoint + if (pCommander->m_vLastPingByStar.Get(me->GetStar()) != me->m_vLastPingByStar.Get(me->GetStar())) + { + me->m_hLeadingPlayer = nullptr; // Stop following and start travelling to ping + m_vGoalPos = pCommander->m_vLastPingByStar.Get(me->GetStar()); + me->m_vLastPingByStar.GetForModify(me->GetStar()) = pCommander->m_vLastPingByStar.Get(me->GetStar()); + + // Force a repath to allow for fine tuned positioning + CNEOBotPathCost cost(me, DEFAULT_ROUTE); + if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) + { + return true; + } + else + { + me->m_hLeadingPlayer = pCommander; // fallback to following commander + // continue with leader following logic below + } + } + + if (FanOutAndCover(me, m_vGoalPos)) + { + // FanOutAndCover true: arrived at destination and settled, so don't recompute path + return true; + } + + CNEOBotPathCost cost(me, DEFAULT_ROUTE); + if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) + { + return true; + } + else + { + me->m_hLeadingPlayer = pCommander; // fallback to following commander + // continue with leader following logic below + } + } + + // Didn't have order from commander, so follow in snake formation + CNEO_Player* pLeader = me->m_hLeadingPlayer.Get(); + if (!pLeader || !pLeader->IsAlive()) + { + // Leader is no longer valid or alive + me->m_hLeadingPlayer = nullptr; + return true; + } + + // Commander can swap and order around different squads while having followers + // but otherwise bots only follow squadmates in the same star or their commander + if ((pLeader != pCommander) && (pLeader->GetStar() != me->GetStar())) + { + me->m_hLeadingPlayer = nullptr; + return true; // restart logic next tick + } + else if (me->GetAbsOrigin().DistToSqr(pLeader->GetAbsOrigin()) < follow_stop_distance_sq) + { + // Anti-collision: follow neighbor in snake chain + for (int idx = 1; idx <= gpGlobals->maxClients; ++idx) + { + CNEO_Player* pOther = static_cast(UTIL_PlayerByIndex(idx)); + if (!pOther || !pOther->IsBot() || pOther == me + || (pOther->m_hLeadingPlayer.Get() != me->m_hLeadingPlayer.Get())) + { + // Not another bot in the same snake formation + continue; + } + + if (me->GetAbsOrigin().DistToSqr(pOther->GetAbsOrigin()) < follow_stop_distance_sq / 2) + { + // Follow person I bumped into and link up to snake chain of followers + me->m_hLeadingPlayer = pOther; + break; + } + } + + m_hTargetEntity = NULL; + m_bGoingToTargetEntity = false; + m_path.Invalidate(); + Vector tempLeaderOrigin = pLeader->GetAbsOrigin(); // don't want to override m_vGoalPos + FanOutAndCover(me, tempLeaderOrigin, false, follow_stop_distance_sq); + return true; + } + + // Set the bot's goal to the leader's position. + m_vGoalPos = pLeader->GetAbsOrigin(); + CNEOBotPathCost cost(me, DEFAULT_ROUTE); + if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) + { + // Prioritize following leader. + return true; + } + else if (me->m_hLeadingPlayer.Get() == me->m_hCommandingPlayer.Get()) + { + // Invalid path to leader who is also the commander, so reset both + // Returning false will trigger OnEnd which resets vars + return false; + } + else + { + // check command chain on next tick + me->m_hLeadingPlayer = nullptr; + return true; + } +} + +// --------------------------------------------------------------------------------------------- +// Process commander ping waypoint commands for bots +// The movementTarget is mutable to accomodate spreading out at goal zone +bool CNEOBotCommandFollow::FanOutAndCover(CNEOBot* me, Vector& movementTarget, bool bMoveToSeparate /*= true*/, float flArrivalZoneSizeSq /*= -1.0f*/) +{ + if (flArrivalZoneSizeSq == -1.0f) + { + flArrivalZoneSizeSq = sv_neo_bot_cmdr_stop_distance_sq.GetFloat(); + } + Vector vBotRepulsion = vec3_origin; + Vector vWallRepulsion = vec3_origin; + Vector vFinalForce = vec3_origin; // Initialize to vec3_origin + bool bTooClose = false; + + if (gpGlobals->curtime > m_flNextFanOutLookCalcTime) + { + // Combined loop for player forces + for (int idx = 1; idx <= gpGlobals->maxClients; ++idx) + { + CBasePlayer* pPlayer = UTIL_PlayerByIndex(idx); + if (!pPlayer || pPlayer == me || !pPlayer->IsAlive()) + continue; + + // Only consider friendly players + if (pPlayer->GetTeamNumber() != me->GetTeamNumber()) + continue; + + CNEO_Player* pOther = static_cast(pPlayer); + if (!pOther) + continue; + + // Determine if we are too close to any friendly bot + if (me->GetAbsOrigin().DistToSqr(pOther->GetAbsOrigin()) < sv_neo_bot_cmdr_stop_distance_sq.GetFloat() / 2) + { + bTooClose = true; + } + + // Check for line of sight to other player for repulsion + trace_t tr; + UTIL_TraceLine(me->EyePosition(), pOther->EyePosition(), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &tr); + + if (tr.fraction == 1.0f || tr.m_pEnt == pOther) + { + Vector vToOther = pOther->GetAbsOrigin() - me->GetAbsOrigin(); + float flDistSqr = vToOther.LengthSqr(); + if (flDistSqr > 0.0f) + { + const float flMaxRepulsionDist = sv_neo_bot_cmdr_look_weights_friendly_max_dist.GetFloat(); + float flDist = FastSqrt(flDistSqr); + float flRepulsionScale = 1.0f - (flDist / flMaxRepulsionDist); + flRepulsionScale = Clamp(flRepulsionScale, 0.0f, 1.0f); + float flRepulsion = flRepulsionScale * flRepulsionScale; + + if (flRepulsion > 0.0f) + { + vBotRepulsion -= vToOther.Normalized() * flRepulsion; + } + } + } + } + + // Next look time + if (bTooClose) + { + m_flNextFanOutLookCalcTime = gpGlobals->curtime + 0.1; // 100 ms + } + else + { + float waitSec = RandomFloat(0.2f, 2.0); + m_flNextFanOutLookCalcTime = gpGlobals->curtime + waitSec; + } + + // Wall Repulsion + trace_t tr; + const int numWhiskers = 12; + for (int i = 0; i < numWhiskers; ++i) + { + QAngle ang = me->GetLocalAngles(); + ang.y += (360.0f / numWhiskers) * i; + Vector vWhiskerDir; + AngleVectors(ang, &vWhiskerDir); + float whiskerDist = sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist.GetFloat(); + vWhiskerDir.z = 0; // look at eye level for windows + UTIL_TraceLine(me->GetBodyInterface()->GetEyePosition(), me->GetBodyInterface()->GetEyePosition() + vWhiskerDir * whiskerDist, MASK_SHOT, me, COLLISION_GROUP_NONE, &tr); + if (tr.DidHit()) + { + float flRepulsion = 1.0f - (tr.fraction); + vWallRepulsion += tr.plane.normal * flRepulsion; + } + } + + // Combine forces and look at final direction + float friendlyRepulsionWeight = sv_neo_bot_cmdr_look_weights_friendly_repulsion.GetFloat(); + float wallRepulsionWeight = sv_neo_bot_cmdr_look_weights_wall_repulsion.GetFloat(); + vFinalForce = (vBotRepulsion * friendlyRepulsionWeight) + (vWallRepulsion * wallRepulsionWeight); + vFinalForce.z = 0; // avoid tilting awkwardly up or down + vFinalForce.NormalizeInPlace(); + if (!vFinalForce.IsZero()) + { + me->GetBodyInterface()->AimHeadTowards(me->GetBodyInterface()->GetEyePosition() + vFinalForce * 500.0f); + } + } + + // Fudge factor to reduce teammates running into each other trying to reach the same point + if (me->GetAbsOrigin().DistToSqr(movementTarget) < flArrivalZoneSizeSq) + { + if (bMoveToSeparate) + { + if (bTooClose) + { + me->PressForwardButton(0.1); + } + } + + movementTarget = me->GetAbsOrigin(); + m_path.Invalidate(); + return true; // Is already at destination + } + + // Still moving to destination, path will be recomputed by the calling context + return false; +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h new file mode 100644 index 000000000..49035ca12 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h @@ -0,0 +1,34 @@ +#ifndef NEO_BOT_COMMAND_FOLLOW_H +#define NEO_BOT_COMMAND_FOLLOW_H +#ifdef _WIN32 +#pragma once +#endif + +#include "NextBotBehavior.h" + +class CNEOBotCommandFollow : public Action< CNEOBot > +{ +public: + CNEOBotCommandFollow(); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; + virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) override; + + virtual const char *GetName( void ) const override { return "CommandFollow"; }; + +private: + bool FollowCommandChain( CNEOBot *me ); + bool FanOutAndCover( CNEOBot *me, Vector &movementTarget, bool bMoveToSeparate = true, float flArrivalZoneSizeSq = -1.0f ); + + PathFollower m_path; + CountdownTimer m_repathTimer; + + EHANDLE m_hTargetEntity; + bool m_bGoingToTargetEntity = false; + Vector m_vGoalPos; + float m_flNextFanOutLookCalcTime = 0.0f; +}; + +#endif // NEO_BOT_COMMAND_FOLLOW_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_pause.cpp b/src/game/server/neo/bot/behavior/neo_bot_pause.cpp new file mode 100644 index 000000000..10b288c80 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_pause.cpp @@ -0,0 +1,61 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_pause.h" +#include "bot/behavior/neo_bot_command_follow.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ConVar sv_neo_bot_cmdr_debug_pause_uncommanded; + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotPause::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + if ( sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool() ) + { + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); + me->StopLookingAroundForEnemies(); + } + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotPause::Update( CNEOBot* me, float interval ) +{ + // If the bot gets commanded, transition to CommandFollow immediately + if (me->m_hLeadingPlayer.Get() || me->m_hCommandingPlayer.Get()) + { + return ChangeTo(new CNEOBotCommandFollow, "Commanded while paused"); + } + + if (!sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool()) + { + return Done("Unpaused"); + } + else + { + if ( !me->HasAttribute( CNEOBot::IGNORE_ENEMIES ) ) + { + // Ensure these are set if cvar changed while in state + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); + me->StopLookingAroundForEnemies(); + } + } + + // Stop moving + if ( me->GetCurrentPath() ) + { + me->SetCurrentPath( NULL ); + } + + me->GetLocomotionInterface()->Reset(); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotPause::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) +{ + me->ClearAttribute( CNEOBot::IGNORE_ENEMIES ); + me->StartLookingAroundForEnemies(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_pause.h b/src/game/server/neo/bot/behavior/neo_bot_pause.h new file mode 100644 index 000000000..a372dde65 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_pause.h @@ -0,0 +1,19 @@ +#ifndef NEO_BOT_PAUSE_H +#define NEO_BOT_PAUSE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "bot/neo_bot.h" + +class CNEOBotPause : public Action< CNEOBot > +{ +public: + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot* me, float interval ) override; + virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) override; + + virtual const char* GetName( void ) const override { return "Pause"; }; +}; + +#endif // NEO_BOT_PAUSE_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp index d9bfb5570..8674eef5c 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp @@ -10,10 +10,12 @@ #include "bot/behavior/neo_bot_tactical_monitor.h" #include "bot/behavior/neo_bot_scenario_monitor.h" +#include "bot/behavior/neo_bot_command_follow.h" #include "bot/behavior/neo_bot_seek_and_destroy.h" #include "bot/behavior/neo_bot_seek_weapon.h" #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/behavior/neo_bot_retreat_from_grenade.h" +#include "bot/behavior/neo_bot_pause.h" #if 0 // NEO TODO (Adam) Fix picking up weapons, search for dropped weapons to pick up ammo #include "bot/behavior/neo_bot_get_ammo.h" #endif @@ -24,6 +26,8 @@ ConVar neo_bot_force_jump( "neo_bot_force_jump", "0", FCVAR_CHEAT, "Force bots to continuously jump" ); ConVar neo_bot_grenade_check_radius( "neo_bot_grenade_check_radius", "500", FCVAR_CHEAT ); +extern ConVar sv_neo_bot_cmdr_enable; +extern ConVar sv_neo_bot_cmdr_debug_pause_uncommanded; //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -275,6 +279,19 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter return result; } + if (sv_neo_bot_cmdr_enable.GetBool()) + { + if (me->m_hLeadingPlayer.Get() || me->m_hCommandingPlayer.Get()) + { + return SuspendFor(new CNEOBotCommandFollow, "Following commander"); + } + + if (sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool()) + { + return SuspendFor( new CNEOBotPause, "Paused by debug convar sv_neo_bot_cmdr_debug_pause_uncommanded" ); + } + } + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); // check if we need to get to cover diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index 2e24d3974..7cbfd896c 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -54,6 +54,7 @@ SendPropInt(SENDINFO(m_iLoadoutWepChoice)), SendPropInt(SENDINFO(m_iNextSpawnClassChoice)), SendPropInt(SENDINFO(m_bInLean)), SendPropEHandle(SENDINFO(m_hServerRagdoll)), +SendPropEHandle(SENDINFO(m_hCommandingPlayer)), SendPropBool(SENDINFO(m_bInThermOpticCamo)), SendPropBool(SENDINFO(m_bLastTickInThermOpticCamo)), @@ -75,6 +76,7 @@ SendPropString(SENDINFO(m_pszTestMessage)), SendPropArray(SendPropInt(SENDINFO_ARRAY(m_rfAttackersScores)), m_rfAttackersScores), SendPropArray(SendPropFloat(SENDINFO_ARRAY(m_rfAttackersAccumlator), -1, SPROP_COORD_MP_LOWPRECISION | SPROP_CHANGES_OFTEN, MIN_COORD_FLOAT, MAX_COORD_FLOAT), m_rfAttackersAccumlator), SendPropArray(SendPropInt(SENDINFO_ARRAY(m_rfAttackersHits)), m_rfAttackersHits), +SendPropArray(SendPropVector(SENDINFO_ARRAY(m_vLastPingByStar), -1, SPROP_COORD), m_vLastPingByStar), SendPropInt(SENDINFO(m_NeoFlags), 4, SPROP_UNSIGNED), SendPropString(SENDINFO(m_szNeoName)), @@ -138,10 +140,13 @@ END_SCRIPTDESC(); static constexpr int SHOWMENU_STRLIMIT = 512; +const Vector CNEO_Player::VECTOR_INVALID_WAYPOINT = vec3_invalid; + CBaseEntity *g_pLastJinraiSpawn, *g_pLastNSFSpawn; CNEOGameRulesProxy* neoGameRules; extern CBaseEntity *g_pLastSpawn; +extern ConVar sv_neo_bot_cmdr_enable; extern ConVar sv_neo_ignore_wep_xp_limit; extern ConVar sv_neo_clantag_allow; extern ConVar sv_neo_dev_test_clantag; @@ -504,6 +509,7 @@ CNEO_Player::CNEO_Player() m_szNameDupePos = 0; m_flNextPingTime = 0; + ResetBotCommandState(); } CNEO_Player::~CNEO_Player( void ) @@ -569,7 +575,6 @@ void CNEO_Player::Spawn(void) m_bIsPendingSpawnForThisRound = false; m_bLastTickInThermOpticCamo = m_bInThermOpticCamo = false; - m_iBotDetectableBleedingInjuryEvents = 0; m_flCamoAuxLastTime = 0; m_bInVision = false; @@ -630,6 +635,9 @@ void CNEO_Player::Spawn(void) m_iLoadoutWepChoice = NEORules()->GetForcedWeapon(); respawn(this, false); } + + ResetBotCommandState(); + m_iBotDetectableBleedingInjuryEvents = 0; } extern ConVar neo_lean_angle; @@ -2037,6 +2045,24 @@ void CNEO_Player::Event_Killed( const CTakeDamageInfo &info ) SetDeadModel(info); } + if (sv_neo_bot_cmdr_enable.GetBool()) + { + // If teamkilled by commander, other subordinates stop following commander + CNEO_Player *pAttacker = ToNEOPlayer(info.GetAttacker()); + if (pAttacker && m_hCommandingPlayer.Get() == pAttacker) + { + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + CNEO_Player *pPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); + if (pPlayer && pPlayer->m_hCommandingPlayer.Get() == pAttacker) + { + pPlayer->m_hCommandingPlayer.Set(NULL); + } + } + } + } + ResetBotCommandState(); + SpectatorTakeoverPlayerRevert(false); // soft reset: may still have live impostor } @@ -3181,6 +3207,120 @@ void CNEO_Player::SetTestMessageVisible(bool visible) m_bShowTestMessage = visible; } +void CNEO_Player::ResetBotCommandState() +{ + if (sv_neo_bot_cmdr_enable.GetBool()) + { + m_hLeadingPlayer = nullptr; + m_hCommandingPlayer = nullptr; + m_tBotPlayerPingCooldown.Invalidate(); + m_flBotDynamicFollowDistanceSq = 0.0f; + for (int i = 0; i < STAR__TOTAL; ++i) + { + m_vLastPingByStar.GetForModify(i) = VECTOR_INVALID_WAYPOINT; + } + } +} + +void CNEO_Player::ToggleBotFollowCommander(CNEO_Player* pCommander) +{ + if (!sv_neo_bot_cmdr_enable.GetBool()) + { + return; + } + + if (!pCommander) + { + DevWarning("ToggleBotFollowCommander called without valid player handle!\n"); + return; + } + + if (!NEORules()->IsTeamplay()) + { + // Do not allow bot commanding in free for all game modes + return; + } + + if (pCommander->GetTeamNumber() != GetTeamNumber()) + { + // Commander is not on the same team, do not allow toggling follow state. + return; + } + + if (m_hLeadingPlayer.Get() == pCommander) + { + // If already following, check if only star needs updating + if (!pCommander->IsBot() && pCommander->GetStar() != GetStar()) + { + // Commander is a player and stars are different, just update bot's star + RequestSetStar(pCommander->GetStar()); + + // Behavior without resetting pings/commander/leader: + // If this is a new squad star with no waypoint this round, bots will leave waypoint to come follow. + // If this is an existing squad star with an active waypoint, bots will return to that position. + // Might allow for complicated strategems or might be confusing. + // e.g. set a fallback position for a squad star and remote use bots to send them there. + } + else + { + // Bot is already following this player in same star, so toggle off. + // Bot will return to following general uncommanded bot behavior. + m_hLeadingPlayer = nullptr; + m_hCommandingPlayer = nullptr; + } + } + // Without other checks, players can steal others' bots. + else + { + // Bot starts following this player. + m_hLeadingPlayer = pCommander; + if (!pCommander->IsBot()) + { + m_hCommandingPlayer = pCommander; + + // If commander is a player and stars are different, update bot's star + if (pCommander->GetStar() != GetStar()) + { + RequestSetStar(pCommander->GetStar()); + } + } + } +} + +void CNEO_Player::PlayerUse( void ) +{ + BaseClass::PlayerUse(); + + if (!sv_neo_bot_cmdr_enable.GetBool()) + { + return; + } + + if ( (m_afButtonPressed & IN_USE) && !FindUseEntity() ) + { + // Select bot under cursor to follow/unfollow. + Vector eyePos = EyePosition(); + Vector forward; + EyeVectors( &forward ); + Vector traceEnd = eyePos + forward * MAX_COORD_RANGE; + + trace_t tr; + // MASK_SHOT_HULL to match friendly fire warning trace + UTIL_TraceLine( eyePos, traceEnd, MASK_SHOT_HULL, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.DidHit() && tr.m_pEnt ) + { + CNEO_Player* pTargetPlayer = ToNEOPlayer(tr.m_pEnt); + if ( pTargetPlayer && pTargetPlayer->IsBot()) + { + // The hit entity is a bot! Now, toggle its follow state. + pTargetPlayer->ToggleBotFollowCommander( this ); + // TODO: Do we want to allow using players for some kind of communication? + } + } + } +} + void CNEO_Player::StartAutoSprint(void) { BaseClass::StartAutoSprint(); diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 5fc476305..d963989fa 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -67,6 +67,7 @@ class CNEO_Player : public CHL2MP_Player virtual void CalculateSpeed(void); virtual void PreThink(void) OVERRIDE; virtual void PlayerDeathThink(void) OVERRIDE; + virtual void PlayerUse(void) OVERRIDE; virtual bool HandleCommand_JoinTeam(int team) OVERRIDE; virtual bool ClientCommand(const CCommand &args) OVERRIDE; virtual void CreateViewModel(int viewmodelindex = 0) OVERRIDE; @@ -308,6 +309,15 @@ class CNEO_Player : public CHL2MP_Player // Bot-only usage float m_flRanOutSprintTime = 0.0f; + CNetworkHandle(CNEO_Player, m_hCommandingPlayer); // The player this bot is commanded by + CHandle m_hLeadingPlayer; // The player this bot is following + CountdownTimer m_tBotPlayerPingCooldown; // The cooldown time for following player ping + float m_flBotDynamicFollowDistanceSq; // The dynamic follow distance interval for bots + CNetworkArray(Vector, m_vLastPingByStar, STAR__TOTAL); // The last ping location from this player for each squad star + // Bot Functions + void ResetBotCommandState(); + void ToggleBotFollowCommander( CNEO_Player *pCommander ); + static const Vector VECTOR_INVALID_WAYPOINT; private: bool m_bFirstDeathTick; diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 60363e8d4..4a69c01e6 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -19,6 +19,10 @@ #include "neo_player.h" #endif +#ifdef GAME_DLL +#include "basetypes.h" +#endif + #include "convar.h" #include "neo_weapon_loadout.h" @@ -157,9 +161,45 @@ void CheckPingButton(CNEO_Player* player) { player->m_flNextPingTime = gpGlobals->curtime; } + + UpdatePingCommands(player, tr.endpos); } } +ConVar sv_neo_bot_cmdr_enable("sv_neo_bot_cmdr_enable", "0", + FCVAR_REPLICATED | FCVAR_ARCHIVE, "Allow bots to follow you after you press use on them", true, 0, true, 1); + +#ifdef GAME_DLL +static ConVar sv_neo_bot_cmdr_stop_distance_sq_max("sv_neo_bot_cmdr_stop_distance_sq_max", "50000", + FCVAR_CHEAT, "Maximum distance bot following gap interval can be set by player pings", true, 5000, true, 500000); +static ConVar sv_neo_bot_cmdr_ping_ignore_delay_min_sec("sv_neo_bot_cmdr_ping_ignore_delay_min_sec", "3", + FCVAR_CHEAT, "Minimum time bots ignore pings for new waypoint settings", true, 0, true, 1000); +#endif // GAME_DLL + +void UpdatePingCommands(CNEO_Player* player, const Vector& pingPos) +{ +#ifdef GAME_DLL + if (sv_neo_bot_cmdr_enable.GetBool()) + { + player->m_vLastPingByStar.GetForModify(player->GetStar()) = pingPos; + float distSqrToPing = player->GetAbsOrigin().DistToSqr(pingPos); + if (distSqrToPing < sv_neo_bot_cmdr_stop_distance_sq_max.GetFloat()) + { + // If pinging close to self, calibrate follow distance of commanded bots based on distance to ping + float minFollowDistanceSq = 0.0f; + sv_neo_bot_cmdr_stop_distance_sq_max.GetMin(minFollowDistanceSq); + player->m_flBotDynamicFollowDistanceSq = Clamp(distSqrToPing, minFollowDistanceSq, sv_neo_bot_cmdr_stop_distance_sq_max.GetFloat()); + } + else + { + player->m_flBotDynamicFollowDistanceSq = 0.0f; + } + + player->m_tBotPlayerPingCooldown.Start(sv_neo_bot_cmdr_ping_ignore_delay_min_sec.GetFloat()); + } +#endif +} + void KillerLineStr(char* killByLine, const int killByLineMax, CNEO_Player* neoAttacker, const CNEO_Player* neoVictim, const char* killedWith) { diff --git a/src/game/shared/neo/neo_player_shared.h b/src/game/shared/neo/neo_player_shared.h index cbbc1d5e9..c7991900c 100644 --- a/src/game/shared/neo/neo_player_shared.h +++ b/src/game/shared/neo/neo_player_shared.h @@ -253,6 +253,7 @@ enum NeoWeponAimToggleE { }; void CheckPingButton(CNEO_Player* player); +void UpdatePingCommands(CNEO_Player* player, const Vector& pingPos); struct AttackersTotals {