From 2846c9e45d9781927eac449a8a2c37c5e7ff015b Mon Sep 17 00:00:00 2001 From: rowea Date: Sun, 31 May 2026 13:29:37 +0100 Subject: [PATCH] Restore Stargate VGUI flow --- game/client/client_hl2mp.vpc | 6 +- game/client/hl2mp/clientmode_hl2mpnormal.cpp | 18 +- game/client/stargate/sg_hud.cpp | 570 +++++++++++++++++++ game/client/stargate/sg_hud.h | 128 +++++ game/client/stargate/sg_teammenu.cpp | 275 +++++++++ game/client/stargate/sg_teammenu.h | 68 +++ game/server/hl2mp/hl2mp_client.cpp | 31 +- game/server/hl2mp/hl2mp_player.cpp | 300 ++++++---- game/server/hl2mp/hl2mp_player.h | 3 + game/server/server_hl2mp.vpc | 1 + game/server/stargate/sg_player_class.cpp | 295 ++++++++++ game/shared/hl2mp/hl2mp_gamerules.cpp | 9 +- 12 files changed, 1586 insertions(+), 118 deletions(-) create mode 100644 game/client/stargate/sg_hud.cpp create mode 100644 game/client/stargate/sg_hud.h create mode 100644 game/client/stargate/sg_teammenu.cpp create mode 100644 game/client/stargate/sg_teammenu.h create mode 100644 game/server/stargate/sg_player_class.cpp diff --git a/game/client/client_hl2mp.vpc b/game/client/client_hl2mp.vpc index c075efac..f4ba3124 100644 --- a/game/client/client_hl2mp.vpc +++ b/game/client/client_hl2mp.vpc @@ -14,7 +14,7 @@ $Configuration { $Compiler { - $AdditionalIncludeDirectories "$BASE;hl2mp\ui,.\hl2mp,$SRCDIR\game\shared\hl2mp,.\hl2,.\hl2\elements,$SRCDIR\game\shared\hl2,$SRCDIR\game\shared\stargate" + $AdditionalIncludeDirectories "$BASE;hl2mp\ui,.\hl2mp,$SRCDIR\game\shared\hl2mp,.\hl2,.\hl2\elements,$SRCDIR\game\shared\hl2,$SRCDIR\game\shared\stargate,.\stargate" $PreprocessorDefinitions "$BASE;HL2MP;HL2_CLIENT_DLL" } } @@ -176,6 +176,10 @@ $Project "Client (HL2MP)" $File "$SRCDIR\game\shared\stargate\sg_weapons.cpp" $File "$SRCDIR\game\shared\stargate\sg_mcp_debug_client.cpp" $File "$SRCDIR\game\shared\stargate\sg_constants.h" + $File "stargate\sg_hud.cpp" + $File "stargate\sg_hud.h" + $File "stargate\sg_teammenu.cpp" + $File "stargate\sg_teammenu.h" } } } diff --git a/game/client/hl2mp/clientmode_hl2mpnormal.cpp b/game/client/hl2mp/clientmode_hl2mpnormal.cpp index 35bfd1c1..6e88b29a 100644 --- a/game/client/hl2mp/clientmode_hl2mpnormal.cpp +++ b/game/client/hl2mp/clientmode_hl2mpnormal.cpp @@ -18,6 +18,7 @@ #include "hl2mpclientscoreboard.h" #include "hl2mptextwindow.h" #include "ienginevgui.h" +#include "stargate/sg_teammenu.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -60,6 +61,12 @@ protected: } virtual IViewPortPanel *CreatePanelByName( const char *szPanelName ); + + virtual void CreateDefaultPanels( void ) + { + BaseClass::CreateDefaultPanels(); + AddNewPanel( CreatePanelByName( PANEL_TEAM ), "PANEL_TEAM" ); + } }; int ClientModeHL2MPNormal::GetDeathMessageStartHeight( void ) @@ -83,12 +90,17 @@ IViewPortPanel* CHudViewport::CreatePanelByName( const char *szPanelName ) } else if ( Q_strcmp(PANEL_SPECGUI, szPanelName) == 0 ) { - newpanel = new CHL2MPSpectatorGUI( this ); + newpanel = new CHL2MPSpectatorGUI( this ); + return newpanel; + } + else if ( Q_strcmp( PANEL_TEAM, szPanelName ) == 0 ) + { + newpanel = new CSgTeamMenu( this ); return newpanel; } - - return BaseClass::CreatePanelByName( szPanelName ); + + return BaseClass::CreatePanelByName( szPanelName ); } //----------------------------------------------------------------------------- diff --git a/game/client/stargate/sg_hud.cpp b/game/client/stargate/sg_hud.cpp new file mode 100644 index 00000000..952db6d7 --- /dev/null +++ b/game/client/stargate/sg_hud.cpp @@ -0,0 +1,570 @@ +//========= StargateTC ============================================================// +// +// sg_hud.cpp - StargateTC custom HUD (1:1 port of the single GoldSrc client HUD +// element). Paints radar+compass+blips, vertical life/armor/stamina stat bars, +// naquada/progress bar, teammate portrait roster + local avatar, tactical alert, +// territory tickets, round timer, bomb key and the zat-stun overlay. +// +// Sprites come from the AI-enhanced VGUI materials at vgui/stargate/hud/* built +// by `stgs textures compile-hud`. Each is a 4x atlas padded to a power of two; +// DrawSpr() derives the content UV from the GoldSrc-native sprite size. +//=================================================================================// +#include "cbase.h" +#include "sg_hud.h" +#include "hud.h" +#include "hud_macros.h" +#include "iclientmode.h" +#include "c_baseplayer.h" +#include "view.h" +#include +#include +#include "vgui/IScheme.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +using namespace vgui; + +DECLARE_HUDELEMENT( CHudStargate ); + +DECLARE_HUD_MESSAGE( CHudStargate, SgAddPortrait ); +DECLARE_HUD_MESSAGE( CHudStargate, SgReset ); +DECLARE_HUD_MESSAGE( CHudStargate, SgAboutMe ); +DECLARE_HUD_MESSAGE( CHudStargate, SgTogglePor ); +DECLARE_HUD_MESSAGE( CHudStargate, SgSetRndTim ); +DECLARE_HUD_MESSAGE( CHudStargate, SgNewRnd ); +DECLARE_HUD_MESSAGE( CHudStargate, SgZatTouch ); +DECLARE_HUD_MESSAGE( CHudStargate, SgRadio ); +DECLARE_HUD_MESSAGE( CHudStargate, SgArmor ); +DECLARE_HUD_MESSAGE( CHudStargate, SgStamina ); +DECLARE_HUD_MESSAGE( CHudStargate, SgSubModel ); +DECLARE_HUD_MESSAGE( CHudStargate, SgHasKey ); +DECLARE_HUD_MESSAGE( CHudStargate, SgHideHud ); +DECLARE_HUD_MESSAGE( CHudStargate, SgHudInfoMode ); +DECLARE_HUD_MESSAGE( CHudStargate, SgTicketMode ); +DECLARE_HUD_MESSAGE( CHudStargate, SgGtickets ); +DECLARE_HUD_MESSAGE( CHudStargate, SgTtickets ); +DECLARE_HUD_MESSAGE( CHudStargate, SgProgress ); +DECLARE_HUD_MESSAGE( CHudStargate, SgBlip ); + +// GoldSrc cvars: view_radar / view_portraits. +ConVar sg_view_radar( "sg_view_radar", "1", FCVAR_ARCHIVE, "Show the StargateTC radar." ); +ConVar sg_view_portraits( "sg_view_portraits", "1", FCVAR_ARCHIVE, "Show the teammate portrait roster." ); +// Inject dummy state so every HUD block can be verified in a single screenshot. +ConVar sg_hud_test( "sg_hud_test", "0", FCVAR_CHEAT, "Force-populate all StargateTC HUD blocks for layout testing." ); + +// GoldSrc team coloring: Tau'ri side green, Goa'uld side red. +#define SG_TEAM_TAURI 2 // StargateTC: team 2 = Tau'ri +#define SG_TEAM_GOAULD 3 // StargateTC: team 3 = Goa'uld + +static const char *SG_NUM[ 10 ] = { + "vgui/stargate/hud/num/0", "vgui/stargate/hud/num/1", "vgui/stargate/hud/num/2", + "vgui/stargate/hud/num/3", "vgui/stargate/hud/num/4", "vgui/stargate/hud/num/5", + "vgui/stargate/hud/num/6", "vgui/stargate/hud/num/7", "vgui/stargate/hud/num/8", + "vgui/stargate/hud/num/9", +}; +#define SG_NUM_W 16 +#define SG_NUM_H 24 + +//----------------------------------------------------------------------------- +CHudStargate::CHudStargate( const char *pElementName ) + : CHudElement( pElementName ), BaseClass( NULL, "HudStargate" ) +{ + vgui::Panel *pParent = g_pClientMode->GetViewport(); + SetParent( pParent ); + + SetPaintBackgroundEnabled( false ); + SetPaintBorderEnabled( false ); + // We manage our own visibility; only hide while fully HUD-suppressed. + SetHiddenBits( HIDEHUD_PLAYERDEAD ); +} + +//----------------------------------------------------------------------------- +void CHudStargate::Init( void ) +{ + HOOK_HUD_MESSAGE( CHudStargate, SgAddPortrait ); + HOOK_HUD_MESSAGE( CHudStargate, SgReset ); + HOOK_HUD_MESSAGE( CHudStargate, SgAboutMe ); + HOOK_HUD_MESSAGE( CHudStargate, SgTogglePor ); + HOOK_HUD_MESSAGE( CHudStargate, SgSetRndTim ); + HOOK_HUD_MESSAGE( CHudStargate, SgNewRnd ); + HOOK_HUD_MESSAGE( CHudStargate, SgZatTouch ); + HOOK_HUD_MESSAGE( CHudStargate, SgRadio ); + HOOK_HUD_MESSAGE( CHudStargate, SgArmor ); + HOOK_HUD_MESSAGE( CHudStargate, SgStamina ); + HOOK_HUD_MESSAGE( CHudStargate, SgSubModel ); + HOOK_HUD_MESSAGE( CHudStargate, SgHasKey ); + HOOK_HUD_MESSAGE( CHudStargate, SgHideHud ); + HOOK_HUD_MESSAGE( CHudStargate, SgHudInfoMode ); + HOOK_HUD_MESSAGE( CHudStargate, SgTicketMode ); + HOOK_HUD_MESSAGE( CHudStargate, SgGtickets ); + HOOK_HUD_MESSAGE( CHudStargate, SgTtickets ); + HOOK_HUD_MESSAGE( CHudStargate, SgProgress ); + HOOK_HUD_MESSAGE( CHudStargate, SgBlip ); + + Reset(); +} + +//----------------------------------------------------------------------------- +void CHudStargate::Reset( void ) +{ + m_iLocalTeam = 0; + m_iLocalId = 0; + m_flArmor = 0; + m_flStamina = 0; + m_iSubModel = 0; + m_bHasKey = false; + m_iHideBits = 0; + m_iInfoMode = 0; + m_bTicketMode = false; + m_iGtickets = 0; + m_iTtickets = 0; + m_iProgress = 0; + m_flRoundLen = 0; + m_flRoundStart = 0; + m_bZat = false; + m_flZatTime = 0; + m_flSlideOffset = 0; + Q_memset( m_Blips, 0, sizeof( m_Blips ) ); + Q_memset( m_Portraits, 0, sizeof( m_Portraits ) ); +} + +void CHudStargate::VidInit( void ) +{ + // Texture ids are bound to the surface; drop the cache on resolution change. + m_TexIds.RemoveAll(); +} + +void CHudStargate::ApplySchemeSettings( vgui::IScheme *pScheme ) +{ + BaseClass::ApplySchemeSettings( pScheme ); + int sw, sh; + surface()->GetScreenSize( sw, sh ); + SetBounds( 0, 0, sw, sh ); +} + +//----------------------------------------------------------------------------- +bool CHudStargate::ShouldDraw( void ) +{ + if ( !engine->IsInGame() ) + return false; + // NOTE: intentionally NOT gating on CHudElement::ShouldDraw() yet - the + // player sits in an unassigned/observer limbo until the team/class join + // flow exists, which would otherwise suppress the whole HUD. Re-add the + // CHudElement::ShouldDraw() gate once join/spawn is wired (Phase 7-8). + return true; +} + +//----------------------------------------------------------------------------- +void CHudStargate::OnThink( void ) +{ + int sw, sh; + surface()->GetScreenSize( sw, sh ); + if ( GetWide() != sw || GetTall() != sh ) + SetBounds( 0, 0, sw, sh ); + + // Decay radar blips (GoldSrc: alpha -= 5 each frame). + for ( int i = 0; i < SG_MAX_BLIPS; i++ ) + { + if ( m_Blips[ i ].alpha > 0 ) + { + m_Blips[ i ].alpha -= 5; + if ( m_Blips[ i ].alpha < 0 ) + m_Blips[ i ].alpha = 0; + } + } +} + +//----------------------------------------------------------------------------- +// helpers +//----------------------------------------------------------------------------- +float CHudStargate::Scale( void ) const +{ + int sw, sh; + surface()->GetScreenSize( sw, sh ); + float s = sh / 600.0f; // GoldSrc HUD was authored ~640x480..800x600 + return ( s < 1.0f ) ? 1.0f : s; +} + +Color CHudStargate::TeamColor( int team ) const +{ + if ( team == SG_TEAM_TAURI ) + return Color( 0, 180, 0, 255 ); + if ( team == SG_TEAM_GOAULD ) + return Color( 210, 0, 0, 255 ); + return Color( 200, 200, 200, 255 ); +} + +int CHudStargate::GetTex( const char *pszMat ) +{ + int idx = m_TexIds.Find( pszMat ); + if ( idx != m_TexIds.InvalidIndex() ) + return m_TexIds[ idx ]; + int id = surface()->CreateNewTextureID(); + surface()->DrawSetTextureFile( id, pszMat, true, false ); + m_TexIds.Insert( pszMat, id ); + return id; +} + +static int SgNextPow2( int n ) { int p = 1; while ( p < n ) p <<= 1; return p; } + +void CHudStargate::DrawSpr( const char *pszMat, float sx, float sy, + int nativeW, int nativeH, const Color &c ) +{ + // content texels = native * 4 (HAT upscale); atlas padded to next pow2. + const int texW = nativeW * 4, texH = nativeH * 4; + const float u = (float)texW / (float)SgNextPow2( texW ); + const float v = (float)texH / (float)SgNextPow2( texH ); + const float s = Scale(); + const int x0 = (int)sx, y0 = (int)sy; + const int x1 = x0 + (int)( nativeW * s ), y1 = y0 + (int)( nativeH * s ); + + surface()->DrawSetColor( c ); + surface()->DrawSetTexture( GetTex( pszMat ) ); + surface()->DrawTexturedSubRect( x0, y0, x1, y1, 0.0f, 0.0f, u, v ); +} + +void CHudStargate::DrawHudNumber( int value, float x, float y, float s, const Color &c ) +{ + if ( value < 0 ) + value = 0; + char buf[ 16 ]; + Q_snprintf( buf, sizeof( buf ), "%d", value ); + float cx = x; + for ( const char *p = buf; *p; ++p ) + { + int d = *p - '0'; + if ( d >= 0 && d <= 9 ) + DrawSpr( SG_NUM[ d ], cx, y, SG_NUM_W, SG_NUM_H, c ); + cx += SG_NUM_W * s; + } +} + +//----------------------------------------------------------------------------- +void CHudStargate::Paint( void ) +{ + int sw, sh; + surface()->GetScreenSize( sw, sh ); + const float s = Scale(); + + if ( sg_hud_test.GetBool() ) + { + // Inject dummy state so every block is visible for layout verification. + m_flArmor = 70; m_flStamina = 55; m_bHasKey = true; + m_iInfoMode = 2; m_bTicketMode = true; m_iGtickets = 42; m_iTtickets = 37; + m_iProgress = 66; if ( m_flRoundLen == 0 ) { m_flRoundLen = 300; m_flRoundStart = gpGlobals->curtime; } + + // A small paint probe makes VGUI-present-but-assets-missing cases obvious. + surface()->DrawSetColor( 0, 0, 0, 180 ); + surface()->DrawFilledRect( 16, 16, 228, 88 ); + surface()->DrawSetColor( 255, 60, 40, 255 ); + surface()->DrawFilledRect( 24, 24, 220, 40 ); + surface()->DrawSetColor( 0, 190, 90, 255 ); + surface()->DrawFilledRect( 24, 48, 220, 64 ); + surface()->DrawSetColor( 60, 140, 255, 255 ); + surface()->DrawFilledRect( 24, 72, 220, 80 ); + } + + DrawFrame( sw, sh, s ); + DrawStatBars( sw, sh, s ); + if ( sg_view_radar.GetBool() ) + DrawRadar( sw, sh, s ); + DrawNaquada( sw, sh, s ); + DrawTac( sw, sh, s ); + DrawTickets( sw, sh, s ); + DrawRoundTimer( sw, sh, s ); + DrawBombKey( sw, sh, s ); + if ( sg_view_portraits.GetBool() ) + DrawPortraits( sw, sh, s ); + DrawZatOverlay( sw, sh, s ); +} + +//----------------------------------------------------------------------------- +// sub-blocks +//----------------------------------------------------------------------------- +void CHudStargate::DrawFrame( int sw, int sh, float s ) +{ + // Bottom-left and bottom-right HUD chrome (hud_left 200x168, Hud_Right 200x168). + const Color white( 255, 255, 255, 255 ); + DrawSpr( "vgui/stargate/hud/hud_left", 0, sh - (int)( 168 * s ), 200, 168, white ); + DrawSpr( "vgui/stargate/hud/Hud_Right", sw - (int)( 200 * s ), sh - (int)( 168 * s ), 200, 168, white ); +} + +void CHudStargate::DrawRadar( int sw, int sh, float s ) +{ + C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer(); + int team = pPlayer ? pPlayer->GetTeamNumber() : m_iLocalTeam; + Color disk = ( team == SG_TEAM_TAURI ) ? Color( 0, 160, 0, 255 ) : Color( 160, 0, 0, 255 ); + + // Disk: top-right, 200x200 native. y shifts down when portraits present. + bool hasPortraits = false; + for ( int i = 0; i < SG_MAX_PORTRAITS; i++ ) if ( m_Portraits[ i ].used ) { hasPortraits = true; break; } + const int rx = sw - (int)( 208 * s ); + const int ry = (int)( ( hasPortraits ? 112 : 8 ) * s ); + DrawSpr( "vgui/stargate/hud/radar", rx, ry, 200, 200, disk ); + + // Compass needle: 16-frame ladder driven by view yaw, centered on the disk. + float yaw = pPlayer ? pPlayer->EyeAngles().y : 0.0f; + yaw = anglemod( yaw ); + int frame = (int)( ( yaw / 360.0f ) * 16.0f ) & 15; + frame = ( frame + 3 ) & 15; + frame = 15 - frame; + char needle[ 64 ]; + Q_snprintf( needle, sizeof( needle ), "vgui/stargate/hud/needle_%03d", frame ); + const int ncx = rx + (int)( ( 100 - 16 ) * s ); // disk center - half needle + const int ncy = ry + (int)( ( 100 - 16 ) * s ); + DrawSpr( needle, ncx, ncy, 32, 32, Color( 210, disk.g(), 0, 255 ) ); + + // Blips (decaying), positioned relative to disk centre. + const int cx = rx + (int)( 100 * s ), cy = ry + (int)( 100 * s ); + for ( int i = 0; i < SG_MAX_BLIPS; i++ ) + { + if ( m_Blips[ i ].alpha <= 0 ) + continue; + int bf = m_Blips[ i ].type % 6; if ( bf < 0 ) bf += 6; + char blip[ 64 ]; + Q_snprintf( blip, sizeof( blip ), "vgui/stargate/hud/blip_%03d", bf ); + const int bx = cx - (int)( m_Blips[ i ].x * s ) - (int)( 8 * s ); + const int by = cy + (int)( m_Blips[ i ].y * s ) - (int)( 8 * s ); + DrawSpr( blip, bx, by, 16, 16, Color( 255, 255, 255, m_Blips[ i ].alpha ) ); + } +} + +void CHudStargate::DrawStatBars( int sw, int sh, float s ) +{ + C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer(); + const float maxv = 100.0f; + float life = pPlayer ? (float)pPlayer->GetHealth() : 0.0f; + const int barY = sh - (int)( 24 * s ); + const int barH = 64; + + // Vertical bars crop from the top by (max-value)/max - statbar.spr is 16x64. + struct { const char *mat; float val; int x; Color c; } bars[] = { + { "vgui/stargate/hud/statbar", life, 4, Color( 0, 220, 0, 255 ) }, + { "vgui/stargate/hud/statbar", m_flArmor, 28, Color( 80, 160, 255, 255 ) }, + { "vgui/stargate/hud/statbar", m_flStamina, 52, Color( 255, 170, 0, 255 ) }, + }; + for ( int i = 0; i < 3; i++ ) + { + float val = bars[ i ].val; + // life (i==0) draws even at full/empty; armor/stamina only when > 0. + if ( i > 0 && val <= 0 ) + continue; + float frac = val / maxv; if ( frac < 0 ) frac = 0; if ( frac > 1 ) frac = 1; + int filledH = (int)( barH * frac ); + int topGap = barH - filledH; // empty portion at top + // UV: statbar is 16x64 native -> 64x256 atlas, pow2 64x256, u=v=1. + const int texW = 16 * 4, texH = 64 * 4; + float u = (float)texW / SgNextPow2( texW ); + float vTop = (float)topGap / 64.0f * ( (float)texH / SgNextPow2( texH ) ); + float vBot = (float)texH / SgNextPow2( texH ); + int x0 = bars[ i ].x; + int y0 = barY + (int)( topGap * s ); + int x1 = x0 + (int)( 16 * s ); + int y1 = barY + (int)( barH * s ); + surface()->DrawSetColor( bars[ i ].c ); + surface()->DrawSetTexture( GetTex( bars[ i ].mat ) ); + surface()->DrawTexturedSubRect( x0, y0, x1, y1, 0.0f, vTop, u, vBot ); + } +} + +void CHudStargate::DrawNaquada( int sw, int sh, float s ) +{ + if ( m_iProgress <= 0 || m_iProgress >= 100 ) + return; + const int bx = sw / 2 - (int)( 71 * s ); + const int by = sh - sh / 4 - (int)( 7 * s ); + DrawSpr( "vgui/stargate/hud/pbar", bx, by, 128, 32, Color( 255, 255, 255, 255 ) ); + // Fill width scaled by progress. + float frac = m_iProgress / 100.0f; + const int fx = sw / 2 - (int)( 64 * s ); + int fullW = 144, drawW = (int)( fullW * frac ); + const int texW = 144 * 4, texH = 48 * 4; + float uFull = (float)texW / SgNextPow2( texW ); + float v = (float)texH / SgNextPow2( texH ); + float u = uFull * frac; + surface()->DrawSetColor( Color( (int)( 255 * ( 1 - frac ) ), (int)( 255 * frac ), 40, 255 ) ); + surface()->DrawSetTexture( GetTex( "vgui/stargate/hud/pbare" ) ); + surface()->DrawTexturedSubRect( fx, by, fx + (int)( drawW * s ), by + (int)( 48 * s ), 0.0f, 0.0f, u, v ); +} + +void CHudStargate::DrawTac( int sw, int sh, float s ) +{ + if ( m_iInfoMode <= 0 ) + return; + const char *mat = "vgui/stargate/hud/tac_green"; + if ( m_iInfoMode == 1 ) mat = "vgui/stargate/hud/tac_red"; + else if ( m_iInfoMode == 2 ) mat = "vgui/stargate/hud/tac_yellow"; + DrawSpr( mat, (int)( 115 * s ), sh - (int)( 76 * s ), 64, 8, Color( 200, 200, 200, 255 ) ); +} + +void CHudStargate::DrawTickets( int sw, int sh, float s ) +{ + if ( !m_bTicketMode ) + return; + DrawSpr( "vgui/stargate/hud/ticketdock", (int)( 120 * s ), sh - (int)( 37 * s ), 120, 64, Color( 200, 200, 200, 255 ) ); + DrawHudNumber( m_iGtickets, (int)( 70 * s ), sh - (int)( 37 * s ), s, Color( 210, 0, 0, 255 ) ); + DrawHudNumber( m_iTtickets, (int)( 130 * s ), sh - (int)( 37 * s ), s, Color( 0, 180, 0, 255 ) ); +} + +void CHudStargate::DrawRoundTimer( int sw, int sh, float s ) +{ + if ( m_flRoundLen <= 0 ) + return; + int remaining = (int)( m_flRoundLen - ( gpGlobals->curtime - m_flRoundStart ) ); + if ( remaining < 0 ) remaining = 0; + int mins = remaining / 60, secs = remaining % 60; + const Color cyan( 168, 211, 255, 255 ); + float cx = sw / 2 - (int)( 28 * s ); + float cy = (int)( sh / 10 ); + DrawHudNumber( mins, cx, cy, s, cyan ); + DrawSpr( "vgui/stargate/hud/dots", cx + (int)( SG_NUM_W * 2 * s ), cy, 8, 24, cyan ); + // seconds, zero-padded + char sb[ 8 ]; Q_snprintf( sb, sizeof( sb ), "%02d", secs ); + float dx = cx + (int)( ( SG_NUM_W * 2 + 8 ) * s ); + for ( const char *p = sb; *p; ++p ) + { + DrawSpr( SG_NUM[ *p - '0' ], dx, cy, SG_NUM_W, SG_NUM_H, cyan ); + dx += SG_NUM_W * s; + } +} + +void CHudStargate::DrawBombKey( int sw, int sh, float s ) +{ + if ( !m_bHasKey ) + return; + DrawSpr( "vgui/stargate/hud/key", (int)( 75 * s ), sh - (int)( 52 * s ), 72, 48, Color( 200, 200, 200, 255 ) ); +} + +void CHudStargate::DrawPortraits( int sw, int sh, float s ) +{ + // Bottom-right teammate avatar column (Portrait_middel cells, 80px wide). + int n = 0; + for ( int i = 0; i < SG_MAX_PORTRAITS; i++ ) + { + if ( !m_Portraits[ i ].used ) + continue; + int px = sw - (int)( ( ( n + 1 ) * 80 + 16 ) * s ); + int py = sh - (int)( 120 * s ); + DrawSpr( "vgui/stargate/hud/Portrait_middel", px, py, 80, 104, TeamColor( m_Portraits[ i ].team ) ); + if ( m_Portraits[ i ].avatar[ 0 ] ) + { + char av[ 96 ]; + Q_snprintf( av, sizeof( av ), "vgui/stargate/avatars/%s", m_Portraits[ i ].avatar ); + DrawSpr( av, px + (int)( 8 * s ), py + (int)( 8 * s ), 64, 64, Color( 255, 255, 255, 255 ) ); + } + n++; + } +} + +void CHudStargate::DrawZatOverlay( int sw, int sh, float s ) +{ + if ( !m_bZat ) + return; + float age = gpGlobals->curtime - m_flZatTime; + if ( age > 1.5f ) { m_bZat = false; return; } + int a = (int)( 200 * ( 1.0f - age / 1.5f ) ); + surface()->DrawSetColor( Color( 0, 130, 255, a ) ); + surface()->DrawSetTexture( GetTex( "vgui/stargate/weapons/d_zap" ) ); + surface()->DrawTexturedRect( 0, 0, sw, sh ); +} + +//----------------------------------------------------------------------------- +// message handlers +//----------------------------------------------------------------------------- +void CHudStargate::MsgFunc_SgArmor( bf_read &msg ) { m_flArmor = msg.ReadShort(); } +void CHudStargate::MsgFunc_SgStamina( bf_read &msg ) { m_flStamina = msg.ReadShort(); } +void CHudStargate::MsgFunc_SgSubModel( bf_read &msg ) { m_iSubModel = msg.ReadShort(); } +void CHudStargate::MsgFunc_SgHasKey( bf_read &msg ) { m_bHasKey = msg.ReadShort() != 0; } +void CHudStargate::MsgFunc_SgHideHud( bf_read &msg ) { m_iHideBits = msg.ReadShort(); } +void CHudStargate::MsgFunc_SgHudInfoMode( bf_read &msg ){ m_iInfoMode = msg.ReadByte(); } +void CHudStargate::MsgFunc_SgTicketMode( bf_read &msg ) { m_bTicketMode = msg.ReadByte() != 0; } +void CHudStargate::MsgFunc_SgGtickets( bf_read &msg ) { m_iGtickets = msg.ReadByte(); } +void CHudStargate::MsgFunc_SgTtickets( bf_read &msg ) { m_iTtickets = msg.ReadByte(); } +void CHudStargate::MsgFunc_SgTogglePor( bf_read &msg ) { sg_view_portraits.SetValue( !sg_view_portraits.GetBool() ); } +void CHudStargate::MsgFunc_SgNewRnd( bf_read &msg ) { m_iSubModel = 0; Q_memset( m_Portraits, 0, sizeof( m_Portraits ) ); } +void CHudStargate::MsgFunc_SgRadio( bf_read &msg ) { msg.ReadByte(); msg.ReadByte(); /* TODO radio text */ } + +void CHudStargate::MsgFunc_SgAboutMe( bf_read &msg ) +{ + m_iLocalId = msg.ReadShort(); + m_iLocalTeam = msg.ReadShort(); +} + +void CHudStargate::MsgFunc_SgSetRndTim( bf_read &msg ) +{ + m_flRoundLen = msg.ReadShort(); + msg.ReadByte(); + m_flRoundStart = gpGlobals->curtime; +} + +void CHudStargate::MsgFunc_SgZatTouch( bf_read &msg ) +{ + m_bZat = true; + m_flZatTime = gpGlobals->curtime; +} + +void CHudStargate::MsgFunc_SgProgress( bf_read &msg ) +{ + m_iProgress = msg.ReadByte(); + // remaining payload (flags + 28-entry array + trailer) consumed but unused. + int remaining = msg.GetNumBytesLeft(); + for ( int i = 0; i < remaining; i++ ) + msg.ReadByte(); +} + +void CHudStargate::MsgFunc_SgBlip( bf_read &msg ) +{ + int cmd = msg.ReadByte(); + float x = msg.ReadShort(); + float y = msg.ReadShort(); + if ( cmd == '=' ) // 0x3d - clear origin reference + return; + // add a blip into the first free/decayed slot + for ( int i = 0; i < SG_MAX_BLIPS; i++ ) + { + if ( m_Blips[ i ].alpha <= 0 ) + { + m_Blips[ i ].type = ( cmd == '2' ) ? 1 : 0; + m_Blips[ i ].x = x; + m_Blips[ i ].y = y; + m_Blips[ i ].alpha = 255; + return; + } + } +} + +void CHudStargate::MsgFunc_SgReset( bf_read &msg ) +{ + int id = msg.ReadShort(); + int flag = msg.ReadByte(); + if ( flag == 0 ) + { + Q_memset( m_Portraits, 0, sizeof( m_Portraits ) ); + return; + } + for ( int i = 0; i < SG_MAX_PORTRAITS; i++ ) + if ( m_Portraits[ i ].used && m_Portraits[ i ].id == id ) + m_Portraits[ i ].used = false; +} + +void CHudStargate::MsgFunc_SgAddPortrait( bf_read &msg ) +{ + int id = msg.ReadShort(); + int team = msg.ReadShort(); + char avatar[ 32 ]; + msg.ReadString( avatar, sizeof( avatar ) ); + // find existing or free slot + int slot = -1; + for ( int i = 0; i < SG_MAX_PORTRAITS; i++ ) + { + if ( m_Portraits[ i ].used && m_Portraits[ i ].id == id ) { slot = i; break; } + if ( slot < 0 && !m_Portraits[ i ].used ) slot = i; + } + if ( slot < 0 ) + return; + m_Portraits[ slot ].used = true; + m_Portraits[ slot ].id = id; + m_Portraits[ slot ].team = team; + Q_strncpy( m_Portraits[ slot ].avatar, avatar, sizeof( m_Portraits[ slot ].avatar ) ); +} diff --git a/game/client/stargate/sg_hud.h b/game/client/stargate/sg_hud.h new file mode 100644 index 00000000..1ddc62a2 --- /dev/null +++ b/game/client/stargate/sg_hud.h @@ -0,0 +1,128 @@ +//========= StargateTC ============================================================// +// +// sg_hud.h - StargateTC custom HUD element. +// +// 1:1 port of the single large GoldSrc client HUD element (decompiled +// FUN_100718df / Init FUN_10071193 / VidInit FUN_10071390, singleton +// &DAT_100e67f0). The original drew ALL Stargate-specific HUD from one element; +// we keep that structure here as one full-screen CHudElement that paints every +// sub-block with ScreenWidth/Height-relative coordinates, matching the original. +// +// See StargateTCSource/docs/HUD_PORT_SPEC.md for the full element/message map. +//=================================================================================// +#ifndef SG_HUD_H +#define SG_HUD_H +#ifdef _WIN32 +#pragma once +#endif + +#include "hudelement.h" +#include +#include "tier1/UtlDict.h" + +#define SG_MAX_BLIPS 256 +#define SG_MAX_PORTRAITS 33 + +struct SgBlip_t +{ + int type; + float x; // world-relative offset supplied by server + float y; + int alpha; // 0..255, decays each frame +}; + +struct SgPortrait_t +{ + bool used; + int id; // player entindex + int team; + char avatar[32]; +}; + +//----------------------------------------------------------------------------- +// CHudStargate - the whole Stargate HUD in one element (faithful to GoldSrc). +//----------------------------------------------------------------------------- +class CHudStargate : public CHudElement, public vgui::Panel +{ + DECLARE_CLASS_SIMPLE( CHudStargate, vgui::Panel ); + +public: + explicit CHudStargate( const char *pElementName ); + + virtual void Init( void ); + virtual void VidInit( void ); + virtual void Reset( void ); + virtual bool ShouldDraw( void ); + virtual void OnThink( void ); + virtual void Paint( void ); + virtual void ApplySchemeSettings( vgui::IScheme *pScheme ); + + // --- Server user messages (19, prefixed Sg to avoid HL2MP clashes) --- + void MsgFunc_SgAddPortrait( bf_read &msg ); + void MsgFunc_SgReset( bf_read &msg ); + void MsgFunc_SgAboutMe( bf_read &msg ); + void MsgFunc_SgTogglePor( bf_read &msg ); + void MsgFunc_SgSetRndTim( bf_read &msg ); + void MsgFunc_SgNewRnd( bf_read &msg ); + void MsgFunc_SgZatTouch( bf_read &msg ); + void MsgFunc_SgRadio( bf_read &msg ); + void MsgFunc_SgArmor( bf_read &msg ); + void MsgFunc_SgStamina( bf_read &msg ); + void MsgFunc_SgSubModel( bf_read &msg ); + void MsgFunc_SgHasKey( bf_read &msg ); + void MsgFunc_SgHideHud( bf_read &msg ); + void MsgFunc_SgHudInfoMode( bf_read &msg ); + void MsgFunc_SgTicketMode( bf_read &msg ); + void MsgFunc_SgGtickets( bf_read &msg ); + void MsgFunc_SgTtickets( bf_read &msg ); + void MsgFunc_SgProgress( bf_read &msg ); + void MsgFunc_SgBlip( bf_read &msg ); + +private: + // texture cache + sprite draw helpers + int GetTex( const char *pszMat ); + // Draw a HUD sprite sized in GoldSrc-native px (auto pow2-UV from 4x atlas). + void DrawSpr( const char *pszMat, float sx, float sy, + int nativeW, int nativeH, const Color &c ); + float Scale( void ) const; // screen-height based HUD scale + Color TeamColor( int team ) const; + + // draw sub-blocks + void DrawFrame( int sw, int sh, float s ); + void DrawRadar( int sw, int sh, float s ); + void DrawStatBars( int sw, int sh, float s ); + void DrawNaquada( int sw, int sh, float s ); + void DrawTickets( int sw, int sh, float s ); + void DrawRoundTimer( int sw, int sh, float s ); + void DrawTac( int sw, int sh, float s ); + void DrawBombKey( int sw, int sh, float s ); + void DrawPortraits( int sw, int sh, float s ); + void DrawZatOverlay( int sw, int sh, float s ); + void DrawHudNumber( int value, float x, float y, float s, const Color &c ); + + CUtlDict< int, int > m_TexIds; // material name -> surface texture id + + // --- HUD state (mirrors the GoldSrc element's fields) --- + int m_iLocalTeam; + int m_iLocalId; + float m_flArmor; + float m_flStamina; + int m_iSubModel; + bool m_bHasKey; + int m_iHideBits; + int m_iInfoMode; // 0 none / 1 red / 2 yellow / 3 green + bool m_bTicketMode; + int m_iGtickets; + int m_iTtickets; + int m_iProgress; // 0..100 + float m_flRoundLen; + float m_flRoundStart; + bool m_bZat; + float m_flZatTime; + float m_flSlideOffset; + + SgBlip_t m_Blips[ SG_MAX_BLIPS ]; + SgPortrait_t m_Portraits[ SG_MAX_PORTRAITS ]; +}; + +#endif // SG_HUD_H diff --git a/game/client/stargate/sg_teammenu.cpp b/game/client/stargate/sg_teammenu.cpp new file mode 100644 index 00000000..db7b4704 --- /dev/null +++ b/game/client/stargate/sg_teammenu.cpp @@ -0,0 +1,275 @@ +//========= StargateTC ============================================================// +// +// sg_teammenu.cpp - combined team + class selection panel. +// See sg_teammenu.h. Buttons are built in code (no .res dependency) and laid out +// in PerformLayout, so the panel is self-contained. +//=================================================================================// +#include "cbase.h" +#include "sg_teammenu.h" +#include +#include +#include +#include +#include +#include +#include + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +using namespace vgui; + +// StargateTC team numbering (matches server: 2 = Tau'ri, 3 = Goa'uld). +#define SG_TEAM_AUTO 0 +#define SG_TEAM_SPECTATOR 1 +#define SG_TEAM_TAURI 2 +#define SG_TEAM_GOAULD 3 + +struct SgClassDef_t { const char *cmd; const char *label; }; + +// Class rosters recovered from the GoldSrc panel (FUN_1002bec0 / Titles.txt). +static const SgClassDef_t s_TauClasses[] = { + { "oneill", "O'Neill" }, + { "carter", "Carter" }, + { "jackson", "Jackson" }, + { "tealc", "Teal'c" }, + { "soldat", "Soldier" }, +}; +static const SgClassDef_t s_GoaClasses[] = { + { "dieu", "System Lord" }, + { "anubis", "Anubis Guard" }, + { "jaffa", "Jaffa" }, + { "prima", "First Prime" }, + { "esclave", "Horus Guard" }, +}; + +static void SgStyleButton( Button *pButton, const Color &fg, const Color &bg ) +{ + if ( !pButton ) + return; + + pButton->SetButtonBorderEnabled( true ); + pButton->SetDefaultColor( fg, bg ); + pButton->SetArmedColor( Color( 255, 255, 255, 255 ), Color( 45, 94, 110, 245 ) ); + pButton->SetDepressedColor( Color( 255, 255, 255, 255 ), Color( 23, 58, 72, 255 ) ); + pButton->SetSelectedColor( fg, Color( 29, 68, 82, 245 ) ); +} + +//----------------------------------------------------------------------------- +CSgTeamMenu::CSgTeamMenu( IViewPort *pViewPort ) : Frame( NULL, PANEL_TEAM ) +{ + m_pViewPort = pViewPort; + + SetScheme( "ClientScheme" ); + SetTitle( "", true ); + SetMoveable( false ); + SetSizeable( false ); + SetTitleBarVisible( false ); + SetProportional( false ); + SetPaintBackgroundEnabled( true ); + SetPaintBorderEnabled( false ); + + m_pTitle = new Label( this, "Title", "Identify yourself" ); + m_pTauHeader = new Label( this, "TauHeader", "Tau'ri" ); + m_pGoaHeader = new Label( this, "GoaHeader", "Goa'uld" ); + + for ( int i = 0; i < ARRAYSIZE( s_TauClasses ); i++ ) + m_TauButtons.AddToTail( AddClassButton( SG_TEAM_TAURI, s_TauClasses[i].cmd, s_TauClasses[i].label ) ); + for ( int i = 0; i < ARRAYSIZE( s_GoaClasses ); i++ ) + m_GoaButtons.AddToTail( AddClassButton( SG_TEAM_GOAULD, s_GoaClasses[i].cmd, s_GoaClasses[i].label ) ); + + m_BottomButtons.AddToTail( AddTeamButton( SG_TEAM_AUTO, "Auto-Assign", "auto" ) ); + m_BottomButtons.AddToTail( AddTeamButton( SG_TEAM_SPECTATOR, "Spectate", "spec" ) ); + m_BottomButtons.AddToTail( AddTeamButton( -1, "Cancel", "cancel" ) ); + + SetVisible( false ); +} + +CSgTeamMenu::~CSgTeamMenu() +{ +} + +//----------------------------------------------------------------------------- +vgui::Button *CSgTeamMenu::AddClassButton( int teamId, const char *cls, const char *label ) +{ + Button *b = new Button( this, cls, label ); + KeyValues *kv = new KeyValues( "JoinClass" ); + kv->SetInt( "team", teamId ); + kv->SetString( "cls", cls ); + b->SetCommand( kv ); + b->AddActionSignalTarget( this ); + return b; +} + +vgui::Button *CSgTeamMenu::AddTeamButton( int teamId, const char *label, const char *key ) +{ + Button *b = new Button( this, key, label ); + KeyValues *kv = new KeyValues( "JoinTeam" ); + kv->SetInt( "team", teamId ); + b->SetCommand( kv ); + b->AddActionSignalTarget( this ); + return b; +} + +//----------------------------------------------------------------------------- +void CSgTeamMenu::ApplySchemeSettings( vgui::IScheme *pScheme ) +{ + BaseClass::ApplySchemeSettings( pScheme ); + SetBgColor( Color( 5, 9, 12, 238 ) ); + + m_pTitle->SetFgColor( Color( 235, 243, 235, 255 ) ); + m_pTauHeader->SetFgColor( Color( 90, 220, 130, 255 ) ); + m_pGoaHeader->SetFgColor( Color( 235, 88, 78, 255 ) ); + + for ( int i = 0; i < m_TauButtons.Count(); i++ ) + SgStyleButton( m_TauButtons[i], Color( 210, 255, 220, 255 ), Color( 16, 46, 30, 235 ) ); + for ( int i = 0; i < m_GoaButtons.Count(); i++ ) + SgStyleButton( m_GoaButtons[i], Color( 255, 220, 210, 255 ), Color( 54, 25, 22, 235 ) ); + for ( int i = 0; i < m_BottomButtons.Count(); i++ ) + SgStyleButton( m_BottomButtons[i], Color( 230, 235, 230, 255 ), Color( 32, 39, 43, 235 ) ); +} + +//----------------------------------------------------------------------------- +void CSgTeamMenu::PaintBackground( void ) +{ + int w, h; + GetSize( w, h ); + + surface()->DrawSetColor( 5, 9, 12, 238 ); + surface()->DrawFilledRect( 0, 0, w, h ); + + surface()->DrawSetColor( 76, 155, 118, 255 ); + surface()->DrawOutlinedRect( 0, 0, w, h ); + surface()->DrawSetColor( 18, 57, 36, 210 ); + surface()->DrawFilledRect( 22, 44, w / 2 - 10, h - 58 ); + surface()->DrawSetColor( 76, 32, 28, 210 ); + surface()->DrawFilledRect( w / 2 + 10, 44, w - 22, h - 58 ); + surface()->DrawSetColor( 28, 37, 40, 235 ); + surface()->DrawFilledRect( 22, h - 54, w - 22, h - 16 ); +} + +//----------------------------------------------------------------------------- +void CSgTeamMenu::PerformLayout( void ) +{ + BaseClass::PerformLayout(); + + int sw, sh; + surface()->GetScreenSize( sw, sh ); + const int w = 460, h = 360; + const int x = ( sw - w ) / 2, y = ( sh - h ) / 2; + SetBounds( x, y, w, h ); + + m_pTitle->SetBounds( 0, 12, w, 24 ); + m_pTitle->SetContentAlignment( Label::a_center ); + + const int colW = 180, btnH = 26, gap = 6; + const int leftX = 30, rightX = w - 30 - colW; + int hy = 48; + m_pTauHeader->SetBounds( leftX, hy, colW, 22 ); + m_pTauHeader->SetContentAlignment( Label::a_center ); + m_pGoaHeader->SetBounds( rightX, hy, colW, 22 ); + m_pGoaHeader->SetContentAlignment( Label::a_center ); + + int by = hy + 28; + for ( int i = 0; i < m_TauButtons.Count(); i++ ) + m_TauButtons[i]->SetBounds( leftX, by + i * ( btnH + gap ), colW, btnH ); + for ( int i = 0; i < m_GoaButtons.Count(); i++ ) + m_GoaButtons[i]->SetBounds( rightX, by + i * ( btnH + gap ), colW, btnH ); + + // bottom row: auto / spectate / cancel + int bottomY = h - 40; + int n = m_BottomButtons.Count(); + int bw = 130, totalW = n * bw + ( n - 1 ) * gap; + int bx = ( w - totalW ) / 2; + for ( int i = 0; i < n; i++ ) + m_BottomButtons[i]->SetBounds( bx + i * ( bw + gap ), bottomY, bw, btnH ); +} + +//----------------------------------------------------------------------------- +void CSgTeamMenu::ShowPanel( bool bShow ) +{ + if ( BaseClass::IsVisible() == bShow ) + return; + + if ( bShow ) + { + Activate(); + SetVisible( true ); + SetMouseInputEnabled( true ); + SetKeyBoardInputEnabled( true ); + InvalidateLayout(); + } + else + { + SetVisible( false ); + SetMouseInputEnabled( false ); + } + + if ( m_pViewPort ) + m_pViewPort->ShowBackGround( bShow ); +} + +//----------------------------------------------------------------------------- +void CSgTeamMenu::OnJoinClass( KeyValues *data ) +{ + int team = data->GetInt( "team" ); + const char *cls = data->GetString( "cls" ); + char cmd[ 128 ]; + Q_snprintf( cmd, sizeof( cmd ), "jointeam %d; joinclass %s\n", team, cls ); + engine->ClientCmd( cmd ); + ShowPanel( false ); +} + +void CSgTeamMenu::OnJoinTeam( KeyValues *data ) +{ + int team = data->GetInt( "team" ); + if ( team < 0 ) // Cancel + { + ShowPanel( false ); + return; + } + char cmd[ 96 ]; + if ( team == SG_TEAM_AUTO ) + Q_snprintf( cmd, sizeof( cmd ), "jointeam 0; joinclass auto\n" ); + else + Q_snprintf( cmd, sizeof( cmd ), "jointeam %d\n", team ); + engine->ClientCmd( cmd ); + ShowPanel( false ); +} + +void CSgTeamMenu::OnKeyCodePressed( vgui::KeyCode code ) +{ + if ( code == KEY_ESCAPE ) + { + ShowPanel( false ); + return; + } + BaseClass::OnKeyCodePressed( code ); +} + +//----------------------------------------------------------------------------- +// Console commands to open the menu (also bind a key, e.g. bind m chooseteam). +//----------------------------------------------------------------------------- +CON_COMMAND( chooseteam, "Open the StargateTC team/class selection menu" ) +{ + if ( gViewPortInterface ) + gViewPortInterface->ShowPanel( PANEL_TEAM, true ); +} + +CON_COMMAND( changeclass, "Open the StargateTC class selection menu" ) +{ + if ( gViewPortInterface ) + gViewPortInterface->ShowPanel( PANEL_TEAM, true ); +} + +CON_COMMAND( showmenu, "Open the StargateTC team/class selection menu" ) +{ + if ( gViewPortInterface ) + gViewPortInterface->ShowPanel( PANEL_TEAM, true ); +} + +CON_COMMAND( showequip, "Open the StargateTC equipment/class selection menu" ) +{ + if ( gViewPortInterface ) + gViewPortInterface->ShowPanel( PANEL_TEAM, true ); +} diff --git a/game/client/stargate/sg_teammenu.h b/game/client/stargate/sg_teammenu.h new file mode 100644 index 00000000..6f4e6c27 --- /dev/null +++ b/game/client/stargate/sg_teammenu.h @@ -0,0 +1,68 @@ +//========= StargateTC ============================================================// +// +// sg_teammenu.h - combined team + class selection panel (IViewPortPanel). +// +// 1:1 port of the GoldSrc "Identifiez-vous" panel (decompiled FUN_1002bec0): +// two team columns (Tau'ri / Goa'uld), five classes each, plus Auto-Assign, +// Spectate and Cancel. Class buttons issue `jointeam ` + `joinclass `. +// Opened by the `chooseteam` / `changeclass` console commands or server VGUIMenu. +//=================================================================================// +#ifndef SG_TEAMMENU_H +#define SG_TEAMMENU_H +#ifdef _WIN32 +#pragma once +#endif + +#include +#include +#include +#include +#include + +class CSgTeamMenu : public vgui::Frame, public IViewPortPanel +{ + DECLARE_CLASS_SIMPLE( CSgTeamMenu, vgui::Frame ); + +public: + explicit CSgTeamMenu( IViewPort *pViewPort ); + virtual ~CSgTeamMenu(); + + // IViewPortPanel + virtual const char *GetName( void ) { return PANEL_TEAM; } + virtual void SetData( KeyValues *data ) {} + virtual void Reset( void ) {} + virtual void Update( void ) {} + virtual bool NeedsUpdate( void ) { return false; } + virtual bool HasInputElements( void ) { return true; } + virtual void ShowPanel( bool bShow ); + + // both vgui::Frame and IViewPortPanel define these -> passthroughs + vgui::VPANEL GetVPanel( void ) { return BaseClass::GetVPanel(); } + virtual bool IsVisible() { return BaseClass::IsVisible(); } + virtual void SetParent( vgui::VPANEL parent ) { BaseClass::SetParent( parent ); } + virtual GameActionSet_t GetPreferredActionSet() { return GAME_ACTION_SET_IN_GAME_HUD; } + +protected: + virtual void ApplySchemeSettings( vgui::IScheme *pScheme ); + virtual void PaintBackground( void ); + virtual void PerformLayout( void ); + virtual void OnKeyCodePressed( vgui::KeyCode code ); + + MESSAGE_FUNC_PARAMS( OnJoinClass, "JoinClass", data ); + MESSAGE_FUNC_PARAMS( OnJoinTeam, "JoinTeam", data ); + +private: + vgui::Button *AddClassButton( int teamId, const char *cls, const char *label ); + vgui::Button *AddTeamButton( int teamId, const char *label, const char *key ); + + IViewPort *m_pViewPort; + + vgui::Label *m_pTitle; + vgui::Label *m_pTauHeader; + vgui::Label *m_pGoaHeader; + CUtlVector< vgui::Button * > m_TauButtons; + CUtlVector< vgui::Button * > m_GoaButtons; + CUtlVector< vgui::Button * > m_BottomButtons; +}; + +#endif // SG_TEAMMENU_H diff --git a/game/server/hl2mp/hl2mp_client.cpp b/game/server/hl2mp/hl2mp_client.cpp index 571fbb85..20e9924f 100644 --- a/game/server/hl2mp/hl2mp_client.cpp +++ b/game/server/hl2mp/hl2mp_client.cpp @@ -35,12 +35,21 @@ void Host_Say( edict_t *pEdict, bool teamonly ); ConVar sv_motd_unload_on_dismissal( "sv_motd_unload_on_dismissal", "0", 0, "If enabled, the MOTD contents will be unloaded when the player closes the MOTD." ); +// StargateTC: the MOTD info panel popped over the screen on every join and the +// mod ships no MOTD content, so it just obscured the game. Default it OFF; +// set "sv_show_motd 1" to restore the vanilla HL2DM message-of-the-day panel. +ConVar sv_show_motd( "sv_show_motd", "0", FCVAR_NOTIFY, "Show the MOTD info panel to players when they join." ); + extern CBaseEntity* FindPickerEntityClass( CBasePlayer *pPlayer, char *classname ); extern bool g_fGameOver; void FinishClientPutInServer( CHL2MP_Player *pPlayer ) { pPlayer->InitialSpawn(); + + extern void Sg_ResetPlayerClass( CHL2MP_Player *pPlayer ); + Sg_ResetPlayerClass( pPlayer ); + pPlayer->Spawn(); @@ -61,20 +70,24 @@ void FinishClientPutInServer( CHL2MP_Player *pPlayer ) if ( HL2MPRules()->IsTeamplay() == true ) { ClientPrint( pPlayer, HUD_PRINTTALK, "You are on team %s1\n", pPlayer->GetTeam()->GetName() ); + pPlayer->ShowViewPortPanel( PANEL_TEAM, true ); } - const ConVar *hostname = cvar->FindVar( "hostname" ); - const char *title = (hostname) ? hostname->GetString() : "MESSAGE OF THE DAY"; + if ( sv_show_motd.GetBool() ) + { + const ConVar *hostname = cvar->FindVar( "hostname" ); + const char *title = (hostname) ? hostname->GetString() : "MESSAGE OF THE DAY"; - KeyValues *data = new KeyValues("data"); - data->SetString( "title", title ); // info panel title - data->SetString( "type", "1" ); // show userdata from stringtable entry - data->SetString( "msg", "motd" ); // use this stringtable entry - data->SetBool( "unload", sv_motd_unload_on_dismissal.GetBool() ); + KeyValues *data = new KeyValues("data"); + data->SetString( "title", title ); // info panel title + data->SetString( "type", "1" ); // show userdata from stringtable entry + data->SetString( "msg", "motd" ); // use this stringtable entry + data->SetBool( "unload", sv_motd_unload_on_dismissal.GetBool() ); - pPlayer->ShowViewPortPanel( PANEL_INFO, true, data ); + pPlayer->ShowViewPortPanel( PANEL_INFO, true, data ); - data->deleteThis(); + data->deleteThis(); + } } /* diff --git a/game/server/hl2mp/hl2mp_player.cpp b/game/server/hl2mp/hl2mp_player.cpp index bc1412ac..485d8703 100644 --- a/game/server/hl2mp/hl2mp_player.cpp +++ b/game/server/hl2mp/hl2mp_player.cpp @@ -20,6 +20,7 @@ #include "grenade_satchel.h" #include "eventqueue.h" #include "gamestats.h" +#include "viewport_panel_names.h" #include "engine/IEngineSound.h" #include "SoundEmitterSystem/isoundemittersystembase.h" @@ -35,6 +36,9 @@ extern CBaseEntity *g_pLastSpawn; #define HL2MP_COMMAND_MAX_RATE 0.3 +ConVar mp_wepselmode( "mp_wepselmode", "2", FCVAR_GAMEDLL | FCVAR_NOTIFY, "StargateTC weapon selection: 0=team melee only, 1=team melee plus class weapon, 2=team default pack plus class weapon." ); +ConVar mp_autobalance( "mp_autobalance", "2", FCVAR_GAMEDLL | FCVAR_NOTIFY, "StargateTC team auto-balance: 0=off, non-zero blocks joins that would stack Tau'ri/Goa'uld." ); + void DropPrimedFragGrenade( CHL2MP_Player *pPlayer, CBaseCombatWeapon *pGrenade ); LINK_ENTITY_TO_CLASS( player, CHL2MP_Player ); @@ -48,6 +52,7 @@ IMPLEMENT_SERVERCLASS_ST(CHL2MP_Player, DT_HL2MP_Player) SendPropEHandle( SENDINFO( m_hRagdoll ) ), SendPropInt( SENDINFO( m_iSpawnInterpCounter), 4 ), SendPropInt( SENDINFO( m_iPlayerSoundType), 3 ), + SendPropInt( SENDINFO( m_iStargateClass ), 4 ), SendPropExclude( "DT_BaseAnimating", "m_flPoseParameter" ), SendPropExclude( "DT_BaseFlex", "m_viewtarget" ), @@ -60,35 +65,82 @@ END_SEND_TABLE() BEGIN_DATADESC( CHL2MP_Player ) END_DATADESC() -const char *g_ppszRandomCitizenModels[] = +// StargateTC: team 3 (Goa'uld, == TEAM_REBELS index) draws from this list. +const char *g_ppszRandomCitizenModels[] = { - "models/humans/group03/male_01.mdl", - "models/humans/group03/male_02.mdl", - "models/humans/group03/female_01.mdl", - "models/humans/group03/male_03.mdl", - "models/humans/group03/female_02.mdl", - "models/humans/group03/male_04.mdl", - "models/humans/group03/female_03.mdl", - "models/humans/group03/male_05.mdl", - "models/humans/group03/female_04.mdl", - "models/humans/group03/male_06.mdl", - "models/humans/group03/female_06.mdl", - "models/humans/group03/male_07.mdl", - "models/humans/group03/female_07.mdl", - "models/humans/group03/male_08.mdl", - "models/humans/group03/male_09.mdl", + "models/stargate/player/goauld3/goauld3.mdl", + "models/stargate/player/anubis/anubis.mdl", + "models/stargate/player/horus/horus.mdl", + "models/stargate/player/systemlord/systemlord.mdl", + "models/stargate/player/snakegoauld/snakegoauld.mdl", }; +// StargateTC: team 2 (Tau'ri, == TEAM_COMBINE index) draws from this list (SG-1). const char *g_ppszRandomCombineModels[] = { - "models/combine_soldier.mdl", - "models/combine_soldier_prisonguard.mdl", - "models/combine_super_soldier.mdl", - "models/police.mdl", + "models/stargate/player/oneill/oneill.mdl", + "models/stargate/player/carter/carter.mdl", + "models/stargate/player/jackson/jackson.mdl", + "models/stargate/player/tealc/tealc.mdl", + "models/stargate/player/commander/commander.mdl", }; +static bool Sg_IsModelInList( const char *pModel, const char * const *ppModels, int iModelCount ) +{ + if ( !pModel || !pModel[0] ) + return false; -#define MAX_COMBINE_MODELS 4 + for ( int i = 0; i < iModelCount; i++ ) + { + if ( !Q_stricmp( ppModels[i], pModel ) ) + return true; + } + + return false; +} + +static int Sg_CountTeamPlayers( int iTeam ) +{ + CTeam *pTeam = GetGlobalTeam( iTeam ); + return pTeam ? pTeam->GetNumPlayers() : 0; +} + +static int Sg_GetAutoTeam() +{ + const int iTorri = Sg_CountTeamPlayers( TEAM_COMBINE ); + const int iGoauld = Sg_CountTeamPlayers( TEAM_REBELS ); + + if ( iTorri < iGoauld ) + return TEAM_COMBINE; + if ( iGoauld < iTorri ) + return TEAM_REBELS; + + return random->RandomInt( 0, 1 ) ? TEAM_COMBINE : TEAM_REBELS; +} + +static bool Sg_WouldStackTeams( CHL2MP_Player *pPlayer, int iTeam ) +{ + if ( mp_autobalance.GetInt() <= 0 ) + return false; + + if ( iTeam != TEAM_COMBINE && iTeam != TEAM_REBELS ) + return false; + + const int iOtherTeam = ( iTeam == TEAM_COMBINE ) ? TEAM_REBELS : TEAM_COMBINE; + int iJoinTeamPlayers = Sg_CountTeamPlayers( iTeam ); + int iOtherTeamPlayers = Sg_CountTeamPlayers( iOtherTeam ); + + if ( pPlayer && pPlayer->GetTeamNumber() == iTeam ) + return false; + + if ( pPlayer && pPlayer->GetTeamNumber() == iOtherTeam ) + iOtherTeamPlayers--; + + return iJoinTeamPlayers > iOtherTeamPlayers; +} + + +#define MAX_COMBINE_MODELS 5 #define MODEL_CHANGE_INTERVAL 5.0f #define TEAM_CHANGE_INTERVAL 5.0f @@ -106,6 +158,7 @@ CHL2MP_Player::CHL2MP_Player() : m_PlayerAnimState( this ) m_flNextTeamChangeTime = 0.0f; m_iSpawnInterpCounter = 0; + m_iStargateClass = 0; m_bEnterObserver = false; m_bReady = false; @@ -198,38 +251,52 @@ void CHL2MP_Player::GiveDefaultItems( void ) { EquipSuit(); - CBasePlayer::GiveAmmo( 255, "Pistol"); - CBasePlayer::GiveAmmo( 45, "SMG1"); - CBasePlayer::GiveAmmo( 1, "grenade" ); - CBasePlayer::GiveAmmo( 6, "Buckshot"); - CBasePlayer::GiveAmmo( 6, "357" ); + // StargateTC: no Half-Life 2 weapons. The recovered GoldSrc rules use + // mp_wepselmode 0/1/2 for melee-only, class choice, or the team pack. + CBasePlayer::GiveAmmo( 255, "Pistol" ); + CBasePlayer::GiveAmmo( 255, "SMG1" ); + CBasePlayer::GiveAmmo( 64, "Buckshot" ); + CBasePlayer::GiveAmmo( 32, "357" ); + CBasePlayer::GiveAmmo( 5, "Grenade" ); - if ( GetPlayerModelType() == PLAYER_SOUNDS_METROPOLICE || GetPlayerModelType() == PLAYER_SOUNDS_COMBINESOLDIER ) + const int iWeaponMode = mp_wepselmode.GetInt(); + const char *pSwitchTo = "weapon_couteau"; + + if ( GetTeamNumber() == TEAM_REBELS ) // Goa'uld (StargateTC::TEAM_GOAULD == 3) { - GiveNamedItem( "weapon_stunstick" ); + GiveNamedItem( "weapon_coujaf" ); // team melee + pSwitchTo = "weapon_coujaf"; + + if ( iWeaponMode >= 2 ) + { + GiveNamedItem( "weapon_zat" ); + GiveNamedItem( "weapon_glstaff" ); // Goa'uld staff + GiveNamedItem( "weapon_gacgoa" ); + pSwitchTo = "weapon_glstaff"; + } } - else if ( GetPlayerModelType() == PLAYER_SOUNDS_CITIZEN ) + else if ( GetTeamNumber() == TEAM_COMBINE ) // Tau'ri / Torri (StargateTC::TEAM_TORRI == 2) { - GiveNamedItem( "weapon_crowbar" ); - } - - GiveNamedItem( "weapon_pistol" ); - GiveNamedItem( "weapon_smg1" ); - GiveNamedItem( "weapon_frag" ); - GiveNamedItem( "weapon_physcannon" ); + GiveNamedItem( "weapon_couteau" ); // team melee (knife) - const char *szDefaultWeaponName = engine->GetClientConVarValue( engine->IndexOfEdict( edict() ), "cl_defaultweapon" ); - - CBaseCombatWeapon *pDefaultWeapon = Weapon_OwnsThisType( szDefaultWeaponName ); - - if ( pDefaultWeapon ) - { - Weapon_Switch( pDefaultWeapon ); + if ( iWeaponMode >= 2 ) + { + GiveNamedItem( "weapon_m92s" ); + GiveNamedItem( "weapon_m16" ); + GiveNamedItem( "weapon_zat" ); + GiveNamedItem( "weapon_flashbang" ); + pSwitchTo = "weapon_m16"; + } } else { - Weapon_Switch( Weapon_OwnsThisType( "weapon_physcannon" ) ); + // Unassigned / spectator: only a knife so they spawn with something SG. + GiveNamedItem( "weapon_couteau" ); } + + CBaseCombatWeapon *pWeapon = Weapon_OwnsThisType( pSwitchTo ); + if ( pWeapon ) + Weapon_Switch( pWeapon ); } void CHL2MP_Player::PickDefaultSpawnTeam( void ) @@ -256,28 +323,9 @@ void CHL2MP_Player::PickDefaultSpawnTeam( void ) } else { - CTeam *pCombine = g_Teams[TEAM_COMBINE]; - CTeam *pRebels = g_Teams[TEAM_REBELS]; - - if ( pCombine == NULL || pRebels == NULL ) - { - ChangeTeam( random->RandomInt( TEAM_COMBINE, TEAM_REBELS ) ); - } - else - { - if ( pCombine->GetNumPlayers() > pRebels->GetNumPlayers() ) - { - ChangeTeam( TEAM_REBELS ); - } - else if ( pCombine->GetNumPlayers() < pRebels->GetNumPlayers() ) - { - ChangeTeam( TEAM_COMBINE ); - } - else - { - ChangeTeam( random->RandomInt( TEAM_COMBINE, TEAM_REBELS ) ); - } - } + // GoldSrc StargateTC starts clients as spectators and opens the + // team/class menu; it does not auto-spawn them onto a combat team. + BaseClass::ChangeTeam( TEAM_SPECTATOR ); } } } @@ -302,6 +350,10 @@ void CHL2MP_Player::Spawn(void) RemoveEffects( EF_NODRAW ); GiveDefaultItems(); + + // StargateTC: re-apply the chosen class loadout + refresh HUD team state. + extern void Sg_OnPlayerSpawn( CHL2MP_Player *pPlayer ); + Sg_OnPlayerSpawn( this ); } SetNumAnimOverlays( 3 ); @@ -366,25 +418,18 @@ bool CHL2MP_Player::ValidatePlayerModel( const char *pModel ) void CHL2MP_Player::SetPlayerTeamModel( void ) { + if ( GetTeamNumber() == TEAM_SPECTATOR ) + return; + + if ( GetStargateClass() > 0 ) + return; + const char *szModelName = NULL; szModelName = engine->GetClientConVarValue( engine->IndexOfEdict( edict() ), "cl_playermodel" ); - int modelIndex = modelinfo->GetModelIndex( szModelName ); - - if ( modelIndex == -1 || ValidatePlayerModel( szModelName ) == false ) - { - szModelName = "models/Combine_Soldier.mdl"; - m_iModelType = TEAM_COMBINE; - - char szReturnString[512]; - - Q_snprintf( szReturnString, sizeof (szReturnString ), "cl_playermodel %s\n", szModelName ); - engine->ClientCommand ( edict(), szReturnString ); - } - if ( GetTeamNumber() == TEAM_COMBINE ) { - if ( Q_stristr( szModelName, "models/human") ) + if ( !Sg_IsModelInList( szModelName, g_ppszRandomCombineModels, ARRAYSIZE( g_ppszRandomCombineModels ) ) ) { int nHeads = ARRAYSIZE( g_ppszRandomCombineModels ); @@ -396,7 +441,7 @@ void CHL2MP_Player::SetPlayerTeamModel( void ) } else if ( GetTeamNumber() == TEAM_REBELS ) { - if ( !Q_stristr( szModelName, "models/human") ) + if ( !Sg_IsModelInList( szModelName, g_ppszRandomCitizenModels, ARRAYSIZE( g_ppszRandomCitizenModels ) ) ) { int nHeads = ARRAYSIZE( g_ppszRandomCitizenModels ); @@ -406,6 +451,11 @@ void CHL2MP_Player::SetPlayerTeamModel( void ) m_iModelType = TEAM_REBELS; } + else + { + szModelName = g_ppszRandomCitizenModels[0]; + m_iModelType = TEAM_REBELS; + } SetModel( szModelName ); SetupPlayerSoundsByModel( szModelName ); @@ -426,7 +476,7 @@ void CHL2MP_Player::SetPlayerModel( void ) if ( ValidatePlayerModel( pszCurrentModelName ) == false ) { - pszCurrentModelName = "models/Combine_Soldier.mdl"; + pszCurrentModelName = "models/stargate/player/oneill/oneill.mdl"; } Q_snprintf( szReturnString, sizeof (szReturnString ), "cl_playermodel %s\n", pszCurrentModelName ); @@ -474,7 +524,7 @@ void CHL2MP_Player::SetPlayerModel( void ) if ( modelIndex == -1 ) { - szModelName = "models/Combine_Soldier.mdl"; + szModelName = "models/stargate/player/oneill/oneill.mdl"; m_iModelType = TEAM_COMBINE; char szReturnString[512]; @@ -503,6 +553,10 @@ void CHL2MP_Player::SetupPlayerSoundsByModel( const char *pModelName ) { m_iPlayerSoundType = (int)PLAYER_SOUNDS_COMBINESOLDIER; } + else + { + m_iPlayerSoundType = (int)PLAYER_SOUNDS_CITIZEN; + } } void CHL2MP_Player::ResetAnimation( void ) @@ -900,6 +954,7 @@ void CHL2MP_Player::ChangeTeam( int iTeam ) }*/ bool bKill = false; + const int iOldTeam = GetTeamNumber(); if ( HL2MPRules()->IsTeamplay() != true && iTeam != TEAM_SPECTATOR ) { @@ -909,7 +964,7 @@ void CHL2MP_Player::ChangeTeam( int iTeam ) if ( HL2MPRules()->IsTeamplay() == true ) { - if ( iTeam != GetTeamNumber() && GetTeamNumber() != TEAM_UNASSIGNED ) + if ( iTeam != iOldTeam && iOldTeam != TEAM_UNASSIGNED && iOldTeam != TEAM_SPECTATOR && iTeam != TEAM_SPECTATOR && IsAlive() ) { bKill = true; } @@ -919,7 +974,13 @@ void CHL2MP_Player::ChangeTeam( int iTeam ) m_flNextTeamChangeTime = gpGlobals->curtime + TEAM_CHANGE_INTERVAL; - if ( HL2MPRules()->IsTeamplay() == true ) + if ( iTeam == TEAM_SPECTATOR ) + { + RemoveAllItems( true ); + + State_Transition( STATE_OBSERVER_MODE ); + } + else if ( HL2MPRules()->IsTeamplay() == true ) { SetPlayerTeamModel(); } @@ -928,13 +989,6 @@ void CHL2MP_Player::ChangeTeam( int iTeam ) SetPlayerModel(); } - if ( iTeam == TEAM_SPECTATOR ) - { - RemoveAllItems( true ); - - State_Transition( STATE_OBSERVER_MODE ); - } - if ( bKill == true ) { CommitSuicide(); @@ -943,7 +997,12 @@ void CHL2MP_Player::ChangeTeam( int iTeam ) bool CHL2MP_Player::HandleCommand_JoinTeam( int team ) { - if ( !GetGlobalTeam( team ) || team == 0 ) + if ( team == 0 ) + { + team = Sg_GetAutoTeam(); + } + + if ( !GetGlobalTeam( team ) ) { Warning( "HandleCommand_JoinTeam( %d ) - invalid team index.\n", team ); return false; @@ -974,12 +1033,25 @@ bool CHL2MP_Player::HandleCommand_JoinTeam( int team ) } else { + if ( Sg_WouldStackTeams( this, team ) ) + { + ClientPrint( this, HUD_PRINTCENTER, "That team has too many players." ); + ShowViewPortPanel( PANEL_TEAM, true ); + return false; + } + + const bool bNeedsRespawn = ( GetTeamNumber() == TEAM_UNASSIGNED || GetTeamNumber() == TEAM_SPECTATOR || IsObserver() || IsDead() ); StopObserverMode(); State_Transition(STATE_ACTIVE); - } - // Switch their actual team... - ChangeTeam( team ); + // Switch their actual team... + ChangeTeam( team ); + + if ( bNeedsRespawn ) + { + Spawn(); + } + } return true; } @@ -1000,6 +1072,7 @@ bool CHL2MP_Player::ClientCommand( const CCommand &args ) if ( args.ArgC() < 2 ) { Warning( "Player sent bad jointeam syntax\n" ); + return true; } if ( ShouldRunRateLimitedCommand( args ) ) @@ -1009,6 +1082,39 @@ bool CHL2MP_Player::ClientCommand( const CCommand &args ) } return true; } + else if ( FStrEq( args[0], "joinclass" ) ) + { + extern void Sg_HandleJoinClass( CHL2MP_Player *pPlayer, const char *pszClass ); + Sg_HandleJoinClass( this, args.ArgC() >= 2 ? args[1] : "" ); + return true; + } + else if ( FStrEq( args[0], "autopick" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + HandleCommand_JoinTeam( 0 ); + extern void Sg_HandleJoinClass( CHL2MP_Player *pPlayer, const char *pszClass ); + Sg_HandleJoinClass( this, "auto" ); + } + return true; + } + else if ( FStrEq( args[0], "classe" ) ) + { + extern void Sg_HandleJoinClass( CHL2MP_Player *pPlayer, const char *pszClass ); + Sg_HandleJoinClass( this, args.ArgC() >= 2 ? args[1] : "" ); + return true; + } + else if ( FStrEq( args[0], "showmenu" ) ) + { + ShowViewPortPanel( PANEL_TEAM, true ); + return true; + } + else if ( FStrEq( args[0], "showequip" ) ) + { + if ( mp_wepselmode.GetInt() > 0 ) + ShowViewPortPanel( PANEL_TEAM, true ); + return true; + } else if ( FStrEq( args[0], "joingame" ) ) { return true; @@ -1576,7 +1682,7 @@ CHL2MPPlayerStateInfo *CHL2MP_Player::State_LookupInfo( HL2MPPlayerState state ) bool CHL2MP_Player::StartObserverMode(int mode) { //we only want to go into observer mode if the player asked to, not on a death timeout - if ( m_bEnterObserver == true ) + if ( m_bEnterObserver == true || GetTeamNumber() == TEAM_SPECTATOR ) { VPhysicsDestroyObject(); return BaseClass::StartObserverMode( mode ); diff --git a/game/server/hl2mp/hl2mp_player.h b/game/server/hl2mp/hl2mp_player.h index f91513c7..c7f3f175 100644 --- a/game/server/hl2mp/hl2mp_player.h +++ b/game/server/hl2mp/hl2mp_player.h @@ -97,6 +97,8 @@ public: void SetPlayerModel( void ); void SetPlayerTeamModel( void ); Activity TranslateTeamActivity( Activity ActToTranslate ); + int GetStargateClass() const { return m_iStargateClass; } + void SetStargateClass( int iClass ) { m_iStargateClass = iClass; } float GetNextModelChangeTime( void ) { return m_flNextModelChangeTime; } float GetNextTeamChangeTime( void ) { return m_flNextTeamChangeTime; } @@ -147,6 +149,7 @@ private: int m_iModelType; CNetworkVar( int, m_iSpawnInterpCounter ); CNetworkVar( int, m_iPlayerSoundType ); + CNetworkVar( int, m_iStargateClass ); float m_flNextModelChangeTime; float m_flNextTeamChangeTime; diff --git a/game/server/server_hl2mp.vpc b/game/server/server_hl2mp.vpc index 7226ec93..6c9c9f4a 100644 --- a/game/server/server_hl2mp.vpc +++ b/game/server/server_hl2mp.vpc @@ -304,6 +304,7 @@ $Project "Server (HL2MP)" $File "stargate\sg_kawoosh.cpp" $File "stargate\sg_mcp_debug.cpp" $File "stargate\sg_player_spawn.cpp" + $File "stargate\sg_player_class.cpp" $File "stargate\STUBS.cpp" $File "$SRCDIR\game\shared\stargate\sg_weapons.cpp" $File "$SRCDIR\game\shared\stargate\sg_constants.h" diff --git a/game/server/stargate/sg_player_class.cpp b/game/server/stargate/sg_player_class.cpp new file mode 100644 index 00000000..9755fe5d --- /dev/null +++ b/game/server/stargate/sg_player_class.cpp @@ -0,0 +1,295 @@ +//============= Copyright (c) StargateTC Source Team ============================ +// +// Server side of the StargateTC team/class selection flow. +// +// The behavior mirrors the recovered GoldSrc rules: clients enter spectator, +// choose Tau'ri or Goa'uld, then choose one of five team classes. mp_wepselmode +// controls whether that class contributes a weapon on top of the team melee or +// default team pack. +// +//============================================================================== +#include "cbase.h" +#include "hl2mp_player.h" +#include "recipientfilter.h" +#include "stargate/sg_constants.h" +#include + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ConVar mp_wepselmode; + +struct SgClassInfo_t +{ + const char *name; // joinclass token + const char *display; // scoreboard/chat label + const char *avatar; // avatar sprite name (vgui/stargate/avatars/) + const char *model; // player model + const char *weapon; // signature loadout weapon + int team; // owning team + int slot; // original class slot, 1..5 inside team + int limit; // 0 means unlimited +}; + +// Class roster recovered from the GoldSrc class menu and class-limit checks. +static const SgClassInfo_t s_Classes[] = { + { "oneill", "O'Neill", "oneill", "models/stargate/player/oneill/oneill.mdl", "weapon_m92s", StargateTC::TEAM_TORRI, 1, 2 }, + { "carter", "Carter", "carter", "models/stargate/player/carter/carter.mdl", "weapon_zat", StargateTC::TEAM_TORRI, 2, 3 }, + { "jackson", "Jackson", "jackson", "models/stargate/player/jackson/jackson.mdl", "weapon_zat", StargateTC::TEAM_TORRI, 3, 3 }, + { "tealc", "Teal'c", "tealc", "models/stargate/player/tealc/tealc.mdl", "weapon_lance", StargateTC::TEAM_TORRI, 4, 0 }, + { "soldat", "Soldier", "commder", "models/stargate/player/commander/commander.mdl", "weapon_m16", StargateTC::TEAM_TORRI, 5, 0 }, + { "dieu", "System Lord", "lord", "models/stargate/player/systemlord/systemlord.mdl", "weapon_zatarc", StargateTC::TEAM_GOAULD, 1, 2 }, + { "anubis", "Anubis Guard", "anubis", "models/stargate/player/anubis/anubis.mdl", "weapon_lance", StargateTC::TEAM_GOAULD, 2, 2 }, + { "jaffa", "Jaffa", "goauld3", "models/stargate/player/goauld3/goauld3.mdl", "weapon_lance", StargateTC::TEAM_GOAULD, 3, 0 }, + { "prima", "First Prime", "snake", "models/stargate/player/snakegoauld/snakegoauld.mdl", "weapon_lance", StargateTC::TEAM_GOAULD, 4, 3 }, + { "esclave", "Horus Guard", "slave", "models/stargate/player/horus/horus.mdl", "weapon_lance", StargateTC::TEAM_GOAULD, 5, 3 }, +}; + +// Chosen class per player slot (1..MAX_PLAYERS). +static char s_PlayerClass[ MAX_PLAYERS + 1 ][ 32 ]; + +static const SgClassInfo_t *Sg_FindClass( const char *name ) +{ + for ( int i = 0; i < ARRAYSIZE( s_Classes ); i++ ) + { + if ( name && !Q_stricmp( s_Classes[i].name, name ) ) + return &s_Classes[i]; + } + + return NULL; +} + +static const SgClassInfo_t *Sg_FindClassBySlot( int team, int slot ) +{ + for ( int i = 0; i < ARRAYSIZE( s_Classes ); i++ ) + { + if ( s_Classes[i].team == team && s_Classes[i].slot == slot ) + return &s_Classes[i]; + } + + return NULL; +} + +static int Sg_CountPlayersUsingClass( const SgClassInfo_t *pClass, CHL2MP_Player *pIgnore ) +{ + if ( !pClass ) + return 0; + + int count = 0; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CHL2MP_Player *pOther = ToHL2MPPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pOther || pOther == pIgnore ) + continue; + + if ( pOther->GetTeamNumber() != pClass->team ) + continue; + + if ( !Q_stricmp( s_PlayerClass[i], pClass->name ) ) + count++; + } + + return count; +} + +static bool Sg_ClassHasRoom( const SgClassInfo_t *pClass, CHL2MP_Player *pPlayer ) +{ + if ( !pClass || pClass->limit <= 0 ) + return true; + + return Sg_CountPlayersUsingClass( pClass, pPlayer ) < pClass->limit; +} + +static const SgClassInfo_t *Sg_FindAutoClass( int team, CHL2MP_Player *pPlayer ) +{ + const int start = random->RandomInt( 0, ARRAYSIZE( s_Classes ) - 1 ); + const SgClassInfo_t *pBest = NULL; + int bestCount = INT_MAX; + + for ( int offset = 0; offset < ARRAYSIZE( s_Classes ); offset++ ) + { + const SgClassInfo_t *pClass = &s_Classes[( start + offset ) % ARRAYSIZE( s_Classes )]; + if ( pClass->team != team ) + continue; + + if ( !Sg_ClassHasRoom( pClass, pPlayer ) ) + continue; + + const int count = Sg_CountPlayersUsingClass( pClass, pPlayer ); + if ( count < bestCount ) + { + pBest = pClass; + bestCount = count; + } + } + + return pBest; +} + +static const SgClassInfo_t *Sg_GetStoredClass( CHL2MP_Player *pPlayer ) +{ + if ( !pPlayer ) + return NULL; + + const int idx = pPlayer->entindex(); + if ( idx < 1 || idx > MAX_PLAYERS ) + return NULL; + + const SgClassInfo_t *pClass = Sg_FindClass( s_PlayerClass[idx] ); + if ( pClass && pClass->team == pPlayer->GetTeamNumber() ) + return pClass; + + return NULL; +} + +//----------------------------------------------------------------------------- +// HUD message senders (client side: CHudStargate::MsgFunc_*). +//----------------------------------------------------------------------------- +void Sg_SendAboutMe( CBasePlayer *pPlayer ) +{ + if ( !pPlayer ) + return; + + CSingleUserRecipientFilter filter( pPlayer ); + filter.MakeReliable(); + UserMessageBegin( filter, "SgAboutMe" ); + WRITE_SHORT( pPlayer->entindex() ); + WRITE_SHORT( pPlayer->GetTeamNumber() ); + MessageEnd(); +} + +void Sg_SendAddPortrait( CBasePlayer *pPlayer, const char *avatar ) +{ + if ( !pPlayer ) + return; + + CSingleUserRecipientFilter filter( pPlayer ); + filter.MakeReliable(); + UserMessageBegin( filter, "SgAddPortrait" ); + WRITE_SHORT( pPlayer->entindex() ); + WRITE_SHORT( pPlayer->GetTeamNumber() ); + WRITE_STRING( avatar ? avatar : "" ); + MessageEnd(); +} + +void Sg_ResetPlayerClass( CHL2MP_Player *pPlayer ) +{ + if ( !pPlayer ) + return; + + const int idx = pPlayer->entindex(); + if ( idx < 1 || idx > MAX_PLAYERS ) + return; + + s_PlayerClass[idx][0] = '\0'; + pPlayer->SetStargateClass( 0 ); +} + +//----------------------------------------------------------------------------- +// Apply the stored class. Safe to call from spawn and from joinclass. +//----------------------------------------------------------------------------- +void Sg_ApplyClass( CHL2MP_Player *pPlayer, bool bResetLoadout ) +{ + if ( !pPlayer ) + return; + + Sg_SendAboutMe( pPlayer ); + + const SgClassInfo_t *pClass = Sg_GetStoredClass( pPlayer ); + if ( !pClass ) + { + pPlayer->SetStargateClass( 0 ); + return; + } + + pPlayer->SetStargateClass( pClass->slot ); + pPlayer->SetModel( pClass->model ); + pPlayer->SetupPlayerSoundsByModel( pClass->model ); + + Sg_SendAddPortrait( pPlayer, pClass->avatar ); + + if ( !pPlayer->IsAlive() ) + return; + + if ( bResetLoadout ) + { + pPlayer->RemoveAllItems( true ); + pPlayer->GiveDefaultItems(); + } + + if ( mp_wepselmode.GetInt() <= 0 || !pClass->weapon[0] ) + return; + + if ( !pPlayer->Weapon_OwnsThisType( pClass->weapon ) ) + pPlayer->GiveNamedItem( pClass->weapon ); + + CBaseCombatWeapon *pWeapon = pPlayer->Weapon_OwnsThisType( pClass->weapon ); + if ( pWeapon ) + pPlayer->Weapon_Switch( pWeapon ); +} + +//----------------------------------------------------------------------------- +// `joinclass `, `joinclass auto`, and legacy `classe <1..5>`. +//----------------------------------------------------------------------------- +void Sg_HandleJoinClass( CHL2MP_Player *pPlayer, const char *pszClass ) +{ + if ( !pPlayer ) + return; + + const int idx = pPlayer->entindex(); + if ( idx < 1 || idx > MAX_PLAYERS ) + return; + + const SgClassInfo_t *pClass = NULL; + const char *pClassArg = pszClass ? pszClass : ""; + + if ( !Q_stricmp( pClassArg, "auto" ) ) + { + if ( pPlayer->GetTeamNumber() != StargateTC::TEAM_TORRI && pPlayer->GetTeamNumber() != StargateTC::TEAM_GOAULD ) + pPlayer->HandleCommand_JoinTeam( 0 ); + + pClass = Sg_FindAutoClass( pPlayer->GetTeamNumber(), pPlayer ); + } + else if ( pClassArg[0] >= '0' && pClassArg[0] <= '9' ) + { + pClass = Sg_FindClassBySlot( pPlayer->GetTeamNumber(), atoi( pClassArg ) ); + } + else + { + pClass = Sg_FindClass( pClassArg ); + } + + if ( !pClass ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "joinclass: unknown class\n" ); + return; + } + + if ( pPlayer->GetTeamNumber() != pClass->team ) + { + if ( !pPlayer->HandleCommand_JoinTeam( pClass->team ) ) + return; + } + + if ( !Sg_ClassHasRoom( pClass, pPlayer ) ) + { + ClientPrint( pPlayer, HUD_PRINTCENTER, "That class is full." ); + return; + } + + Q_strncpy( s_PlayerClass[idx], pClass->name, sizeof( s_PlayerClass[idx] ) ); + + char msg[ 96 ]; + Q_snprintf( msg, sizeof( msg ), "Class: %s\n", pClass->display ); + ClientPrint( pPlayer, HUD_PRINTTALK, msg ); + + Sg_ApplyClass( pPlayer, true ); +} + +//----------------------------------------------------------------------------- +// Called from CHL2MP_Player::Spawn so the class loadout/HUD survive respawns. +//----------------------------------------------------------------------------- +void Sg_OnPlayerSpawn( CHL2MP_Player *pPlayer ) +{ + Sg_ApplyClass( pPlayer, false ); +} diff --git a/game/shared/hl2mp/hl2mp_gamerules.cpp b/game/shared/hl2mp/hl2mp_gamerules.cpp index 8d29d949..19962583 100644 --- a/game/shared/hl2mp/hl2mp_gamerules.cpp +++ b/game/shared/hl2mp/hl2mp_gamerules.cpp @@ -811,14 +811,7 @@ void CHL2MPRules::ClientSettingsChanged( CBasePlayer *pPlayer ) } else { - if ( Q_stristr( szModelName, "models/human") ) - { - pHL2Player->ChangeTeam( TEAM_REBELS ); - } - else - { - pHL2Player->ChangeTeam( TEAM_COMBINE ); - } + pHL2Player->SetPlayerTeamModel(); } } if ( sv_report_client_settings.GetInt() == 1 )