From e507d00b980a3e60844f3596e3697c677605bc23 Mon Sep 17 00:00:00 2001 From: Hosomi-PC Date: Thu, 21 May 2026 21:27:38 +0100 Subject: [PATCH 1/8] Implement ATK --- .../client/neo/ui/neo_hud_round_state.cpp | 4 +- src/game/shared/neo/neo_gamerules.cpp | 312 ++++++++++++------ src/game/shared/neo/neo_gamerules.h | 13 +- src/game/shared/neo/neo_player_spawnpoint.cpp | 37 ++- src/game/shared/neo/neo_player_spawnpoint.h | 10 +- 5 files changed, 257 insertions(+), 119 deletions(-) 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 aec87e7174..5b5a39f215 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -378,9 +378,9 @@ void CNEOHud_RoundState::UpdateStateForNeoHudElementDraw() roundTimeLeft = NEORules()->GetRemainingPreRoundFreezeTime(true); int secsTotal = 0.0f; - if (roundStatus == NeoRoundStatus::Overtime && NEORules()->GetGameType() == NEO_GAME_TYPE_CTG) + if (roundStatus == NeoRoundStatus::Overtime && (NEORules()->GetGameType() == NEO_GAME_TYPE_CTG || NEORules()->GetGameType() == NEO_GAME_TYPE_ATK)) { - secsTotal = RoundFloatToInt(NEORules()->GetCTGOverTime()); + secsTotal = RoundFloatToInt(NEORules()->GetOverTime((NeoGameType)NEORules()->GetGameType())); } else { diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 0ddbbb19ab..0230ace686 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -372,6 +372,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_ATK*/ {"ATK", false, true, false, true, false}, }; #ifdef CLIENT_DLL @@ -459,6 +460,8 @@ ConVar sv_neo_dm_score_limit("sv_neo_dm_score_limit", "7", FCVAR_REPLICATED, "DM ConVar sv_neo_jgr_score_limit("sv_neo_jgr_score_limit", "0", FCVAR_REPLICATED, "JGR score limit", true, 0.0f, true, 99.0f); +ConVar sv_neo_atk_score_limit("sv_neo_atk_score_limit", "7", FCVAR_REPLICATED, "ATK score limit", true, 0.0f, true, 99.0f); + // Round Limit ConVar sv_neo_tdm_round_limit("sv_neo_tdm_round_limit", "0", FCVAR_REPLICATED, "TDM max amount of rounds, 0 for no limit.", true, 0.0f, false, 0.0f); @@ -470,6 +473,8 @@ ConVar sv_neo_dm_round_limit("sv_neo_dm_round_limit", "0", FCVAR_REPLICATED, "DM ConVar sv_neo_jgr_round_limit("sv_neo_jgr_round_limit", "5", FCVAR_REPLICATED, "JGR max amount of rounds, 0 for no limit.", true, 0.0f, false, 0.0f); +ConVar sv_neo_atk_round_limit("sv_neo_atk_round_limit", "0", FCVAR_REPLICATED, "ATK max amount of rounds, 0 for no limit.", true, 0.0f, false, 0.0f); + // Round Time Limit (make these sv_neo at some point) ConVar neo_tdm_round_timelimit("neo_tdm_round_timelimit", "10.25", FCVAR_REPLICATED, "TDM round timelimit, in minutes.", true, 0.0f, false, 600.0f); @@ -486,6 +491,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_atk_round_timelimit("neo_atk_round_timelimit", "3.25", FCVAR_REPLICATED, "ATK 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); @@ -496,6 +504,10 @@ ConVar sv_neo_ctg_ghost_overtime_enabled("sv_neo_ctg_ghost_overtime_enabled", "0 ConVar sv_neo_ctg_ghost_overtime("sv_neo_ctg_ghost_overtime", "45", FCVAR_REPLICATED, "Adds up to this many seconds to the round while the ghost is held.", true, 0, true, 120); ConVar sv_neo_ctg_ghost_overtime_grace("sv_neo_ctg_ghost_overtime_grace", "10", FCVAR_REPLICATED, "Number of seconds left in the round when the ghost is dropped in overtime.", true, 0, true, 30); ConVar sv_neo_ctg_ghost_overtime_grace_decay("sv_neo_ctg_ghost_overtime_grace_decay", "0", FCVAR_REPLICATED, "Slowly reduce the grace time as overtime goes on.", true, 0, true, 1); +ConVar sv_neo_atk_ghost_overtime_enabled("sv_neo_atk_ghost_overtime_enabled", "0", FCVAR_REPLICATED, "Enable ghost overtime in the ATK mode.", true, 0, true, 1); +ConVar sv_neo_atk_ghost_overtime("sv_neo_atk_ghost_overtime", "45", FCVAR_REPLICATED, "Adds up to this many seconds to the round while the ghost is held.", true, 0, true, 120); +ConVar sv_neo_atk_ghost_overtime_grace("sv_neo_atk_ghost_overtime_grace", "10", FCVAR_REPLICATED, "Number of seconds left in the round when the ghost is dropped in overtime.", true, 0, true, 30); +ConVar sv_neo_atk_ghost_overtime_grace_decay("sv_neo_atk_ghost_overtime_grace_decay", "0", FCVAR_REPLICATED, "Slowly reduce the grace time as overtime goes on.", true, 0, true, 1); #ifdef CLIENT_DLL extern ConVar neo_fov; @@ -1250,19 +1262,7 @@ void CNEORules::Think(void) return; } - if (m_nGameTypeSelected == NEO_GAME_TYPE_CTG) - { - if (sv_neo_ctg_ghost_overtime_enabled.GetBool() && m_nRoundStatus == NeoRoundStatus::RoundLive && m_iGhosterPlayer && - (m_flNeoRoundStartTime + (neo_ctg_round_timelimit.GetFloat() * 60) - sv_neo_ctg_ghost_overtime_grace.GetFloat()) < gpGlobals->curtime) - { - m_nRoundStatus = NeoRoundStatus::Overtime; - } - - if (m_nRoundStatus == NeoRoundStatus::Overtime && m_iGhosterPlayer) - { - m_flGhostLastHeld = gpGlobals->curtime; - } - } + CheckOvertime(); if (g_fGameOver) // someone else quit the game already { @@ -1376,77 +1376,7 @@ void CNEORules::Think(void) // Note that exactly zero here means infinite round time. else if (GetRoundRemainingTime() < 0) { - if (GetGameType() == NEO_GAME_TYPE_TDM) - { - if (GetGlobalTeam(TEAM_JINRAI)->GetScore() > GetGlobalTeam(TEAM_NSF)->GetScore()) - { - SetWinningTeam(TEAM_JINRAI, NEO_VICTORY_POINTS, false, true, false, false); - return; - } - - if (GetGlobalTeam(TEAM_NSF)->GetScore() > GetGlobalTeam(TEAM_JINRAI)->GetScore()) - { - SetWinningTeam(TEAM_NSF, NEO_VICTORY_POINTS, false, true, false, false); - return; - } - } - else if (GetGameType() == NEO_GAME_TYPE_DM) - { - // Winning player - CNEO_Player *pWinners[MAX_PLAYERS + 1] = {}; - int iWinnersTotal = 0; - int iWinnerXP = 0; - GetDMHighestScorers(&pWinners, &iWinnersTotal, &iWinnerXP); - if (iWinnersTotal == 1) - { - SetWinningDMPlayer(pWinners[0]); - return; - } - // Otherwise go into overtime - } - else if (GetGameType() == NEO_GAME_TYPE_JGR) - { - if ((!m_pJuggernautPlayer && m_pJuggernautItem && !m_pJuggernautItem->IsBeingActivatedByLosingTeam()) || - (!m_pJuggernautPlayer && !m_pJuggernautItem)) // Juggernaut is absent entirely - { - if (GetGlobalTeam(TEAM_JINRAI)->GetScore() > GetGlobalTeam(TEAM_NSF)->GetScore()) - { - SetWinningTeam(TEAM_JINRAI, NEO_VICTORY_POINTS, false, true, false, false); - return; - } - - if (GetGlobalTeam(TEAM_NSF)->GetScore() > GetGlobalTeam(TEAM_JINRAI)->GetScore()) - { - SetWinningTeam(TEAM_NSF, NEO_VICTORY_POINTS, false, true, false, false); - return; - } - } - else - { - if (m_nRoundStatus == NeoRoundStatus::RoundLive) - { - m_nRoundStatus = NeoRoundStatus::Overtime; - } - - if (m_pJuggernautPlayer) - { - const int jgrTeam = m_pJuggernautPlayer->GetTeamNumber(); - const int oppositeTeam = (m_pJuggernautPlayer->GetTeamNumber() == TEAM_JINRAI ? TEAM_NSF : TEAM_JINRAI); - if (GetGlobalTeam(jgrTeam)->GetScore() > GetGlobalTeam(oppositeTeam)->GetScore()) - { - SetWinningTeam(jgrTeam, NEO_VICTORY_POINTS, false, true, false, false); - return; - } - } - - return; - } - } - - if (IsTeamplay()) - { - SetWinningTeam(TEAM_SPECTATOR, NEO_VICTORY_STALEMATE, false, false, true, false); - } + RoundTimeout(); } if (m_pGhost) @@ -1792,6 +1722,41 @@ void CNEORules::AwardRankUp(CNEO_Player *pClient) pClient->AddPoints(1, false, true); } +void CNEORules::CheckOvertime() +{ + bool overtimeEnabled; + float overtimeGrace; + float roundTimeLimit; + + switch (m_nGameTypeSelected) + { + case NEO_GAME_TYPE_CTG: + overtimeEnabled = sv_neo_ctg_ghost_overtime_enabled.GetBool(); + roundTimeLimit = neo_ctg_round_timelimit.GetFloat() * 60; + overtimeGrace = sv_neo_ctg_ghost_overtime_grace.GetFloat(); + break; + case NEO_GAME_TYPE_ATK: + overtimeEnabled = sv_neo_atk_ghost_overtime_enabled.GetBool(); + roundTimeLimit = neo_atk_round_timelimit.GetFloat() * 60; + overtimeGrace = sv_neo_atk_ghost_overtime_grace.GetFloat(); + break; + default: + return; + } + + if (overtimeEnabled && m_nRoundStatus == NeoRoundStatus::RoundLive && m_iGhosterPlayer && + (m_nGameTypeSelected != NEO_GAME_TYPE_ATK || m_iGhosterTeam == GetAttackingTeam()) && // only the attacking team gets overtime in ATK + (m_flNeoRoundStartTime + roundTimeLimit - overtimeGrace) < gpGlobals->curtime) + { + m_nRoundStatus = NeoRoundStatus::Overtime; + } + + if (m_nRoundStatus == NeoRoundStatus::Overtime && m_iGhosterPlayer) + { + m_flGhostLastHeld = gpGlobals->curtime; + } +} + // Return remaining time in seconds. Zero means there is no time limit. float CNEORules::GetRoundRemainingTime() const { @@ -1824,7 +1789,7 @@ float CNEORules::GetRoundRemainingTime() const roundTimeLimit = neo_ctg_round_timelimit.GetFloat() * 60.f; if (m_nRoundStatus == NeoRoundStatus::Overtime) { - return GetCTGOverTime(); + return GetOverTime(NEO_GAME_TYPE_CTG); } break; case NEO_GAME_TYPE_VIP: @@ -1836,6 +1801,13 @@ float CNEORules::GetRoundRemainingTime() const case NEO_GAME_TYPE_JGR: roundTimeLimit = neo_jgr_round_timelimit.GetFloat() * 60.f; break; + case NEO_GAME_TYPE_ATK: + roundTimeLimit = neo_atk_round_timelimit.GetFloat() * 60.f; + if (m_nRoundStatus == NeoRoundStatus::Overtime) + { + return GetOverTime(NEO_GAME_TYPE_ATK); + } + break; default: break; } @@ -1844,12 +1816,34 @@ float CNEORules::GetRoundRemainingTime() const return (m_flNeoRoundStartTime + roundTimeLimit) - gpGlobals->curtime; } -float CNEORules::GetCTGOverTime() const +float CNEORules::GetOverTime(NeoGameType eGameType) const { - float roundTimeLimit = neo_ctg_round_timelimit.GetFloat() * 60.f; - float overtime = (m_flNeoRoundStartTime + roundTimeLimit + sv_neo_ctg_ghost_overtime.GetFloat()) - gpGlobals->curtime; + float roundTimeLimit; + float overtimeBaseAmount; + float overtimeGrace; + bool graceDecay; + + if (eGameType == NeoGameType::NEO_GAME_TYPE_CTG) + { + roundTimeLimit = neo_ctg_round_timelimit.GetFloat() * 60.f; + overtimeBaseAmount = sv_neo_ctg_ghost_overtime.GetFloat(); + overtimeGrace = sv_neo_ctg_ghost_overtime_grace.GetFloat(); + graceDecay = sv_neo_ctg_ghost_overtime_grace_decay.GetBool(); + } + else if (eGameType == NeoGameType::NEO_GAME_TYPE_ATK) + { + roundTimeLimit = neo_atk_round_timelimit.GetFloat() * 60.f; + overtimeBaseAmount = sv_neo_atk_ghost_overtime.GetFloat(); + overtimeGrace = sv_neo_atk_ghost_overtime_grace.GetFloat(); + graceDecay = sv_neo_atk_ghost_overtime_grace_decay.GetBool(); + } + else + { + throw std::exception("Tried to calculate overtime for a gamemode with no overtime implementation"); + } + float overtime = (m_flNeoRoundStartTime + roundTimeLimit + overtimeBaseAmount) - gpGlobals->curtime; - if (sv_neo_ctg_ghost_overtime_grace_decay.GetBool()) + if (graceDecay) { if (m_iGhosterPlayer) { @@ -1857,13 +1851,13 @@ float CNEORules::GetCTGOverTime() const } else { - float overtimeAtGhostDrop = (m_flNeoRoundStartTime + roundTimeLimit + sv_neo_ctg_ghost_overtime.GetFloat()) - m_flGhostLastHeld; - return (overtimeAtGhostDrop * sv_neo_ctg_ghost_overtime_grace.GetFloat() / (sv_neo_ctg_ghost_overtime.GetFloat() + sv_neo_ctg_ghost_overtime_grace.GetFloat())) - (gpGlobals->curtime - m_flGhostLastHeld); + float overtimeAtGhostDrop = (m_flNeoRoundStartTime + roundTimeLimit + overtimeBaseAmount) - m_flGhostLastHeld; + return (overtimeAtGhostDrop * overtimeGrace / (overtimeBaseAmount + overtimeGrace)) - (gpGlobals->curtime - m_flGhostLastHeld); } } else { - float grace = sv_neo_ctg_ghost_overtime_grace.GetFloat() - (gpGlobals->curtime - m_flGhostLastHeld); + float grace = overtimeGrace - (gpGlobals->curtime - m_flGhostLastHeld); if (m_iGhosterPlayer || overtime < grace) { return overtime; @@ -1938,8 +1932,8 @@ void CNEORules::FireGameEvent(IGameEvent* event) // Purpose: Spawns one ghost at a randomly chosen Neo ghost spawn point. void CNEORules::SpawnTheGhost(const Vector *origin) { - // No ghost spawns and this map isn't named "_ctg". Probably not a CTG map. - if (m_ghostSpawns.IsEmpty() && (V_stristr(GameRules()->MapName(), "_ctg") == 0)) + // No ghost spawns + if (m_ghostSpawns.IsEmpty()) { m_pGhost = nullptr; return; @@ -2642,6 +2636,9 @@ const int CNEORules::GetScoreLimit() const case NEO_GAME_TYPE_JGR: return sv_neo_jgr_score_limit.GetInt(); break; + case NEO_GAME_TYPE_ATK: + return sv_neo_atk_score_limit.GetInt(); + break; default: return sv_neo_ctg_score_limit.GetInt(); break; @@ -2667,6 +2664,9 @@ const int CNEORules::GetRoundLimit() const case NEO_GAME_TYPE_JGR: return sv_neo_jgr_round_limit.GetInt(); break; + case NEO_GAME_TYPE_ATK: + return sv_neo_atk_round_limit.GetInt(); + break; default: return sv_neo_ctg_round_limit.GetInt(); break; @@ -2926,6 +2926,105 @@ void CNEORules::StartNextRound() DevMsg("New round start here!\n"); } + +void CNEORules::RoundTimeout() +{ + bool winnerDetermined = false; + switch (GetGameType()) + { + case NEO_GAME_TYPE_TDM: winnerDetermined = RoundTimeoutTDM(); + break; + case NEO_GAME_TYPE_DM: winnerDetermined = RoundTimeoutDM(); + break; + case NEO_GAME_TYPE_JGR: winnerDetermined = RoundTimeoutJGR(); + break; + case NEO_GAME_TYPE_ATK: winnerDetermined = RoundTimeoutATK(); + break; + default: + break; + } + if (!winnerDetermined && IsTeamplay()) + { + SetWinningTeam(TEAM_SPECTATOR, NEO_VICTORY_STALEMATE, false, false, true, false); + } +} + +bool CNEORules::RoundTimeoutTDM() +{ + if (GetGlobalTeam(TEAM_JINRAI)->GetScore() > GetGlobalTeam(TEAM_NSF)->GetScore()) + { + SetWinningTeam(TEAM_JINRAI, NEO_VICTORY_POINTS, false, true, false, false); + return true; + } + if (GetGlobalTeam(TEAM_NSF)->GetScore() > GetGlobalTeam(TEAM_JINRAI)->GetScore()) + { + SetWinningTeam(TEAM_NSF, NEO_VICTORY_POINTS, false, true, false, false); + return true; + } + return false; +} + +bool CNEORules::RoundTimeoutDM() +{ + // Winning player + CNEO_Player* pWinners[MAX_PLAYERS + 1] = {}; + int iWinnersTotal = 0; + int iWinnerXP = 0; + GetDMHighestScorers(&pWinners, &iWinnersTotal, &iWinnerXP); + if (iWinnersTotal == 1) + { + SetWinningDMPlayer(pWinners[0]); + return true; + } + // Otherwise go into overtime + return false; +} + +bool CNEORules::RoundTimeoutJGR() +{ + if ((!m_pJuggernautPlayer && m_pJuggernautItem && !m_pJuggernautItem->IsBeingActivatedByLosingTeam()) || + (!m_pJuggernautPlayer && !m_pJuggernautItem)) // Juggernaut is absent entirely + { + if (GetGlobalTeam(TEAM_JINRAI)->GetScore() > GetGlobalTeam(TEAM_NSF)->GetScore()) + { + SetWinningTeam(TEAM_JINRAI, NEO_VICTORY_POINTS, false, true, false, false); + return true; + } + + if (GetGlobalTeam(TEAM_NSF)->GetScore() > GetGlobalTeam(TEAM_JINRAI)->GetScore()) + { + SetWinningTeam(TEAM_NSF, NEO_VICTORY_POINTS, false, true, false, false); + return true; + } + } + else + { + if (m_nRoundStatus == NeoRoundStatus::RoundLive) + { + m_nRoundStatus = NeoRoundStatus::Overtime; + } + + if (m_pJuggernautPlayer) + { + const int jgrTeam = m_pJuggernautPlayer->GetTeamNumber(); + const int oppositeTeam = (m_pJuggernautPlayer->GetTeamNumber() == TEAM_JINRAI ? TEAM_NSF : TEAM_JINRAI); + if (GetGlobalTeam(jgrTeam)->GetScore() > GetGlobalTeam(oppositeTeam)->GetScore()) + { + SetWinningTeam(jgrTeam, NEO_VICTORY_POINTS, false, true, false, false); + return true; + } + } + + return true; + } + return false; +} + +bool CNEORules::RoundTimeoutATK() +{ + SetWinningTeam(GetDefendingTeam(), NEO_VICTORY_ATK_TIMEOUT, false, true, false, false); + return true; +} #endif bool CNEORules::IsRoundPreRoundFreeze() const @@ -2987,6 +3086,8 @@ const SZWSZTexts NEO_GAME_TYPE_DESC_STRS[NEO_GAME_TYPE__TOTAL] = { SZWSZ_INIT("Deathmatch"), SZWSZ_INIT("Free Roam"), SZWSZ_INIT("Training"), + SZWSZ_INIT("Juggernaut"), + SZWSZ_INIT("Attack/Defend"), }; const char *CNEORules::GetGameDescription(void) @@ -3207,13 +3308,14 @@ void CNEORules::SetGameRelatedVars() ResetTDM(); ResetGhost(); - if (GetGameType() == NEO_GAME_TYPE_CTG) + NeoGameType eGameType = (NeoGameType)GetGameType(); + if (eGameType == NEO_GAME_TYPE_CTG || eGameType == NEO_GAME_TYPE_ATK) { SpawnTheGhost(); } ResetVIP(); - if (GetGameType() == NEO_GAME_TYPE_VIP) + if (eGameType == NEO_GAME_TYPE_VIP) { if (!m_iEscortingTeam) { @@ -3231,7 +3333,7 @@ void CNEORules::SetGameRelatedVars() m_iEscortingTeam.Set(0); } - if (GetGameType() == NEO_GAME_TYPE_TDM) + if (eGameType == NEO_GAME_TYPE_TDM) { for (int i = 0; i < GetNumberOfTeams(); i++) { @@ -3239,7 +3341,7 @@ void CNEORules::SetGameRelatedVars() } } - if (GetGameType() == NEO_GAME_TYPE_DM) + if (eGameType == NEO_GAME_TYPE_DM) { for (int i = 1; i <= gpGlobals->maxClients; ++i) { @@ -3251,7 +3353,7 @@ void CNEORules::SetGameRelatedVars() } } - if (GetGameType() == NEO_GAME_TYPE_JGR) + if (eGameType == NEO_GAME_TYPE_JGR) { ResetJGR(); SpawnTheJuggernaut(); @@ -3893,7 +3995,7 @@ void CNEORules::SetWinningTeam(int team, int iWinReason, bool bForceMapReset, bo } } } - else if (GetGameType() == NEO_GAME_TYPE_CTG || GetGameType() == NEO_GAME_TYPE_VIP) + else if (GetGameType() == NEO_GAME_TYPE_CTG || GetGameType() == NEO_GAME_TYPE_VIP || GetGameType() == NEO_GAME_TYPE_ATK) { if (sv_neo_survivor_bonus.GetBool() && player->IsAlive()) { @@ -4705,6 +4807,16 @@ bool CNEORules::IsJuggernautLocked() const return false; } +int CNEORules::GetAttackingTeam() const +{ + return roundNumberIsEven() ? TEAM_JINRAI : TEAM_NSF; +} + +int CNEORules::GetDefendingTeam() const +{ + return roundNumberIsEven() ? TEAM_NSF : TEAM_JINRAI; +} + const char *CNEORules::GetTeamClantag(const int iTeamNum) const { switch (iTeamNum) diff --git a/src/game/shared/neo/neo_gamerules.h b/src/game/shared/neo/neo_gamerules.h index 5602e0b267..79dcf4b2b7 100644 --- a/src/game/shared/neo/neo_gamerules.h +++ b/src/game/shared/neo/neo_gamerules.h @@ -104,6 +104,7 @@ enum NeoGameType { NEO_GAME_TYPE_EMT, NEO_GAME_TYPE_TUT, NEO_GAME_TYPE_JGR, + NEO_GAME_TYPE_ATK, NEO_GAME_TYPE__TOTAL // Number of game types }; @@ -131,6 +132,7 @@ enum NeoWinReason { NEO_VICTORY_VIP_ELIMINATION, NEO_VICTORY_TEAM_ELIMINATION, NEO_VICTORY_TIMEOUT_WIN_BY_NUMBERS, + NEO_VICTORY_ATK_TIMEOUT, NEO_VICTORY_POINTS, NEO_VICTORY_FORFEIT, NEO_VICTORY_STALEMATE, // Not actually a victory @@ -287,8 +289,10 @@ class CNEORules : public CHL2MPRules, public CGameEventListener virtual bool CheckGameOver(void) OVERRIDE; + void CheckOvertime(); + float GetRoundRemainingTime() const; - float GetCTGOverTime() const; + float GetOverTime(NeoGameType eGameType) const; float GetRoundAccumulatedTime() const; #ifdef GAME_DLL float MirrorDamageMultiplier() const; @@ -331,6 +335,11 @@ class CNEORules : public CHL2MPRules, public CGameEventListener void CheckGameType(); void CheckGameConfig(); void StartNextRound(); + void RoundTimeout(); + bool RoundTimeoutTDM(); + bool RoundTimeoutDM(); + bool RoundTimeoutJGR(); + bool RoundTimeoutATK(); virtual const char* GetChatFormat(bool bTeamOnly, CBasePlayer* pPlayer) OVERRIDE; virtual const char* GetChatPrefix(bool bTeamOnly, CBasePlayer* pPlayer) OVERRIDE { return ""; } // handled by GetChatFormat @@ -379,6 +388,8 @@ class CNEORules : public CHL2MPRules, public CGameEventListener inline int roundNumber() const { return m_iRoundNumber; } inline bool roundNumberIsEven() const { return (roundNumber() % 2 == 0); } + int GetAttackingTeam() const; + int GetDefendingTeam() const; #ifdef GLOWS_ENABLE void GetTeamGlowColor(int teamNumber, float &r, float &g, float &b) diff --git a/src/game/shared/neo/neo_player_spawnpoint.cpp b/src/game/shared/neo/neo_player_spawnpoint.cpp index f3acb0f902..3085af1dca 100644 --- a/src/game/shared/neo/neo_player_spawnpoint.cpp +++ b/src/game/shared/neo/neo_player_spawnpoint.cpp @@ -10,25 +10,25 @@ // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" -class CNEOSpawnPoint_Jinrai : public CNEOSpawnPoint +class CNEOSpawnPoint_Attacker : public CNEOSpawnPoint { public: - CNEOSpawnPoint_Jinrai() : CNEOSpawnPoint() + CNEOSpawnPoint_Attacker() : CNEOSpawnPoint() { - m_iOwningTeam = TEAM_JINRAI; + m_eSide = E_TeamSide::Attacker; } }; -LINK_ENTITY_TO_CLASS(info_player_attacker, CNEOSpawnPoint_Jinrai); +LINK_ENTITY_TO_CLASS(info_player_attacker, CNEOSpawnPoint_Attacker); -class CNEOSpawnPoint_NSF : public CNEOSpawnPoint +class CNEOSpawnPoint_Defender : public CNEOSpawnPoint { public: - CNEOSpawnPoint_NSF() : CNEOSpawnPoint() + CNEOSpawnPoint_Defender() : CNEOSpawnPoint() { - m_iOwningTeam = TEAM_NSF; + m_eSide = E_TeamSide::Defender; } }; -LINK_ENTITY_TO_CLASS(info_player_defender, CNEOSpawnPoint_NSF); +LINK_ENTITY_TO_CLASS(info_player_defender, CNEOSpawnPoint_Defender); #ifdef GAME_DLL IMPLEMENT_SERVERCLASS_ST(CNEOSpawnPoint, DT_NEOSpawnPoint) @@ -55,7 +55,7 @@ END_DATADESC() CNEOSpawnPoint::CNEOSpawnPoint() { - m_iOwningTeam = TEAM_UNASSIGNED; + m_eSide = E_TeamSide::Unspecified; } CNEOSpawnPoint::~CNEOSpawnPoint() @@ -69,7 +69,7 @@ void CNEOSpawnPoint::Spawn() { BaseClass::Spawn(); - AssertMsg(m_iOwningTeam == TEAM_JINRAI || m_iOwningTeam == TEAM_NSF || m_iOwningTeam == TEAM_ANY, + AssertMsg(m_eSide == E_TeamSide::Unspecified || m_eSide == E_TeamSide::Attacker || m_eSide == E_TeamSide::Defender, "CNEOSpawnPoint shouldn't be instantiated directly; use info_player_attacker/defender instead!\n"); #if(0) @@ -85,13 +85,20 @@ void CNEOSpawnPoint::Spawn() int CNEOSpawnPoint::GetOwningTeam() const { - const bool alternate = NEORules()->roundNumberIsEven(); - int owningTeam = m_iOwningTeam; - if (!alternate && owningTeam != TEAM_ANY) + switch (m_eSide) { - owningTeam = (owningTeam == TEAM_JINRAI) ? TEAM_NSF : (owningTeam == TEAM_NSF) ? TEAM_JINRAI : owningTeam; + case E_TeamSide::Attacker: + return NEORules()->GetAttackingTeam(); + case E_TeamSide::Defender: + return NEORules()->GetDefendingTeam(); + default: + return TEAM_ANY; } - return owningTeam; +} + +E_TeamSide CNEOSpawnPoint::GetSide() const +{ + return m_eSide; } #ifdef GAME_DLL diff --git a/src/game/shared/neo/neo_player_spawnpoint.h b/src/game/shared/neo/neo_player_spawnpoint.h index 9095a603b3..90598453e8 100644 --- a/src/game/shared/neo/neo_player_spawnpoint.h +++ b/src/game/shared/neo/neo_player_spawnpoint.h @@ -12,6 +12,13 @@ #define CNEOSpawnPoint C_NEOSpawnPoint #endif +enum class E_TeamSide +{ + Unspecified, + Attacker, + Defender +}; + class CNEOSpawnPoint : public CBaseEntity { DECLARE_CLASS(CNEOSpawnPoint, CBaseEntity); @@ -29,6 +36,7 @@ class CNEOSpawnPoint : public CBaseEntity virtual void Spawn() override; int GetOwningTeam() const; + E_TeamSide GetSide() const; #ifdef GAME_DLL bool m_bDisabled; @@ -40,7 +48,7 @@ class CNEOSpawnPoint : public CBaseEntity #endif protected: - int m_iOwningTeam; + E_TeamSide m_eSide; private: CNEOSpawnPoint(const CNEOSpawnPoint &other); From 1135162c2d9c5ab801a9928e2a05cce82963f9d2 Mon Sep 17 00:00:00 2001 From: Hosomi-PC Date: Thu, 21 May 2026 22:02:00 +0100 Subject: [PATCH 2/8] fix build error --- src/game/server/subs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/server/subs.cpp b/src/game/server/subs.cpp index 92e27d7e09..f49201c969 100644 --- a/src/game/server/subs.cpp +++ b/src/game/server/subs.cpp @@ -55,7 +55,7 @@ class CBaseDMStart : public CPointEntity CBaseDMStart() : CNEOSpawnPoint() { - m_iOwningTeam = TEAM_ANY; + m_eSide = E_TeamSide::Unspecified; } #else DECLARE_CLASS( CBaseDMStart, CPointEntity ); From c4227c8517d43ea4bf4fc2c081928b5a2fd430bd Mon Sep 17 00:00:00 2001 From: Hosomi-PC Date: Thu, 21 May 2026 22:02:07 +0100 Subject: [PATCH 3/8] Add victory message --- src/game/shared/neo/neo_gamerules.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 0230ace686..2bfc06d89d 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -3903,6 +3903,9 @@ void CNEORules::SetWinningTeam(int team, int iWinReason, bool bForceMapReset, bo case NEO_VICTORY_TIMEOUT_WIN_BY_NUMBERS: V_sprintf_safe(victoryMsg, "Team %s wins by numbers!\n", (team == TEAM_JINRAI ? "Jinrai" : "NSF")); break; + case NEO_VICTORY_ATK_TIMEOUT: + V_sprintf_safe(victoryMsg, "Team %s wins by defending the ghost!\n", (team == TEAM_JINRAI ? "Jinrai" : "NSF")); + break; case NEO_VICTORY_POINTS: V_sprintf_safe(victoryMsg, "Team %s wins by highest score!\n", (team == TEAM_JINRAI ? "Jinrai" : "NSF")); break; From 6a43998268575820a96f9fe6f29a7d6ba6452cb6 Mon Sep 17 00:00:00 2001 From: Hosomi-PC Date: Thu, 21 May 2026 22:33:28 +0100 Subject: [PATCH 4/8] Update fgd --- game/bin/rebuild.fgd | 1 + 1 file changed, 1 insertion(+) diff --git a/game/bin/rebuild.fgd b/game/bin/rebuild.fgd index 19d69818fa..4f5aa29b1e 100644 --- a/game/bin/rebuild.fgd +++ b/game/bin/rebuild.fgd @@ -40,6 +40,7 @@ 2 : "VIP" 3 : "DM" 6 : "JGR" + 7 : "ATK 4 : "Empty" 5 : "Tutorial" ] From 1744ebad7c19f8fffc9e4cd7a72e77ce4305cf76 Mon Sep 17 00:00:00 2001 From: Hosomi-PC Date: Thu, 21 May 2026 22:55:15 +0100 Subject: [PATCH 5/8] fix linux build --- src/game/shared/neo/neo_gamerules.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 2bfc06d89d..26824377db 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -1839,7 +1839,7 @@ float CNEORules::GetOverTime(NeoGameType eGameType) const } else { - throw std::exception("Tried to calculate overtime for a gamemode with no overtime implementation"); + Assert(false && "Tried to calculate overtime for a gamemode with no overtime implementation"); } float overtime = (m_flNeoRoundStartTime + roundTimeLimit + overtimeBaseAmount) - gpGlobals->curtime; From ce73170fbb0cff19c83d3ed9e7802bd5f22c7815 Mon Sep 17 00:00:00 2001 From: Hosomi-PC Date: Thu, 21 May 2026 23:08:06 +0100 Subject: [PATCH 6/8] Fix CI error --- src/game/shared/neo/neo_gamerules.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 26824377db..a9c69f5f22 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -1823,24 +1823,25 @@ float CNEORules::GetOverTime(NeoGameType eGameType) const float overtimeGrace; bool graceDecay; - if (eGameType == NeoGameType::NEO_GAME_TYPE_CTG) + switch (eGameType) { + case NeoGameType::NEO_GAME_TYPE_CTG: roundTimeLimit = neo_ctg_round_timelimit.GetFloat() * 60.f; overtimeBaseAmount = sv_neo_ctg_ghost_overtime.GetFloat(); overtimeGrace = sv_neo_ctg_ghost_overtime_grace.GetFloat(); graceDecay = sv_neo_ctg_ghost_overtime_grace_decay.GetBool(); - } - else if (eGameType == NeoGameType::NEO_GAME_TYPE_ATK) - { + break; + case NeoGameType::NEO_GAME_TYPE_ATK: roundTimeLimit = neo_atk_round_timelimit.GetFloat() * 60.f; overtimeBaseAmount = sv_neo_atk_ghost_overtime.GetFloat(); overtimeGrace = sv_neo_atk_ghost_overtime_grace.GetFloat(); graceDecay = sv_neo_atk_ghost_overtime_grace_decay.GetBool(); - } - else - { + break; + default: Assert(false && "Tried to calculate overtime for a gamemode with no overtime implementation"); + return; } + float overtime = (m_flNeoRoundStartTime + roundTimeLimit + overtimeBaseAmount) - gpGlobals->curtime; if (graceDecay) From 5f944c1a1f0cec760e172b7a45398814ff15c9da Mon Sep 17 00:00:00 2001 From: Hosomi-PC Date: Thu, 21 May 2026 23:13:38 +0100 Subject: [PATCH 7/8] My shame is growing --- src/game/shared/neo/neo_gamerules.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index a9c69f5f22..2cc3aa8e3d 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -1839,7 +1839,7 @@ float CNEORules::GetOverTime(NeoGameType eGameType) const break; default: Assert(false && "Tried to calculate overtime for a gamemode with no overtime implementation"); - return; + return (m_flNeoRoundStartTime + roundTimeLimit) - gpGlobals->curtime; } float overtime = (m_flNeoRoundStartTime + roundTimeLimit + overtimeBaseAmount) - gpGlobals->curtime; From 1a2af01443459f92184cae1588efbe94f1ecc841 Mon Sep 17 00:00:00 2001 From: Hosomi-PC Date: Thu, 21 May 2026 23:32:35 +0100 Subject: [PATCH 8/8] Fix uninitialized return after assert(false) --- src/game/shared/neo/neo_gamerules.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 2cc3aa8e3d..95a7927669 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -1839,7 +1839,7 @@ float CNEORules::GetOverTime(NeoGameType eGameType) const break; default: Assert(false && "Tried to calculate overtime for a gamemode with no overtime implementation"); - return (m_flNeoRoundStartTime + roundTimeLimit) - gpGlobals->curtime; + return 0; } float overtime = (m_flNeoRoundStartTime + roundTimeLimit + overtimeBaseAmount) - gpGlobals->curtime;