diff --git a/.gitignore b/.gitignore index ee09bd1a5c..53c3508549 100644 --- a/.gitignore +++ b/.gitignore @@ -694,3 +694,7 @@ FodyWeavers.xsd # Selected Background /game/neo/scripts/[Cc]hapter[Bb]ackgrounds.txt + +# clion stuff +/src/cmake-build-debug/ +/.idea/ \ No newline at end of file diff --git a/game/bin/rebuild.fgd b/game/bin/rebuild.fgd index 19d69818fa..9c2892b0a5 100644 --- a/game/bin/rebuild.fgd +++ b/game/bin/rebuild.fgd @@ -42,7 +42,8 @@ 6 : "JGR" 4 : "Empty" 5 : "Tutorial" - ] + 7 : "KOTH" + ] ForcedTeam(choices) : "Forced Team" : -1 : "Skip team menu and spawn as this team" = [ @@ -461,3 +462,21 @@ [ radius(float) : "Radius" : 12 : "" ] + +@SolidClass base(Trigger) = neo_trigger_koth_zone: "Trigger for zone capture detection. Can be grouped with other triggers into a single zone." +[ + zone_name(target_destination) : "Zone name" : "" : "Name of the neo_info_koth_zone this trigger belongs to." +] + +@PointClass base(Targetname) iconsprite("vgui/hud/cp/hammer_koth_info.vmt") = neo_info_koth_zone : "Creates a zone to capture. Give it a name, then point one or more neo_trigger_koth_zone's zone_name at that name." +[ +] + +@PointClass iconsprite("vgui/hud/cp/hammer_koth_master.vmt") = neo_koth_master : "Manages all neo_info_koth_zone entities on the map: picks which one is active and rotates between them. Place exactly one per map." +[ +] + +@SolidClass base(Targetname) = neo_func_koth_border : "Visual border brush for a KOTH zone. Solidity, +use, shadows and color are all forced by default - not editable here." +[ + zone_name(target_destination) : "Zone name" : "" : "Name of the neo_info_koth_zone this border belongs to." +] \ No newline at end of file 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 1274ee4f2c..e2220e38b8 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -321,6 +321,9 @@ void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw() case NEO_GAME_TYPE_JGR: m_pWszStatusUnicode = L"Control the Juggernaut\n"; break; + case NEO_GAME_TYPE_KOTH: + m_pWszStatusUnicode = L"Capture the objective!\n"; + break; default: m_pWszStatusUnicode = L"Await further orders\n"; break; @@ -365,6 +368,10 @@ void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw() { m_iWszRoundUCSize = V_swprintf_safe(m_wszRoundUnicode, L"DEATHMATCH"); } + else if (NEORules()->GetGameType() == NEO_GAME_TYPE_KOTH) + { + m_iWszRoundUCSize = V_swprintf_safe(m_wszRoundUnicode, L"FIRST TO %i WINS", sv_neo_koth_max_score.GetInt()); + } else { m_iWszRoundUCSize = V_swprintf_safe(m_wszRoundUnicode, L"ROUND %i", NEORules()->roundNumber()); @@ -393,13 +400,21 @@ void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw() const int localPlayerTeam = GetLocalPlayerTeam(); if (NEORules()->IsTeamplay()) { - if (cl_neo_hud_team_swap_sides.GetBool() && (localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF)) { - V_snwprintf(m_wszLeftTeamScore, 3, L"%i", GetGlobalTeam(localPlayerTeam)->GetRoundsWon()); - V_snwprintf(m_wszRightTeamScore, 3, L"%i", GetGlobalTeam(NEORules()->GetOpposingTeam(localPlayerTeam))->GetRoundsWon()); + const bool bSwapToLocal = cl_neo_hud_team_swap_sides.GetBool() && (localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF); + const int leftScoreTeam = bSwapToLocal ? localPlayerTeam : TEAM_JINRAI; + const int rightScoreTeam = bSwapToLocal ? NEORules()->GetOpposingTeam(localPlayerTeam) : TEAM_NSF; + + if (NEORules()->GetGameType() == NEO_GAME_TYPE_KOTH) + { + const int leftScore = (leftScoreTeam == TEAM_JINRAI) ? NEORules()->GetKothTimeJinrai() : NEORules()->GetKothTimeNSF(); + const int rightScore = (rightScoreTeam == TEAM_JINRAI) ? NEORules()->GetKothTimeJinrai() : NEORules()->GetKothTimeNSF(); + V_snwprintf(m_wszLeftTeamScore, ARRAYSIZE(m_wszLeftTeamScore), L"%i", leftScore); + V_snwprintf(m_wszRightTeamScore, ARRAYSIZE(m_wszRightTeamScore), L"%i", rightScore); } - else { - V_snwprintf(m_wszLeftTeamScore, 3, L"%i", GetGlobalTeam(TEAM_JINRAI)->GetRoundsWon()); - V_snwprintf(m_wszRightTeamScore, 3, L"%i", GetGlobalTeam(TEAM_NSF)->GetRoundsWon()); + else + { + V_snwprintf(m_wszLeftTeamScore, ARRAYSIZE(m_wszLeftTeamScore), L"%i", GetGlobalTeam(leftScoreTeam)->GetRoundsWon()); + V_snwprintf(m_wszRightTeamScore, ARRAYSIZE(m_wszRightTeamScore), L"%i", GetGlobalTeam(rightScoreTeam)->GetRoundsWon()); } } else @@ -430,6 +445,13 @@ void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw() V_sprintf_safe(szPlayersAliveANSI, "%i:%i", GetGlobalTeam(TEAM_JINRAI)->Get_Score(), GetGlobalTeam(TEAM_NSF)->Get_Score()); } } + // neo TODO: add KOTH gameType (wip) or detect KOTH maps (removed this feature) +// else if (NEORules()->GetGameType() == NEO_GAME_TYPE_KOTH) +// { +// V_sprintf_safe(szPlayersAliveANSI, "J:%d N:%d", +// NEORules()->GetKothTimeJinrai(), +// NEORules()->GetKothTimeNSF()); +// } else { V_sprintf_safe(szPlayersAliveANSI, "%i vs %i", m_iLeftPlayersAlive, m_iRightPlayersAlive); @@ -547,12 +569,13 @@ void CNEOHud_RoundState::DrawNeoHudElement() surface()->DrawSetTextFont(m_hOCRLargeFont); surface()->DrawSetTextPos(m_posLeftTeamScore.x - (fontWidth / 2), m_posLeftTeamScore.y); surface()->DrawSetTextColor(leftTeamInfo.color); - surface()->DrawPrintText(m_wszLeftTeamScore, 2); + surface()->DrawPrintText(m_wszLeftTeamScore, V_wcslen(m_wszLeftTeamScore)); surface()->GetTextSize(m_hOCRLargeFont, m_wszRightTeamScore, fontWidth, fontHeight); + surface()->DrawSetTextFont(m_hOCRLargeFont); surface()->DrawSetTextPos(m_posRightTeamScore.x - (fontWidth / 2), m_posRightTeamScore.y); surface()->DrawSetTextColor(rightTeamInfo.color); - surface()->DrawPrintText(m_wszRightTeamScore, 2); + surface()->DrawPrintText(m_wszRightTeamScore, V_wcslen(m_wszRightTeamScore)); } m_iLeftPlayersAlive = m_iLeftPlayersTotal = m_iRightPlayersAlive = m_iRightPlayersTotal = 0; 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 adcd056014..e033f8bd7f 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.h +++ b/src/game/client/neo/ui/neo_hud_round_state.h @@ -78,11 +78,11 @@ class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, publ int m_iSmallFontHeight = 0; // Center Box info - wchar_t m_wszRoundUnicode[12] = {}; + wchar_t m_wszRoundUnicode[32] = {}; int m_iWszRoundUCSize = 0; wchar_t m_wszTime[6] = {}; - wchar_t m_wszLeftTeamScore[3] = {}; - wchar_t m_wszRightTeamScore[3] = {}; + wchar_t m_wszLeftTeamScore[12] = {}; + wchar_t m_wszRightTeamScore[12] = {}; wchar_t m_wszPlayersAliveUnicode[16] = {}; const wchar_t *m_pWszStatusUnicode = nullptr; int m_iStatusUnicodeSize = 0; diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 4866f13b63..c926ee89a9 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1498,6 +1498,14 @@ target_sources_grouped( neo/neo_te_tocflash.h neo/neo_tracefilter_collisiongroupdelta.h neo/neo_dm_spawn.h + neo/neo_trigger_koth_zone.h + neo/neo_info_koth_zone.h + neo/neo_koth_master.h + neo/neo_koth_border.h + neo/neo_trigger_koth_zone.cpp + neo/neo_info_koth_zone.cpp + neo/neo_koth_master.cpp + neo/neo_koth_border.cpp neo/neo_gamerules_restore.h ${UNITY_SOURCE_NEO_SRC_SERVER} ) diff --git a/src/game/server/neo/neo_info_koth_zone.cpp b/src/game/server/neo/neo_info_koth_zone.cpp new file mode 100644 index 0000000000..4623bf5791 --- /dev/null +++ b/src/game/server/neo/neo_info_koth_zone.cpp @@ -0,0 +1,320 @@ +#include "neo_info_koth_zone.h" +#include "neo_trigger_koth_zone.h" +#include "neo_koth_master.h" +#include "neo_koth_border.h" +#include "neo_player.h" +#include "neo_player_shared.h" + +#define KOTH_PRUNE_INTERVAL 0.5f +#define KOTH_SCORE_CONTEXT "KothZoneScoreThink" +#define KOTH_SCORE_INTERVAL 0.05f + +#define KOTH_SPRITE_NONE "vgui/hud/cp/cp_none.vmt" +#define KOTH_SPRITE_JINRAI "vgui/hud/cp/cp_jinrai.vmt" +#define KOTH_SPRITE_NSF "vgui/hud/cp/cp_nsf.vmt" +#define KOTH_SPRITE_BOTH "vgui/hud/cp/cp_both.vmt" +#define KOTH_SPRITE_LOCKED "vgui/hud/cp/cp_locked.vmt" + +LINK_ENTITY_TO_CLASS(neo_info_koth_zone, CNEO_InfoKOTHZone); + +static const char *KothZoneSpriteForState(KothControllingTeams team) +{ + switch (team) + { + case KOTH_JINRAI: + return KOTH_SPRITE_JINRAI; + case KOTH_NSF: + return KOTH_SPRITE_NSF; + case KOTH_BOTH: + return KOTH_SPRITE_BOTH; + case KOTH_NONE: + default: + return KOTH_SPRITE_NONE; + } +} + +BEGIN_DATADESC(CNEO_InfoKOTHZone) + DEFINE_THINKFUNC(Think), + DEFINE_THINKFUNC(ScoreThink), +END_DATADESC() + +void CNEO_InfoKOTHZone::Spawn() +{ + SetModelName(MAKE_STRING(KothZoneSpriteForState(m_State))); + + BaseClass::Spawn(); + + SetSpriteScale(0.5f); + + SetThink(&CNEO_InfoKOTHZone::Think); + SetNextThink(gpGlobals->curtime + KOTH_PRUNE_INTERVAL); + + SetContextThink(&CNEO_InfoKOTHZone::ScoreThink, gpGlobals->curtime + KOTH_SCORE_INTERVAL, KOTH_SCORE_CONTEXT); +} + +void CNEO_InfoKOTHZone::Precache() +{ + BaseClass::Precache(); + + PrecacheModel(KOTH_SPRITE_NONE); + PrecacheModel(KOTH_SPRITE_JINRAI); + PrecacheModel(KOTH_SPRITE_NSF); + PrecacheModel(KOTH_SPRITE_LOCKED); + PrecacheModel(KOTH_SPRITE_BOTH); +} + +void CNEO_InfoKOTHZone::Activate() +{ + BaseClass::Activate(); + + CNEO_KOTHMaster *pMaster = static_cast( // NOLINT + gEntList.FindEntityByClassname(nullptr, "neo_koth_master") + ); + + // neo TODO: do something when we see that there are several masters + if (pMaster == nullptr) + Warning("neo_info_koth_zone found with no neo_koth_master on the map!\n"); + else + pMaster->RegisterZone(this); +} + +void CNEO_InfoKOTHZone::Think() +{ + PruneStaleCaptors(); + SetNextThink(gpGlobals->curtime + KOTH_PRUNE_INTERVAL); +} + +void CNEO_InfoKOTHZone::ScoreThink() +{ + if (!NEORules()->IsRoundLive()) + { + SetNextThink(gpGlobals->curtime + KOTH_SCORE_INTERVAL, KOTH_SCORE_CONTEXT); + return; + } + + switch (m_State) + { + case KOTH_JINRAI: + m_flAccumulatorJinrai += KOTH_SCORE_INTERVAL; + m_flAccumulatorNSF = 0.0f; + break; + case KOTH_NSF: + m_flAccumulatorNSF += KOTH_SCORE_INTERVAL; + m_flAccumulatorJinrai = 0.0f; + break; + default: + // KOTH_NONE (empty) or KOTH_BOTH (contested) - nobody accumulates + m_flAccumulatorNSF = 0.0f; + m_flAccumulatorJinrai = 0.0f; + break; + } + + const float flSecondsPerPoint = sv_neo_koth_seconds_per_point.GetFloat(); + if (m_flAccumulatorJinrai >= flSecondsPerPoint) + { + const int points = int(m_flAccumulatorJinrai / flSecondsPerPoint); + m_flAccumulatorJinrai -= flSecondsPerPoint * points; + NEORules()->AddKothScore(TEAM_JINRAI, points); + } + if (m_flAccumulatorNSF >= flSecondsPerPoint) + { + const int points = int(m_flAccumulatorNSF / flSecondsPerPoint); + m_flAccumulatorNSF -= flSecondsPerPoint * points; + NEORules()->AddKothScore(TEAM_NSF, points); + } + + SetNextThink(gpGlobals->curtime + KOTH_SCORE_INTERVAL, KOTH_SCORE_CONTEXT); +} + +void CNEO_InfoKOTHZone::OnPlayerEnter(CNEO_Player *pPlayer) +{ + if (!pPlayer) + return; + + EHANDLE hPlayer = pPlayer; + for (int i = 0; i < m_Captors.Count(); ++i) + { + if (m_Captors[i].hPlayer == hPlayer) + { + // already standing in another trigger of this same zone + m_Captors[i].touchCount++; + return; + } + } + + ZoneCaptor captor; + captor.hPlayer = hPlayer; + // if they're somehow dead on entry, don't credit either team + captor.team = pPlayer->IsAlive() ? pPlayer->GetTeamNumber() : TEAM_UNASSIGNED; + captor.touchCount = 1; + m_Captors.AddToTail(captor); + + if (captor.team == TEAM_JINRAI) + m_iJinraiCount++; + else if (captor.team == TEAM_NSF) + m_iNSFCount++; + + UpdateState(); +} + +void CNEO_InfoKOTHZone::OnPlayerLeave(CNEO_Player *pPlayer) +{ + if (!pPlayer) + return; + + EHANDLE hPlayer = pPlayer; + for (int i = 0; i < m_Captors.Count(); ++i) + { + if (m_Captors[i].hPlayer != hPlayer) + continue; + + // still touching another trigger belonging to this zone + if (--m_Captors[i].touchCount > 0) + return; + + if (m_Captors[i].team == TEAM_JINRAI) + m_iJinraiCount--; + else if (m_Captors[i].team == TEAM_NSF) + m_iNSFCount--; + + m_Captors.Remove(i); + UpdateState(); + return; + } +} + +void CNEO_InfoKOTHZone::AddChildTrigger(CNEO_TriggerKOTHZone *pTrigger) +{ + if (!pTrigger) + return; + + // avoid double activation + if (m_ChildTriggers.Find(pTrigger) != m_ChildTriggers.InvalidIndex()) + return; + + m_ChildTriggers.AddToTail(pTrigger); +} + +void CNEO_InfoKOTHZone::AddChildBorder(CNEO_KOTHBorder *pBorder) +{ + if (!pBorder) + return; + + // avoid double activation + if (m_ChildBorders.Find(pBorder) != m_ChildBorders.InvalidIndex()) + return; + + m_ChildBorders.AddToTail(pBorder); + pBorder->SetZoneColor(m_State); +} + +void CNEO_InfoKOTHZone::SetVisible(bool bVisible) +{ + if (bVisible == m_bVisible) + return; + + m_bVisible = bVisible; + + if (bVisible) + { + SetModel(KOTH_SPRITE_LOCKED); + TurnOn(); + } + else + { + TurnOff(); + } + + for (int i = 0; i < m_ChildBorders.Count(); ++i) + { + CNEO_KOTHBorder *pBorder = m_ChildBorders[i].Get(); + if (pBorder) + pBorder->SetBorderVisible(bVisible); + } +} + +void CNEO_InfoKOTHZone::SetCapturable(bool bCapturable) +{ + if (bCapturable == m_bCapturable) + return; + + m_bCapturable = bCapturable; + + if (bCapturable) + SetModel(KothZoneSpriteForState(m_State)); + + for (int i = 0; i < m_ChildTriggers.Count(); ++i) + { + CNEO_TriggerKOTHZone *pTrigger = m_ChildTriggers[i].Get(); + if (!pTrigger) + continue; + + if (bCapturable) + pTrigger->Enable(); + else + pTrigger->Disable(); + } +} + +void CNEO_InfoKOTHZone::ResetCapture() +{ + m_Captors.Purge(); + m_iJinraiCount = 0; + m_iNSFCount = 0; + // neo TODO: it could lead to an incorrect result if players somehow spawn right inside the trigger, but who cares? + m_State = KOTH_NONE; + m_flAccumulatorNSF = 0.0f; + m_flAccumulatorJinrai = 0.0f; +} + +void CNEO_InfoKOTHZone::PruneStaleCaptors() +{ + bool bChanged = false; + + // EndTouch won't reliably fire for players who disconnected or died without + // physically leaving the trigger volume (e.g. respawning elsewhere) - catch those here + for (int i = m_Captors.Count() - 1; i >= 0; --i) + { + CNEO_Player *pPlayer = static_cast(m_Captors[i].hPlayer.Get()); + if (pPlayer && pPlayer->IsAlive()) + continue; + + if (m_Captors[i].team == TEAM_JINRAI) + m_iJinraiCount--; + else if (m_Captors[i].team == TEAM_NSF) + m_iNSFCount--; + + m_Captors.Remove(i); + bChanged = true; + } + + if (bChanged) + UpdateState(); +} + +void CNEO_InfoKOTHZone::UpdateState() +{ + KothControllingTeams newState; + if (m_iJinraiCount > 0 && m_iNSFCount > 0) + newState = KOTH_BOTH; + else if (m_iJinraiCount > 0) + newState = KOTH_JINRAI; + else if (m_iNSFCount > 0) + newState = KOTH_NSF; + else + newState = KOTH_NONE; + + if (newState == m_State) + return; + + for (int i = 0; i < m_ChildBorders.Count(); ++i) + { + CNEO_KOTHBorder *pBorder = m_ChildBorders[i].Get(); + if (pBorder) + pBorder->SetZoneColor(newState); + } + + SetModel(KothZoneSpriteForState(newState)); + + m_State = newState; +} diff --git a/src/game/server/neo/neo_info_koth_zone.h b/src/game/server/neo/neo_info_koth_zone.h new file mode 100644 index 0000000000..9a9f584bfe --- /dev/null +++ b/src/game/server/neo/neo_info_koth_zone.h @@ -0,0 +1,70 @@ +#pragma once +#include "cbase.h" +#include "utlvector.h" +#include "Sprite.h" +#include "neo_gamerules.h" + +class CNEO_Player; +class CNEO_TriggerKOTHZone; +class CNEO_KOTHBorder; + +class CNEO_InfoKOTHZone : public CSprite +{ +public: + DECLARE_CLASS(CNEO_InfoKOTHZone, CSprite); + DECLARE_DATADESC(); + + virtual void Spawn() override; + virtual void Precache() override; + virtual void Activate() override; + virtual void Think() override; + void ScoreThink(); + + // called by every neo_trigger_koth_zone referring to this zone + void OnPlayerEnter(CNEO_Player *pPlayer); + void OnPlayerLeave(CNEO_Player *pPlayer); + + // both are called by a neo_trigger_koth_zone once it has resolved us as parent + void AddChildTrigger(CNEO_TriggerKOTHZone *pTrigger); + void AddChildBorder(CNEO_KOTHBorder *pBorder); + + // both are called by neo_koth_master: show/hide ONLY the our sprite + child borders + void SetVisible(bool bVisible); + bool IsVisible() const { return m_bVisible; } + + // both are called by neo_koth_master: enable/disable ONLY the child triggers + void SetCapturable(bool bCapturable); + bool IsCapturable() const { return m_bCapturable; } + + // clears captors/score state (round restart) without touching visibility/capturable state + void ResetCapture(); + + KothControllingTeams GetState() const { return m_State; } + +private: + void UpdateState(); + // safety net: drops captors that died/disconnected without a matching EndTouch + void PruneStaleCaptors(); + + struct ZoneCaptor + { + EHANDLE hPlayer; + int team{}; // team recorded at the moment they entered (survives team switch/death before leaving) + int touchCount{}; // how many of this zone's triggers currently touch them + }; + + CUtlVector m_Captors; + int m_iJinraiCount = 0; + int m_iNSFCount = 0; + KothControllingTeams m_State = KOTH_NONE; + + CUtlVector> m_ChildTriggers; + CUtlVector> m_ChildBorders; + bool m_bVisible = false; + bool m_bCapturable = false; + + // score accumulation - runs on its own think context (see ScoreThink), independent + // of PruneStaleCaptors' slower one, since seconds-held needs to be tracked continuously + float m_flAccumulatorNSF = 0.0f; + float m_flAccumulatorJinrai = 0.0f; +}; diff --git a/src/game/server/neo/neo_koth_border.cpp b/src/game/server/neo/neo_koth_border.cpp new file mode 100644 index 0000000000..30b3ba43e3 --- /dev/null +++ b/src/game/server/neo/neo_koth_border.cpp @@ -0,0 +1,66 @@ +#include "neo_koth_border.h" +#include "neo_info_koth_zone.h" + +LINK_ENTITY_TO_CLASS(neo_func_koth_border, CNEO_KOTHBorder); + +BEGIN_DATADESC(CNEO_KOTHBorder) + DEFINE_KEYFIELD(m_iszZoneName, FIELD_STRING, "zone_name"), +END_DATADESC() + +void CNEO_KOTHBorder::Spawn() +{ + // some obvious settings + AddSpawnFlags(SF_IGNORE_PLAYERUSE); + m_iSolidity = BRUSHSOLID_NEVER; + + BaseClass::Spawn(); + + // starts hidden - matches its zone's m_bActive starting false, SetBorderVisible() handles the rest + AddEffects(EF_NOSHADOW | EF_NODRAW); +} + +void CNEO_KOTHBorder::Activate() +{ + BaseClass::Activate(); + + pZone = dynamic_cast( + gEntList.FindEntityByName(nullptr, STRING(m_iszZoneName)) + ); + + if (pZone == nullptr) + Warning("Some neo_func_koth_border was not connected to any neo_info_koth_zone entity!\n"); + else + pZone->AddChildBorder(this); +} + +void CNEO_KOTHBorder::SetZoneColor(KothControllingTeams team) +{ + switch (team) + { + case KOTH_JINRAI: + SetRenderColor(COLOR_JINRAI.r(), COLOR_JINRAI.g(), COLOR_JINRAI.b()); + DevMsg("JINRAI\n"); + break; + case KOTH_NSF: + SetRenderColor(64, 64, 255); + DevMsg("NSF\n"); + break; + case KOTH_BOTH: + SetRenderColor(COLOR_RED.r(), COLOR_RED.g(), COLOR_RED.b()); + DevMsg("STALEMATE\n"); + break; + case KOTH_NONE: + default: + SetRenderColor(COLOR_WHITE.r(), COLOR_WHITE.g(), COLOR_WHITE.b()); + DevMsg("EMPTY\n"); + break; + } +} + +void CNEO_KOTHBorder::SetBorderVisible(bool bVisible) +{ + if (bVisible) + RemoveEffects(EF_NODRAW); + else + AddEffects(EF_NODRAW); +} diff --git a/src/game/server/neo/neo_koth_border.h b/src/game/server/neo/neo_koth_border.h new file mode 100644 index 0000000000..838c797d98 --- /dev/null +++ b/src/game/server/neo/neo_koth_border.h @@ -0,0 +1,24 @@ +#pragma once +#include "cbase.h" +#include "modelentities.h" +#include "neo_gamerules.h" + +class CNEO_InfoKOTHZone; + +class CNEO_KOTHBorder : public CFuncBrush +{ +public: + DECLARE_CLASS(CNEO_KOTHBorder, CFuncBrush); + DECLARE_DATADESC(); + + virtual void Spawn() override; + virtual void Activate() override; + + // called by neo_info_koth_zone + void SetZoneColor(KothControllingTeams team); + void SetBorderVisible(bool bVisible); + + string_t m_iszZoneName; +private: + CNEO_InfoKOTHZone *pZone = nullptr; +}; diff --git a/src/game/server/neo/neo_koth_master.cpp b/src/game/server/neo/neo_koth_master.cpp new file mode 100644 index 0000000000..249b1ec6a1 --- /dev/null +++ b/src/game/server/neo/neo_koth_master.cpp @@ -0,0 +1,137 @@ +#include "neo_koth_master.h" +#include "neo_info_koth_zone.h" + +LINK_ENTITY_TO_CLASS(neo_koth_master, CNEO_KOTHMaster); + +BEGIN_DATADESC(CNEO_KOTHMaster) + DEFINE_THINKFUNC(RevealNextZoneThink), + DEFINE_THINKFUNC(UnlockZoneThink), + DEFINE_THINKFUNC(CloseZoneThink), +END_DATADESC() + +void CNEO_KOTHMaster::Spawn() +{ + BaseClass::Spawn(); + + // not called directly cuz we need to cancel our previous reveal/unlock/close calls + SetThink(&CNEO_KOTHMaster::RevealNextZoneThink); + SetNextThink(gpGlobals->curtime); +} + +void CNEO_KOTHMaster::RevealNextZoneThink() +{ + // round isn't live (still in freeze/warmup, or intermission after a round already + // ended) - the whole cycle is meaningless right now, just wait and check again + if (!NEORules()->IsRoundLive()) + { + SetNextThink(gpGlobals->curtime + 0.1f); + return; + } + + m_hPendingZone = PickNextZone(m_hActiveZone.Get()); + + CNEO_InfoKOTHZone *pNextZone = m_hPendingZone.Get(); + if (pNextZone) + pNextZone->SetVisible(true); + + SetThink(&CNEO_KOTHMaster::UnlockZoneThink); + SetNextThink(gpGlobals->curtime + sv_neo_koth_zone_pause_time.GetFloat()); +} + +void CNEO_KOTHMaster::UnlockZoneThink() +{ + if (!NEORules()->IsRoundLive()) + { + SetNextThink(gpGlobals->curtime + 0.1f); + return; + } + + CNEO_InfoKOTHZone *pZone = m_hPendingZone.Get(); + if (pZone) + { + pZone->SetCapturable(true); + UTIL_CenterPrintAll("CAPTURE NEW OBJECTIVE."); + } + m_hActiveZone = pZone; + m_hPendingZone = nullptr; + // do not switch zones if we have only one + if (m_Zones.Size() > 1) { + SetThink(&CNEO_KOTHMaster::CloseZoneThink); + SetNextThink(gpGlobals->curtime + sv_neo_koth_zone_switch_time.GetFloat()); + } +} + +void CNEO_KOTHMaster::CloseZoneThink() +{ + if (!NEORules()->IsRoundLive()) + { + SetNextThink(gpGlobals->curtime + 0.1f); + return; + } + + CNEO_InfoKOTHZone *pOldZone = m_hActiveZone.Get(); + if (pOldZone) + { + pOldZone->SetCapturable(false); + pOldZone->SetVisible(false); + UTIL_CenterPrintAll("NEW OBJECTIVE FOUND. GET READY."); + } + m_hActiveZone = nullptr; + + RevealNextZoneThink(); +} + +void CNEO_KOTHMaster::RegisterZone(CNEO_InfoKOTHZone *pZone) +{ + if (!pZone) + return; + + // avoid double activation + if (m_Zones.Find(pZone) != m_Zones.InvalidIndex()) + return; + + m_Zones.AddToTail(pZone); +} + +CNEO_InfoKOTHZone *CNEO_KOTHMaster::PickNextZone(CNEO_InfoKOTHZone *pOldZone) const +{ + if (m_Zones.IsEmpty()) + return nullptr; + + int idx = RandomInt(0, m_Zones.Count() - 1); + if (m_Zones.Count() > 1 && m_Zones[idx].Get() == pOldZone) + idx = (idx + 1) % m_Zones.Count(); + + return m_Zones[idx].Get(); +} + +void CNEO_KOTHMaster::ResetAllZones() +{ + for (int i = 0; i < m_Zones.Count(); ++i) + { + CNEO_InfoKOTHZone *pZone = m_Zones[i].Get(); + if (pZone) + pZone->ResetCapture(); + } + + // force-close whatever was visible/active and restart the reveal cycle fresh for the new round + CNEO_InfoKOTHZone *pActive = m_hActiveZone.Get(); + if (pActive) + { + pActive->SetCapturable(false); + pActive->SetVisible(false); + } + + CNEO_InfoKOTHZone *pPending = m_hPendingZone.Get(); + if (pPending && pPending != pActive) + { + pPending->SetCapturable(false); + pPending->SetVisible(false); + } + + m_hActiveZone = nullptr; + m_hPendingZone = nullptr; + + SetThink(&CNEO_KOTHMaster::RevealNextZoneThink); + SetNextThink(gpGlobals->curtime + NEORules()->GetRemainingPreRoundFreezeTime(true)); +} diff --git a/src/game/server/neo/neo_koth_master.h b/src/game/server/neo/neo_koth_master.h new file mode 100644 index 0000000000..3d6e2ad758 --- /dev/null +++ b/src/game/server/neo/neo_koth_master.h @@ -0,0 +1,32 @@ +#pragma once +#include "cbase.h" +#include "utlvector.h" + +class CNEO_InfoKOTHZone; + +class CNEO_KOTHMaster : public CLogicalEntity +{ +public: + DECLARE_CLASS( CNEO_KOTHMaster, CLogicalEntity ); + DECLARE_DATADESC(); + + virtual void Spawn() override; + // picks the next zone and makes it visible, but still locked (not capturable); schedules UnlockZoneThink + void RevealNextZoneThink(); + // makes the revealed zone capturable (this is now the active zone); schedules CloseZoneThink + void UnlockZoneThink(); + // closes the active zone (hides it, no longer capturable), then immediately reveals the next one + void CloseZoneThink(); + + // called by every neo_info_koth_zone on the map once it has found us + void RegisterZone(CNEO_InfoKOTHZone *pZone); + + void ResetAllZones(); + +private: + CNEO_InfoKOTHZone *PickNextZone(CNEO_InfoKOTHZone *pOldZone) const; + + CUtlVector> m_Zones; + CHandle m_hActiveZone; + CHandle m_hPendingZone; +}; diff --git a/src/game/server/neo/neo_trigger_koth_zone.cpp b/src/game/server/neo/neo_trigger_koth_zone.cpp new file mode 100644 index 0000000000..e66d46cf72 --- /dev/null +++ b/src/game/server/neo/neo_trigger_koth_zone.cpp @@ -0,0 +1,62 @@ +#include "neo_trigger_koth_zone.h" +#include "neo_gamerules.h" + +//############################## +// Trigger koth zone +//############################## + +LINK_ENTITY_TO_CLASS(neo_trigger_koth_zone, CNEO_TriggerKOTHZone); + +BEGIN_DATADESC(CNEO_TriggerKOTHZone) + DEFINE_KEYFIELD(m_iszZoneName, FIELD_STRING, "zone_name"), +END_DATADESC() + +void CNEO_TriggerKOTHZone::Spawn() +{ + BaseClass::Spawn(); + InitTrigger(); +} + +void CNEO_TriggerKOTHZone::Activate() +{ + BaseClass::Activate(); + + // resolved here (not in Spawn) since the target neo_info_koth_zone + // is not guaranteed to have spawned yet while we're still spawning + pZone = dynamic_cast( + gEntList.FindEntityByName( + nullptr, STRING(m_iszZoneName) + ) + ); + + if (pZone == nullptr) + { + // we SHOULD connect each trigger to some _info_ entity + Warning("Some neo_trigger_koth_zone was not connected to any neo_info_koth_zone entity!\n"); + } + else + { + pZone->AddChildTrigger(this); + // it will be inactive until neo_koth_master enables it + } + Disable(); +} + +void CNEO_TriggerKOTHZone::StartTouch(CBaseEntity *pOther) +{ + CNEO_Player *pPlayer = ToNEOPlayer(pOther); + if (!pPlayer || !pZone) + return; + + pZone->OnPlayerEnter(pPlayer); +} + +void CNEO_TriggerKOTHZone::EndTouch(CBaseEntity *pOther) +{ + CNEO_Player *pPlayer = ToNEOPlayer(pOther); + // if player is dead we still need to update zone + if (!pPlayer || !pZone) + return; + + pZone->OnPlayerLeave(pPlayer); +} diff --git a/src/game/server/neo/neo_trigger_koth_zone.h b/src/game/server/neo/neo_trigger_koth_zone.h new file mode 100644 index 0000000000..497c6f6230 --- /dev/null +++ b/src/game/server/neo/neo_trigger_koth_zone.h @@ -0,0 +1,20 @@ +#pragma once +#include "cbase.h" +#include "triggers.h" +#include "neo_info_koth_zone.h" + +class CNEO_TriggerKOTHZone : public CBaseTrigger +{ +public: + DECLARE_CLASS(CNEO_TriggerKOTHZone, CBaseTrigger); + DECLARE_DATADESC(); + + virtual void Spawn() override; + virtual void Activate() override; + virtual void StartTouch(CBaseEntity *pOther) override; + virtual void EndTouch(CBaseEntity *pOther) override; + + string_t m_iszZoneName; +private: + CNEO_InfoKOTHZone *pZone = nullptr; +}; diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 30f9053365..e47d216e4a 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -23,6 +23,7 @@ #include "neo_model_manager.h" #include "neo_ghost_spawn_point.h" #include "neo_ghost_cap_point.h" +#include "neo_koth_master.h" #include "neo/weapons/weapon_ghost.h" #include "neo/weapons/weapon_neobasecombatweapon.h" #include "eventqueue.h" @@ -151,6 +152,12 @@ ConVar sv_neo_suicide_prevent_cap_punish("sv_neo_suicide_prevent_cap_punish", "1 "while the other team is holding the ghost, reward the ghost holder team " "a rank up.", true, 0.0f, true, 1.0f); +// koth +ConVar sv_neo_koth_seconds_per_point("sv_neo_koth_point_multiplyer", "1.75", FCVAR_REPLICATED, "Seconds to get 1 point"); +ConVar sv_neo_koth_zone_switch_time("sv_neo_koth_zone_switch_time", "45", FCVAR_REPLICATED, + "How often (in seconds) neo_koth_master rotates the active KOTH zone.", true, 5.0f, false, 0.0f); +ConVar sv_neo_koth_zone_pause_time("sv_neo_koth_zone_pause_time", "5", FCVAR_REPLICATED, + "How long (in seconds) neo_koth_master waits after closing a zone before opening the next one.", true, 0.0f, false, 0.0f); #define DEF_TEAMPLAYERTHRES 5 static_assert(DEF_TEAMPLAYERTHRES <= ((MAX_PLAYERS - 1) / 2)); @@ -166,6 +173,7 @@ ConVar sv_neo_server_autorecord("sv_neo_server_autorecord", "0", FCVAR_NONE, "Au // Both CLIENT_DLL + GAME_DLL, but server-side setting so it's replicated onto client to read the values ConVar sv_neo_readyup_lobby("sv_neo_readyup_lobby", "0", FCVAR_REPLICATED, "If enabled, players would need to ready up and match the players total requirements to start a game.", true, 0.0f, true, 1.0f); +ConVar sv_neo_koth_max_score("sv_neo_koth_max_score", "100", FCVAR_REPLICATED, "The points needed to win this round"); ConVar sv_neo_pausematch_enabled("sv_neo_pausematch_enabled", "0", FCVAR_REPLICATED, "If enabled, players will be able to pause the match mid-game.", true, 0.0f, true, 1.0f); ConVar sv_neo_pausematch_unpauseimmediate("sv_neo_pausematch_unpauseimmediate", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "Testing only - If enabled, unpause will be immediate.", true, 0.0f, true, 1.0f); ConVar sv_neo_readyup_countdown("sv_neo_readyup_countdown", "5", FCVAR_REPLICATED, "Set the countdown from fully ready to start of match in seconds.", true, 0.0f, true, 120.0f); @@ -314,6 +322,8 @@ BEGIN_NETWORK_TABLE_NOBASE( CNEORules, DT_NEORules ) RecvPropInt(RECVINFO(m_iLastAttacker)), RecvPropInt(RECVINFO(m_iLastKiller)), RecvPropInt(RECVINFO(m_iLastGhoster)), + RecvPropInt(RECVINFO(m_iKothTimeJinrai)), + RecvPropInt(RECVINFO(m_iKothTimeNSF)), RecvPropInt(RECVINFO(m_iClassLimitRecon)), RecvPropInt(RECVINFO(m_iClassLimitAssault)), RecvPropInt(RECVINFO(m_iClassLimitSupport)), @@ -357,6 +367,8 @@ BEGIN_NETWORK_TABLE_NOBASE( CNEORules, DT_NEORules ) SendPropInt(SENDINFO(m_iLastAttacker), NumBitsForCount(MAX_PLAYERS_ARRAY_SAFE), SPROP_UNSIGNED), SendPropInt(SENDINFO(m_iLastKiller), NumBitsForCount(MAX_PLAYERS_ARRAY_SAFE), SPROP_UNSIGNED), SendPropInt(SENDINFO(m_iLastGhoster), NumBitsForCount(MAX_PLAYERS_ARRAY_SAFE), SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_iKothTimeJinrai)), + SendPropInt(SENDINFO(m_iKothTimeNSF)), SendPropInt(SENDINFO(m_iClassLimitRecon)), SendPropInt(SENDINFO(m_iClassLimitAssault)), SendPropInt(SENDINFO(m_iClassLimitSupport)), @@ -413,6 +425,7 @@ const NeoGameTypeSettings NEO_GAME_TYPE_SETTINGS[NEO_GAME_TYPE__TOTAL] = { /*NEO_GAME_TYPE_EMT*/ {"EMT", true, false, true, false, false}, /*NEO_GAME_TYPE_TUT*/ {"TUT", true, false, false, false, false}, /*NEO_GAME_TYPE_JGR*/ {"JGR", true, true, false, true, false}, +/*NEO_GAME_TYPE_KOTH*/ {"KOTH", true, true, false, false, false}, }; #ifdef CLIENT_DLL @@ -527,6 +540,9 @@ ConVar neo_dm_round_timelimit("neo_dm_round_timelimit", "10.25", FCVAR_REPLICATE ConVar neo_jgr_round_timelimit("neo_jgr_round_timelimit", "4.25", FCVAR_REPLICATED, "JGR round timelimit, in minutes.", true, 0.0f, false, 600.0f); +ConVar neo_koth_round_timelimit("neo_koth_round_timelimit", "5", FCVAR_REPLICATED, "KOTH round timelimit, in minutes.", + true, 0.0f, false, 600.0f); + ConVar sv_neo_ignore_wep_xp_limit("sv_neo_ignore_wep_xp_limit", "0", FCVAR_CHEAT | FCVAR_REPLICATED, "If true, allow equipping any loadout regardless of player XP.", true, 0.0f, true, 1.0f); @@ -1087,6 +1103,7 @@ void CNEORules::CheckGameType() m_nGameTypeSelected = (pEntGameCfg) ? pEntGameCfg->m_GameType : NEO_GAME_TYPE_EMT; } break; } + m_bGamemodeTypeBeenInitialized = true; iStaticInitOnCmd = iGamemodeEnforce; iStaticInitOnRandAllow = iGamemodeRandAllow; @@ -1121,6 +1138,21 @@ bool CNEORules::CheckShouldNotThink() return false; } +#ifdef GAME_DLL +void CNEORules::AddKothScore(const int team, const int points) +{ + if (points <= 0) + return; + + if (team == TEAM_JINRAI) + m_iKothTimeJinrai = Min(m_iKothTimeJinrai + points, sv_neo_koth_max_score.GetInt()); + else if (team == TEAM_NSF) + m_iKothTimeNSF = Min(m_iKothTimeNSF + points, sv_neo_koth_max_score.GetInt()); + else + Warning("Got unexpected KOTH team in score count: %d\n", team); +} +#endif + void CNEORules::Think(void) { #ifdef GAME_DLL @@ -1414,6 +1446,23 @@ void CNEORules::Think(void) return; } } + if (GetGameType() == NEO_GAME_TYPE_KOTH) + { + if (m_iKothTimeJinrai > m_iKothTimeNSF) + { + SetWinningTeam(TEAM_JINRAI, NEO_VICTORY_POINTS, false, true, false, false); + return; + } + + if (m_iKothTimeNSF > m_iKothTimeJinrai) + { + SetWinningTeam(TEAM_NSF, NEO_VICTORY_POINTS, false, true, false, false); + return; + } + + SetWinningTeam(TEAM_SPECTATOR, NEO_VICTORY_STALEMATE, false, true, true, false); + return; + } else if (GetGameType() == NEO_GAME_TYPE_DM) { // Winning player @@ -1565,7 +1614,6 @@ void CNEORules::Think(void) m_pJuggernautItem->m_bLocked = false; } } - if (GetGameType() == NEO_GAME_TYPE_JGR && IsRoundLive()) { if (GetGlobalTeam(TEAM_JINRAI)->GetScore() >= sv_neo_jgr_max_points.GetInt()) @@ -1680,7 +1728,8 @@ void CNEORules::Think(void) else if (IsRoundLive()) { COMPILE_TIME_ASSERT(TEAM_JINRAI == 2 && TEAM_NSF == 3); - if (GetGameType() != NEO_GAME_TYPE_TDM && GetGameType() != NEO_GAME_TYPE_DM && GetGameType() != NEO_GAME_TYPE_JGR) + if (GetGameType() != NEO_GAME_TYPE_TDM && GetGameType() != NEO_GAME_TYPE_DM && + GetGameType() != NEO_GAME_TYPE_JGR && GetGameType() != NEO_GAME_TYPE_KOTH) { auto jinraiAlive = GetGlobalTeam(TEAM_JINRAI)->GetAliveMembers(); auto nsfAlive = GetGlobalTeam(TEAM_NSF)->GetAliveMembers(); @@ -1705,6 +1754,29 @@ void CNEORules::Think(void) SetWinningDMPlayer(pHighestPlayers[0]); } } + if (GetGameType() == NEO_GAME_TYPE_KOTH) + { + if (m_iKothTimeJinrai >= sv_neo_koth_max_score.GetInt() && m_iKothTimeNSF >= sv_neo_koth_max_score.GetInt()) + { + // impossible but we should do something here... + SetWinningTeam(TEAM_SPECTATOR, NEO_VICTORY_STALEMATE, false, true, true, false); + return; + } + + if (m_iKothTimeNSF >= sv_neo_koth_max_score.GetInt()) + { + m_iKothTimeNSF = sv_neo_koth_max_score.GetInt(); + SetWinningTeam(TEAM_NSF, NEO_VICTORY_POINTS, false, true, false, false); + return; + } + + if (m_iKothTimeJinrai >= sv_neo_koth_max_score.GetInt()) + { + m_iKothTimeJinrai = sv_neo_koth_max_score.GetInt(); + SetWinningTeam(TEAM_JINRAI, NEO_VICTORY_POINTS, false, true, false, false); + return; + } + } } #endif } @@ -1860,6 +1932,9 @@ float CNEORules::GetRoundRemainingTime() const case NEO_GAME_TYPE_JGR: roundTimeLimit = neo_jgr_round_timelimit.GetFloat() * 60.f; break; + case NEO_GAME_TYPE_KOTH: + roundTimeLimit = neo_koth_round_timelimit.GetFloat() * 60.f; + break; default: break; } @@ -3026,6 +3101,9 @@ const SZWSZTexts NEO_GAME_TYPE_DESC_STRS[NEO_GAME_TYPE__TOTAL] = { SZWSZ_INIT("Deathmatch"), SZWSZ_INIT("Free Roam"), SZWSZ_INIT("Training"), + // neo todo: I added this line and then commented it out because I thought it might have been missed on purpose + // SZWSZ_INIT("Juggernaut"), + SZWSZ_INIT("King of the Hill"), }; const char *CNEORules::GetGameDescription(void) @@ -3295,6 +3373,11 @@ void CNEORules::SetGameRelatedVars() ResetJGR(); SpawnTheJuggernaut(); } + + if (GetGameType() == NEO_GAME_TYPE_KOTH) + { + ResetKOTH(); + } } void CNEORules::ResetTDM() @@ -3345,6 +3428,17 @@ void CNEORules::ResetJGR() } } +void CNEORules::ResetKOTH() { + m_iKothTimeJinrai = 0; + m_iKothTimeNSF = 0; + // neo TODO: give xp for holding the point? + + if (!m_pKothMaster) + m_pKothMaster = dynamic_cast(gEntList.FindEntityByClassname(nullptr, "neo_koth_master")); + + m_pKothMaster->ResetAllZones(); +} + void CNEORules::RestartGame() { // bounds check diff --git a/src/game/shared/neo/neo_gamerules.h b/src/game/shared/neo/neo_gamerules.h index 4497b7264a..21ed4ad9db 100644 --- a/src/game/shared/neo/neo_gamerules.h +++ b/src/game/shared/neo/neo_gamerules.h @@ -85,6 +85,21 @@ class CNEOGhostCapturePoint; class CNEO_Player; class CWeaponGhost; class CNEOBotSeekAndDestroy; +class CNEO_KOTHMaster; + +// this is not good, but it is used ONLY to indicate who controls the checkpoint. +// when its possible I prefer default TEAM_NSF/JINRAI vars +enum KothControllingTeams +{ + KOTH_NONE = 0, + KOTH_BOTH, + KOTH_NSF, + KOTH_JINRAI, +}; + +extern ConVar sv_neo_koth_seconds_per_point; +extern ConVar sv_neo_koth_zone_switch_time; +extern ConVar sv_neo_koth_zone_pause_time; extern ConVar sv_neo_mirror_teamdamage_multiplier; extern ConVar sv_neo_mirror_teamdamage_duration; @@ -95,6 +110,8 @@ extern ConVar sv_neo_teamdamage_kick; class C_NEO_Player; #endif +extern ConVar sv_neo_koth_max_score; + extern ConVar sv_neo_player_restore; enum NeoGameType { @@ -105,6 +122,7 @@ enum NeoGameType { NEO_GAME_TYPE_EMT, NEO_GAME_TYPE_TUT, NEO_GAME_TYPE_JGR, + NEO_GAME_TYPE_KOTH, NEO_GAME_TYPE__TOTAL // Number of game types }; @@ -276,6 +294,7 @@ class CNEORules : public CHL2MPRules, public CGameEventListener void ResetGhost(); void ResetVIP(); void ResetJGR(); + void ResetKOTH(); void CheckRestartGame(); @@ -360,6 +379,12 @@ class CNEORules : public CHL2MPRules, public CGameEventListener bool InReadyUpState() const; bool InRoundState() const; +#ifdef GAME_DLL + // called by the currently active neo_info_koth_zone once it has accumulated whole points + void AddKothScore(const int team, const int points); +#endif + int GetKothTimeJinrai() const { return m_iKothTimeJinrai; } + int GetKothTimeNSF() const { return m_iKothTimeNSF; } int GetOpposingTeam(const int team) const { @@ -481,6 +506,7 @@ class CNEORules : public CHL2MPRules, public CGameEventListener const int GetLastGhoster() const { return m_iLastGhoster; } #ifdef GAME_DLL private: + CNEO_KOTHMaster *m_pKothMaster = nullptr; CNEO_Juggernaut *m_pJuggernautItem = nullptr; CNEO_Player *m_pJuggernautPlayer = nullptr; float m_flJuggernautDeathTime = 0.0f; @@ -539,6 +565,10 @@ class CNEORules : public CHL2MPRules, public CGameEventListener CNetworkVar(float, m_flGhostLastHeld); CNetworkHandle( CWeaponGhost, m_hGhost ); + // KOTH networked variables + CNetworkVar(int, m_iKothTimeJinrai); + CNetworkVar(int, m_iKothTimeNSF); + // Juggernaut networked variables CNetworkVar(int, m_iJuggernautPlayerIndex); CNetworkVar(bool, m_bJuggernautItemExists);