init commit

This commit is contained in:
2025-05-31 10:13:27 +02:00
parent 3da26f025c
commit ada9afc48d
30 changed files with 3955 additions and 16 deletions

View File

@@ -0,0 +1,230 @@
SquadMenu = SquadMenu or {}
if CLIENT then
-- Settings file
SquadMenu.DATA_FILE = "squad_menu.json"
end
-- Chat prefixes that allow messaging squad members only
SquadMenu.CHAT_PREFIXES = { "/s", "!s", "/p", "!pchat" }
-- Primary color used for the UI theme
SquadMenu.THEME_COLOR = Color( 34, 52, 142 )
-- Max. length of a squad name
SquadMenu.MAX_NAME_LENGTH = 30
-- Size limit for JSON data
SquadMenu.MAX_JSON_SIZE = 49152 -- 48 kibibytes
-- Used on net.WriteUInt for the command ID
SquadMenu.COMMAND_SIZE = 4
-- Command IDs (Max. ID when COMMAND_SIZE = 4 is 15)
SquadMenu.BROADCAST_EVENT = 0
SquadMenu.SQUAD_LIST = 1
SquadMenu.SETUP_SQUAD = 2
SquadMenu.JOIN_SQUAD = 3
SquadMenu.LEAVE_SQUAD = 4
SquadMenu.ACCEPT_REQUESTS = 5
SquadMenu.REQUESTS_LIST = 6
SquadMenu.KICK = 7
SquadMenu.PING = 8
-- Reasons given when a member is removed from a squad
SquadMenu.LEAVE_REASON_DELETED = 0
SquadMenu.LEAVE_REASON_LEFT = 1
SquadMenu.LEAVE_REASON_KICKED = 2
-- Server settings
local maxMembersCvar = CreateConVar(
"squad_max_members",
"10",
FCVAR_ARCHIVE + FCVAR_REPLICATED + FCVAR_NOTIFY,
"Limits how many members a single squad can have.",
1, 100
)
local squadListPosCvar = CreateConVar(
"squad_members_position",
"6",
FCVAR_ARCHIVE + FCVAR_REPLICATED + FCVAR_NOTIFY,
"Sets the position of the squad members on the screen. Takes numbers betweek 1-9 and uses the same positions as a numpad.",
1, 9
)
local broadcastCvar = CreateConVar(
"squad_broadcast_creation_message",
"1",
FCVAR_ARCHIVE + FCVAR_REPLICATED + FCVAR_NOTIFY,
"When set to 1, Squad Menu will print when a new squad is created on the chat.",
0, 1
)
local friendlyfireCvar = CreateConVar(
"squad_force_friendly_fire",
"0",
FCVAR_ARCHIVE + FCVAR_REPLICATED + FCVAR_NOTIFY,
"Makes so squads always have friendly fire enabled.",
0, 1
)
function SquadMenu.PrintF( str, ... )
MsgC( SquadMenu.THEME_COLOR, "[Squad Menu] ", Color( 255, 255, 255 ), string.format( str, ... ), "\n" )
end
function SquadMenu.TableToJSON( t )
return util.TableToJSON( t, false )
end
function SquadMenu.JSONToTable( s )
if type( s ) ~= "string" or s == "" then
return {}
end
return util.JSONToTable( s ) or {}
end
function SquadMenu.GetMemberLimit()
return maxMembersCvar:GetInt()
end
function SquadMenu.GetMembersPosition()
return squadListPosCvar:GetInt()
end
function SquadMenu.GetShowCreationMessage()
return broadcastCvar:GetInt() > 0
end
function SquadMenu.GetForceFriendlyFire()
return friendlyfireCvar:GetInt() > 0
end
function SquadMenu.GetPlayerId( ply )
if ply:IsBot() then
return "BOT_" .. ply:AccountID()
end
return ply:SteamID()
end
local PID = SquadMenu.GetPlayerId
function SquadMenu.AllPlayersById()
local all = player.GetAll()
local byId = {}
for _, ply in ipairs( all ) do
byId[PID( ply )] = ply
end
return byId
end
function SquadMenu.FindPlayerById( id )
local all = player.GetAll()
for _, ply in ipairs( all ) do
if id == PID( ply ) then return ply end
end
end
function SquadMenu.ValidateNumber( n, default, min, max )
return math.Clamp( tonumber( n ) or default, min, max )
end
function SquadMenu.ValidateString( s, default, maxLength )
if type( s ) ~= "string" then
return default
end
s = string.Trim( s )
if s == "" then
return default
end
if s:len() > maxLength then
return string.Left( s, maxLength - 3 ) .. "..."
end
return s
end
function SquadMenu.StartCommand( id )
net.Start( "squad_menu.command", false )
net.WriteUInt( id, SquadMenu.COMMAND_SIZE )
end
function SquadMenu.WriteTable( t )
local data = util.Compress( SquadMenu.TableToJSON( t ) )
local bytes = #data
net.WriteUInt( bytes, 16 )
if bytes > SquadMenu.MAX_JSON_SIZE then
SquadMenu.PrintF( "Tried to write JSON that was too big! (%d/%d)", bytes, SquadMenu.MAX_JSON_SIZE )
return
end
net.WriteData( data )
end
function SquadMenu.ReadTable()
local bytes = net.ReadUInt( 16 )
if bytes > SquadMenu.MAX_JSON_SIZE then
SquadMenu.PrintF( "Tried to read JSON that was too big! (%d/%d)", bytes, SquadMenu.MAX_JSON_SIZE )
return {}
end
local data = net.ReadData( bytes )
return SquadMenu.JSONToTable( util.Decompress( data ) )
end
if SERVER then
-- Shared files
include( "squad_menu/player.lua" )
AddCSLuaFile( "squad_menu/player.lua" )
-- Server files
include( "squad_menu/server/main.lua" )
include( "squad_menu/server/squad.lua" )
include( "squad_menu/server/network.lua" )
-- Client files
AddCSLuaFile( "includes/modules/styled_theme.lua" )
AddCSLuaFile( "includes/modules/styled_theme_tabbed_frame.lua" )
AddCSLuaFile( "squad_menu/client/main.lua" )
AddCSLuaFile( "squad_menu/client/config.lua" )
AddCSLuaFile( "squad_menu/client/menu.lua" )
AddCSLuaFile( "squad_menu/client/hud.lua" )
AddCSLuaFile( "squad_menu/client/vgui/member_status.lua" )
AddCSLuaFile( "squad_menu/client/vgui/squad_list_row.lua" )
end
if CLIENT then
-- Shared files
include( "squad_menu/player.lua" )
-- Setup UI theme
require( "styled_theme" )
require( "styled_theme_tabbed_frame" )
StyledTheme.RegisterFont( "SquadMenuInfo", 0.016, {
font = "Roboto-Condensed",
weight = 600,
} )
-- Client files
include( "squad_menu/client/main.lua" )
include( "squad_menu/client/config.lua" )
include( "squad_menu/client/menu.lua" )
include( "squad_menu/client/hud.lua" )
include( "squad_menu/client/vgui/member_status.lua" )
include( "squad_menu/client/vgui/squad_list_row.lua" )
end

View File

@@ -0,0 +1,8 @@
E2Helper.Descriptions["isSquadMember(e:)"] = "Returns 1 is this player is part of a squad."
E2Helper.Descriptions["getSquadID(e:)"] = "Get the ID of the squad this player is part of. Returns -1 if this player is not in one."
E2Helper.Descriptions["doesSquadExist(n)"] = "Returns 1 if the given ID points to a valid squad."
E2Helper.Descriptions["getSquadName(n)"] = "Finds a squad by it's ID and returns the name. Returns a empty string if the squad does not exist."
E2Helper.Descriptions["getSquadColor(n)"] = "Finds a squad by it's ID and returns the color. Returns vec(0) if the squad does not exist."
E2Helper.Descriptions["getSquadMemberCount(n)"] = "Returns the number of members in a squad, or 0 if the squad does not exist."
E2Helper.Descriptions["getSquadMembers(n)"] = "Returns an array of active players associated with the squad ID, or a empty array if the squad does not exist."
E2Helper.Descriptions["getAllSquadIDs()"] = "Returns an array of all available squad IDs."

View File

@@ -0,0 +1,59 @@
E2Lib.RegisterExtension( "squad_menu", true, "Add player functions related to the Squad Menu" )
local function ValidatePlayer( self, ent )
if not IsValid( ent ) then self:throw( "Invalid entity!", 0 ) end
if not ent:IsPlayer() then self:throw( "Not a player entity!", 0 ) end
end
__e2setcost( 5 )
e2function number entity:isSquadMember()
ValidatePlayer( self, this )
return this:GetSquadID() == -1 and 0 or 1
end
e2function number entity:getSquadID()
ValidatePlayer( self, this )
return this:GetSquadID()
end
e2function number doesSquadExist( number id )
return SquadMenu:GetSquad( id ) == nil and 0 or 1
end
e2function string getSquadName( number id )
local squad = SquadMenu:GetSquad( id )
return squad and squad.name or ""
end
e2function vector getSquadColor( number id )
local squad = SquadMenu:GetSquad( id )
return squad and Vector( squad.r, squad.g, squad.b ) or Vector()
end
e2function number getSquadMemberCount( number id )
local squad = SquadMenu:GetSquad( id )
if not squad then return 0 end
local _, count = squad:GetActiveMembers()
return count
end
e2function array getSquadMembers( number id )
local squad = SquadMenu:GetSquad( id )
if not squad then return {} end
local members = squad:GetActiveMembers()
return members
end
e2function array getAllSquadIDs()
local all, i = {}, 0
for id, _ in pairs( SquadMenu.squads ) do
i = i + 1
all[i] = id
end
return all
end

View File

@@ -0,0 +1,681 @@
--[[
StyledStrike's VGUI theme utilities
A collection of functions to create common
UI panels and to apply a custom theme to them.
]]
StyledTheme = StyledTheme or {}
--[[
Setup color constants
]]
do
StyledTheme.colors = StyledTheme.colors or {}
local colors = StyledTheme.colors or {}
colors.accent = Color( 56, 113, 179 )
colors.panelBackground = Color( 46, 46, 46, 240 )
colors.panelDisabledBackground = Color( 90, 90, 90, 255 )
colors.scrollBackground = Color( 0, 0, 0, 200 )
colors.labelText = Color( 255, 255, 255, 255 )
colors.labelTextDisabled = Color( 180, 180, 180, 255 )
colors.buttonHover = Color( 150, 150, 150, 50 )
colors.buttonPress = colors.accent
colors.buttonBorder = Color( 32, 32, 32, 255 )
colors.buttonText = Color( 255, 255, 255, 255 )
colors.buttonTextDisabled = Color( 180, 180, 180, 255 )
colors.entryBackground = Color( 20, 20, 20, 255 )
colors.entryBorder = Color( 80, 80, 80, 255 )
colors.entryHighlight = colors.accent
colors.entryPlaceholder = Color( 150, 150, 150, 255 )
colors.entryText = Color( 255, 255, 255, 255 )
end
--[[
Setup dimensions
]]
StyledTheme.dimensions = StyledTheme.dimensions or {}
hook.Add( "StyledTheme_OnResolutionChange", "StyledTheme.UpdateDimensions", function()
local dimensions = StyledTheme.dimensions
local ScaleSize = StyledTheme.ScaleSize
dimensions.framePadding = ScaleSize( 10 )
dimensions.frameButtonSize = ScaleSize( 36 )
dimensions.buttonHeight = ScaleSize( 40 )
dimensions.headerHeight = ScaleSize( 32 )
dimensions.scrollBarWidth = ScaleSize( 16 )
dimensions.scrollPadding = ScaleSize( 8 )
dimensions.formPadding = ScaleSize( 20 )
dimensions.formSeparator = ScaleSize( 6 )
dimensions.formLabelWidth = ScaleSize( 300 )
dimensions.menuPadding = ScaleSize( 6 )
dimensions.indicatorSize = ScaleSize( 20 )
end )
--[[
Setup fonts
]]
StyledTheme.BASE_FONT_NAME = "Roboto"
StyledTheme.fonts = StyledTheme.fonts or {}
function StyledTheme.RegisterFont( name, screenSize, data )
data = data or {}
data.screenSize = screenSize
data.font = data.font or StyledTheme.BASE_FONT_NAME
data.extended = true
StyledTheme.fonts[name] = data
StyledTheme.forceUpdateResolution = true
end
StyledTheme.RegisterFont( "StyledTheme_Small", 0.018, {
weight = 500,
} )
StyledTheme.RegisterFont( "StyledTheme_Tiny", 0.013, {
weight = 500,
} )
hook.Add( "StyledTheme_OnResolutionChange", "StyledTheme.UpdateFonts", function( _, screenH )
for name, data in pairs( StyledTheme.fonts ) do
data.size = math.floor( screenH * data.screenSize )
surface.CreateFont( name, data )
end
end )
--[[
Watch for changes in screen resolution
]]
do
local screenW, screenH = ScrW(), ScrH()
local Floor = math.floor
--- Scales the given size (in pixels) from a 1080p resolution to
--- the resolution currently being used by the game.
function StyledTheme.ScaleSize( size )
return Floor( ( size / 1080 ) * screenH )
end
local function UpdateResolution()
screenW, screenH = ScrW(), ScrH()
StyledTheme.forceUpdateResolution = false
hook.Run( "StyledTheme_OnResolutionChange", screenW, screenH )
end
-- Only update resolution on gamemode initialization.
hook.Add( "Initialize", "StyledTheme.UpdateResolution", UpdateResolution )
local ScrW, ScrH = ScrW, ScrH
timer.Create( "StyledTheme.CheckResolution", 2, 0, function()
if ScrW() ~= screenW or ScrH() ~= screenH or StyledTheme.forceUpdateResolution then
UpdateResolution()
end
end )
end
--[[
Misc. utility functions
]]
do
--- Gets a localized language string, with the first character being in uppercase.
function StyledTheme.GetUpperLanguagePhrase( text )
text = language.GetPhrase( text )
return text:sub( 1, 1 ):upper() .. text:sub( 2 )
end
local SetDrawColor = surface.SetDrawColor
local DrawRect = surface.DrawRect
--- Draw box, using the specified background color.
--- It allows overriding the alpha while keeping the supplied color table intact.
function StyledTheme.DrawRect( x, y, w, h, color, alpha )
alpha = alpha or 1
SetDrawColor( color.r, color.g, color.b, color.a * alpha )
DrawRect( x, y, w, h )
end
local SetMaterial = surface.SetMaterial
local MAT_BLUR = Material( "pp/blurscreen" )
--- Blur the background of a panel.
function StyledTheme.BlurPanel( panel, alpha, density )
SetDrawColor( 255, 255, 255, alpha or panel:GetAlpha() )
SetMaterial( MAT_BLUR )
MAT_BLUR:SetFloat( "$blur", density or 4 )
MAT_BLUR:Recompute()
render.UpdateScreenEffectTexture()
local x, y = panel:LocalToScreen( 0, 0 )
surface.DrawTexturedRect( -x, -y, ScrW(), ScrH() )
end
local cache = {}
-- Get a material given a path to a material or .png file.
function StyledTheme.GetMaterial( path )
if cache[path] then
return cache[path]
end
cache[path] = Material( path, "smooth ignorez" )
return cache[path]
end
local GetMaterial = StyledTheme.GetMaterial
local DrawTexturedRect = surface.DrawTexturedRect
local COLOR_WHITE = Color( 255, 255, 255, 255 )
--- Draw a icon, using the specified image file path and color.
--- It allows overriding the alpha while keeping the supplied color table intact.
function StyledTheme.DrawIcon( path, x, y, w, h, alpha, color )
color = color or COLOR_WHITE
alpha = alpha or 1
SetMaterial( GetMaterial( path ) )
SetDrawColor( color.r, color.g, color.b, 255 * alpha )
DrawTexturedRect( x, y, w, h )
end
end
--[[
Utility function to apply the theme to existing VGUI panels
]]
do
local ClassFunctions = {}
function StyledTheme.Apply( panel, classOverride )
local funcs = ClassFunctions[classOverride or panel.ClassName]
if not funcs then return end
if funcs.Prepare then
funcs.Prepare( panel )
end
if funcs.Paint then
panel.Paint = funcs.Paint
end
if funcs.UpdateColours then
panel.UpdateColours = funcs.UpdateColours
end
if funcs.Close then
panel.Close = funcs.Close
end
end
local colors = StyledTheme.colors
local dimensions = StyledTheme.dimensions
local DrawRect = StyledTheme.DrawRect
ClassFunctions["DLabel"] = {
Prepare = function( self )
self:SetColor( colors.labelText )
self:SetFont( "StyledTheme_Small" )
end
}
ClassFunctions["DPanel"] = {
Paint = function( self, w, h )
DrawRect( 0, 0, w, h, self:GetBackgroundColor() or colors.panelBackground )
end
}
local function CustomMenuAdd( self, class )
local pnl = self:OriginalAdd( class )
if class == "DButton" then
StyledTheme.Apply( pnl )
timer.Simple( 0, function()
if not IsValid( pnl ) then return end
pnl:SetPaintBackground( true )
pnl:SizeToContentsX( StyledTheme.ScaleSize( 20 ) )
pnl:DockMargin( 0, 0, dimensions.menuPadding, 0 )
end )
end
return pnl
end
ClassFunctions["DMenuBar"] = {
Prepare = function( self )
self:SetTall( dimensions.buttonHeight )
self:DockMargin( 0, 0, 0, 0 )
self:DockPadding( dimensions.menuPadding, dimensions.menuPadding, dimensions.menuPadding, dimensions.menuPadding )
self.OriginalAdd = self.Add
self.Add = CustomMenuAdd
end,
Paint = function( self, w, h )
DrawRect( 0, 0, w, h, self:GetBackgroundColor() or colors.accent )
end
}
local Lerp = Lerp
local FrameTime = FrameTime
ClassFunctions["DButton"] = {
Prepare = function( self )
self:SetFont( "StyledTheme_Small" )
self:SetTall( dimensions.buttonHeight )
self.animHover = 0
self.animPress = 0
end,
Paint = function( self, w, h )
local dt = FrameTime() * 10
local enabled = self:IsEnabled()
self.animHover = Lerp( dt, self.animHover, ( enabled and self.Hovered ) and 1 or 0 )
self.animPress = Lerp( dt, self.animPress, ( enabled and ( self:IsDown() or self.m_bSelected ) ) and 1 or 0 )
DrawRect( 0, 0, w, h, ( self.isToggle and self.isChecked ) and colors.buttonPress or colors.buttonBorder )
DrawRect( 1, 1, w - 2, h - 2, enabled and colors.panelBackground or colors.panelDisabledBackground )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonHover, self.animHover )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonPress, self.animPress )
end,
UpdateColours = function( self )
if self:IsEnabled() then
self:SetTextStyleColor( colors.buttonText )
else
self:SetTextStyleColor( colors.buttonTextDisabled )
end
end
}
ClassFunctions["DBinder"] = ClassFunctions["DButton"]
ClassFunctions["DTextEntry"] = {
Prepare = function( self )
self:SetFont( "StyledTheme_Small" )
self:SetTall( dimensions.buttonHeight )
self:SetDrawBorder( false )
self:SetPaintBackground( false )
self:SetTextColor( colors.entryText )
self:SetCursorColor( colors.entryText )
self:SetHighlightColor( colors.entryHighlight )
self:SetPlaceholderColor( colors.entryPlaceholder )
end,
Paint = function( self, w, h )
local enabled = self:IsEnabled()
DrawRect( 0, 0, w, h, ( self:IsEditing() and enabled ) and colors.entryHighlight or colors.entryBorder )
DrawRect( 1, 1, w - 2, h - 2, enabled and colors.entryBackground or colors.panelDisabledBackground )
derma.SkinHook( "Paint", "TextEntry", self, w, h )
end
}
ClassFunctions["DComboBox"] = {
Prepare = function( self )
self:SetFont( "StyledTheme_Small" )
self:SetTall( dimensions.buttonHeight )
self:SetTextColor( colors.entryText )
self.animHover = 0
end,
Paint = function( self, w, h )
local dt = FrameTime() * 10
local enabled = self:IsEnabled()
self.animHover = Lerp( dt, self.animHover, ( enabled and self.Hovered ) and 1 or 0 )
DrawRect( 0, 0, w, h, ( self:IsMenuOpen() and enabled ) and colors.entryHighlight or colors.buttonBorder )
DrawRect( 1, 1, w - 2, h - 2, enabled and colors.panelBackground or colors.panelDisabledBackground )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonHover, self.animHover )
end
}
ClassFunctions["DNumSlider"] = {
Prepare = function( self )
StyledTheme.Apply( self.TextArea )
StyledTheme.Apply( self.Label )
end
}
ClassFunctions["DScrollPanel"] = {
Prepare = function( self )
StyledTheme.Apply( self.VBar )
local padding = dimensions.scrollPadding
self.pnlCanvas:DockPadding( padding, padding, padding, padding )
self:SetPaintBackground( true )
end,
Paint = function( self, w, h )
if self:GetPaintBackground() then
DrawRect( 0, 0, w, h, colors.scrollBackground )
end
end
}
local Clamp = math.Clamp
local function AddScroll( self, delta )
local oldScroll = self.animTargetScroll or self:GetScroll()
local newScroll = Clamp( oldScroll + delta * 40, 0, self.CanvasSize )
if oldScroll == newScroll then
return false
end
self:Stop()
self.animTargetScroll = newScroll
local anim = self:NewAnimation( 0.4, 0, 0.25, function( _, pnl )
pnl.animTargetScroll = nil
end )
anim.StartPos = oldScroll
anim.TargetPos = newScroll
anim.Think = function( a, pnl, fraction )
pnl:SetScroll( Lerp( fraction, a.StartPos, a.TargetPos ) )
end
return true
end
local function DrawGrip( self, w, h )
local dt = FrameTime() * 10
self.animHover = Lerp( dt, self.animHover, self.Hovered and 1 or 0 )
self.animPress = Lerp( dt, self.animPress, self.Depressed and 1 or 0 )
DrawRect( 0, 0, w, h, colors.buttonBorder )
DrawRect( 1, 1, w - 2, h - 2, colors.panelBackground )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonHover, self.animHover )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonPress, self.animPress )
end
ClassFunctions["DVScrollBar"] = {
Prepare = function( self )
self:SetWide( dimensions.scrollBarWidth )
self:SetHideButtons( true )
self.AddScroll = AddScroll
self.btnGrip.animHover = 0
self.btnGrip.animPress = 0
self.btnGrip.Paint = DrawGrip
end,
Paint = function( _, w, h )
DrawRect( 0, 0, w, h, colors.scrollBackground )
end
}
local function FrameSlideAnim( anim, panel, fraction )
if not anim.StartPos then
anim.StartPos = Vector( panel.x, panel.y + anim.StartOffset, 0 )
anim.TargetPos = Vector( panel.x, panel.y + anim.EndOffset, 0 )
end
local pos = LerpVector( fraction, anim.StartPos, anim.TargetPos )
panel:SetPos( pos.x, pos.y )
panel:SetAlpha( 255 * Lerp( fraction, anim.StartAlpha, anim.EndAlpha ) )
end
local function FramePerformLayout( self, w )
local padding = dimensions.framePadding
local buttonSize = dimensions.frameButtonSize
self.btnClose:SetSize( buttonSize, buttonSize )
self.btnClose:SetPos( w - self.btnClose:GetWide() - padding, padding )
local iconMargin = 0
if IsValid( self.imgIcon ) then
local iconSize = buttonSize * 0.6
self.imgIcon:SetPos( padding, padding + ( buttonSize * 0.5 ) - ( iconSize * 0.5 ) )
self.imgIcon:SetSize( iconSize, iconSize )
iconMargin = iconSize + padding * 0.5
end
self.lblTitle:SetPos( padding + iconMargin, padding )
self.lblTitle:SetSize( w - ( padding * 2 ) - iconMargin, buttonSize )
end
ClassFunctions["DFrame"] = {
Prepare = function( self )
self._OriginalClose = self.Close
self.PerformLayout = FramePerformLayout
StyledTheme.Apply( self.btnClose )
StyledTheme.Apply( self.lblTitle )
local padding = dimensions.framePadding
local buttonSize = dimensions.frameButtonSize
self:DockPadding( padding, buttonSize + padding * 2, padding, padding )
self.btnClose:SetText( "X" )
if IsValid( self.btnMaxim ) then
self.btnMaxim:Remove()
end
if IsValid( self.btnMinim ) then
self.btnMinim:Remove()
end
local anim = self:NewAnimation( 0.4, 0, 0.25 )
anim.StartOffset = -80
anim.EndOffset = 0
anim.StartAlpha = 0
anim.EndAlpha = 1
anim.Think = FrameSlideAnim
end,
Close = function( self )
self:SetMouseInputEnabled( false )
self:SetKeyboardInputEnabled( false )
if self.OnStartClosing then
self.OnStartClosing()
end
local anim = self:NewAnimation( 0.2, 0, 0.5, function()
self:_OriginalClose()
end )
anim.StartOffset = 0
anim.EndOffset = -80
anim.StartAlpha = 1
anim.EndAlpha = 0
anim.Think = FrameSlideAnim
end,
Paint = function( self, w, h )
if self.m_bBackgroundBlur then
Derma_DrawBackgroundBlur( self, self.m_fCreateTime )
else
StyledTheme.BlurPanel( self )
end
DrawRect( 0, 0, w, h, colors.panelBackground, self:GetAlpha() / 255 )
end
}
end
--[[
Utility functions to create "form" panels.
]]
do
local colors = StyledTheme.colors
local dimensions = StyledTheme.dimensions
function StyledTheme.CreateFormHeader( parent, text, mtop, mbottom )
mtop = mtop or dimensions.formSeparator
mbottom = mbottom or dimensions.formSeparator
local panel = vgui.Create( "DPanel", parent )
panel:SetTall( dimensions.headerHeight )
panel:Dock( TOP )
panel:DockMargin( -dimensions.formPadding, mtop, -dimensions.formPadding, mbottom )
panel:SetBackgroundColor( colors.scrollBackground )
StyledTheme.Apply( panel )
local label = vgui.Create( "DLabel", panel )
label:SetText( text )
label:SetContentAlignment( 5 )
label:SizeToContents()
label:Dock( FILL )
StyledTheme.Apply( label )
return panel
end
function StyledTheme.CreateFormLabel( parent, text )
local label = vgui.Create( "DLabel", parent )
label:Dock( TOP )
label:DockMargin( 0, 0, 0, dimensions.formSeparator )
label:SetText( text )
label:SetTall( dimensions.buttonHeight )
StyledTheme.Apply( label )
return label
end
function StyledTheme.CreateFormButton( parent, label, callback )
local button = vgui.Create( "DButton", parent )
button:SetText( label )
button:Dock( TOP )
button:DockMargin( 0, 0, 0, dimensions.formSeparator )
button.DoClick = callback
StyledTheme.Apply( button )
return button
end
function StyledTheme.CreateFormToggle( parent, label, isChecked, callback )
local button = vgui.Create( "DButton", parent )
button:SetIcon( isChecked and "icon16/accept.png" or "icon16/cancel.png" )
button:SetText( label )
button:Dock( TOP )
button:DockMargin( 0, 0, 0, dimensions.formSeparator )
button.isToggle = true
button.isChecked = isChecked
StyledTheme.Apply( button )
button.SetChecked = function( s, value )
value = value == true
s.isChecked = value
button:SetIcon( value and "icon16/accept.png" or "icon16/cancel.png" )
callback( value )
end
button.DoClick = function( s )
s:SetChecked( not s.isChecked )
end
return button
end
function StyledTheme.CreateFormSlider( parent, label, default, min, max, decimals, callback )
local slider = vgui.Create( "DNumSlider", parent )
slider:SetText( label )
slider:SetMin( min )
slider:SetMax( max )
slider:SetValue( default )
slider:SetDecimals( decimals )
slider:Dock( TOP )
slider:DockMargin( 0, 0, 0, dimensions.formSeparator )
slider.PerformLayout = function( s )
s.Label:SetWide( dimensions.formLabelWidth )
end
StyledTheme.Apply( slider )
slider.OnValueChanged = function( _, value )
callback( decimals == 0 and math.floor( value ) or math.Round( value, decimals ) )
end
return slider
end
function StyledTheme.CreateFormCombo( parent, text, options, defaultIndex, callback )
local panel = vgui.Create( "DPanel", parent )
panel:SetTall( dimensions.buttonHeight )
panel:SetPaintBackground( false )
panel:Dock( TOP )
panel:DockMargin( 0, 0, 0, dimensions.formSeparator )
local label = vgui.Create( "DLabel", panel )
label:Dock( LEFT )
label:DockMargin( 0, 0, 0, 0 )
label:SetText( text )
label:SetWide( dimensions.formLabelWidth )
StyledTheme.Apply( label )
local combo = vgui.Create( "DComboBox", panel )
combo:Dock( FILL )
combo:SetSortItems( false )
for _, v in ipairs( options ) do
combo:AddChoice( v )
end
if defaultIndex then
combo:ChooseOptionID( defaultIndex )
end
StyledTheme.Apply( combo )
combo.OnSelect = function( _, index )
callback( index )
end
end
function StyledTheme.CreateFormBinder( parent, text, defaultKey )
local panel = vgui.Create( "DPanel", parent )
panel:SetTall( dimensions.buttonHeight )
panel:SetPaintBackground( false )
panel:Dock( TOP )
panel:DockMargin( 0, 0, 0, dimensions.formSeparator )
local label = vgui.Create( "DLabel", panel )
label:Dock( LEFT )
label:DockMargin( 0, 0, 0, 0 )
label:SetText( text )
label:SetWide( dimensions.formLabelWidth )
StyledTheme.Apply( label )
local binder = vgui.Create( "DBinder", panel )
binder:SetValue( defaultKey or KEY_NONE )
binder:Dock( FILL )
StyledTheme.Apply( binder )
return binder
end
end

View File

@@ -0,0 +1,160 @@
--[[
StyledStrike's VGUI theme utilities
This file adds a new panel class: the tabbed frame
]]
if not StyledTheme then
error( "styled_theme.lua must be included first!" )
end
local colors = StyledTheme.colors
local dimensions = StyledTheme.dimensions
local TAB_BUTTON = {}
AccessorFunc( TAB_BUTTON, "iconPath", "Icon", FORCE_STRING )
function TAB_BUTTON:Init()
self:SetCursor( "hand" )
self:SetIcon( "icon16/bricks.png" )
self.isSelected = false
self.notificationCount = 0
self.animHover = 0
end
function TAB_BUTTON:OnMousePressed( keyCode )
if keyCode == MOUSE_LEFT then
self:GetParent():GetParent():SetActiveTab( self.tab )
end
end
local Lerp = Lerp
local FrameTime = FrameTime
local DrawRect = StyledTheme.DrawRect
local DrawIcon = StyledTheme.DrawIcon
local COLOR_INDICATOR = Color( 200, 0, 0, 255 )
function TAB_BUTTON:Paint( w, h )
self.animHover = Lerp( FrameTime() * 10, self.animHover, self:IsHovered() and 1 or 0 )
DrawRect( 0, 0, w, h, colors.buttonBorder )
DrawRect( 1, 1, w - 2, h - 2, colors.panelBackground )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonHover, self.animHover )
if self.isSelected then
DrawRect( 1, 1, w - 2, h - 2, colors.buttonPress )
end
local iconSize = math.floor( math.max( w, h ) * 0.5 )
DrawIcon( self.iconPath, ( w * 0.5 ) - ( iconSize * 0.5 ), ( h * 0.5 ) - ( iconSize * 0.5 ), iconSize, iconSize )
if self.notificationCount > 0 then
local size = dimensions.indicatorSize
local margin = math.floor( h * 0.05 )
local x = w - size - margin
local y = h - size - margin
draw.RoundedBox( size * 0.5, x, y, size, size, COLOR_INDICATOR )
draw.SimpleText( self.notificationCount, "StyledTheme_Tiny", x + size * 0.5, y + size * 0.5, colors.buttonText, 1, 1 )
end
end
vgui.Register( "Styled_TabButton", TAB_BUTTON, "DPanel" )
local TABBED_FRAME = {}
local ScaleSize = StyledTheme.ScaleSize
function TABBED_FRAME:Init()
StyledTheme.Apply( self, "DFrame" )
local w = ScaleSize( 850 )
local h = ScaleSize( 600 )
self:SetSize( w, h )
self:SetSizable( true )
self:SetDraggable( true )
self:SetDeleteOnClose( true )
self:SetScreenLock( true )
self:SetMinWidth( w )
self:SetMinHeight( h )
self.tabList = vgui.Create( "DPanel", self )
self.tabList:SetWide( ScaleSize( 64 ) )
self.tabList:Dock( LEFT )
self.tabList:DockPadding( 0, 0, 0, 0 )
self.tabList:SetPaintBackground( false )
--StyledTheme.Apply( self.tabList )
self.contentContainer = vgui.Create( "DPanel", self )
self.contentContainer:Dock( FILL )
self.contentContainer:DockMargin( ScaleSize( 4 ), 0, 0, 0 )
self.contentContainer:DockPadding( 0, 0, 0, 0 )
self.contentContainer:SetPaintBackground( false )
self.tabs = {}
end
function TABBED_FRAME:AddTab( icon, tooltip, panelClass )
panelClass = panelClass or "DScrollPanel"
local tab = {}
tab.button = vgui.Create( "Styled_TabButton", self.tabList )
tab.button:SetIcon( icon )
tab.button:SetTall( ScaleSize( 64 ) )
tab.button:SetTooltip( tooltip )
tab.button:Dock( TOP )
tab.button:DockMargin( 0, 0, 0, 2 )
tab.button.tab = tab
tab.panel = vgui.Create( panelClass, self.contentContainer )
tab.panel:Dock( FILL )
tab.panel:DockMargin( 0, 0, 0, 0 )
tab.panel:DockPadding( 0, 0, 0, 0 )
tab.panel:SetVisible( false )
StyledTheme.Apply( tab.panel )
if panelClass == "DScrollPanel" then
local padding = dimensions.formPadding
tab.panel.pnlCanvas:DockPadding( padding, 0, padding, padding )
end
self.tabs[#self.tabs + 1] = tab
if #self.tabs == 1 then
self:SetActiveTab( tab )
end
return tab.panel
end
function TABBED_FRAME:SetActiveTab( tab )
for i, t in ipairs( self.tabs ) do
local isThisOne = t == tab
t.button.isSelected = isThisOne
t.panel:SetVisible( isThisOne )
if isThisOne then
self.lastTabIndex = i
end
end
end
function TABBED_FRAME:SetActiveTabByIndex( index )
if self.tabs[index] then
self:SetActiveTab( self.tabs[index] )
end
end
function TABBED_FRAME:SetTabNotificationCountByIndex( index, count )
if self.tabs[index] then
self.tabs[index].button.notificationCount = count
end
end
vgui.Register( "Styled_TabbedFrame", TABBED_FRAME, "DFrame" )

View File

@@ -0,0 +1,63 @@
local Config = SquadMenu.Config or {}
SquadMenu.Config = Config
function Config:Reset()
self.showMembers = true
self.showRings = true
self.showHalos = false
self.enableSounds = true
self.nameDistance = 3000
self.haloDistance = 8000
self.pingKey = KEY_B
end
function Config:Load()
self:Reset()
local data = file.Read( SquadMenu.DATA_FILE, "DATA" )
if not data then return end
data = SquadMenu.JSONToTable( data )
self.showMembers = data.showMembers == true
self.showRings = data.showRings == true
self.showHalos = data.showHalos == true
self.enableSounds = data.enableSounds == true
self.nameDistance = SquadMenu.ValidateNumber( data.nameDistance, 3000, 500, 50000 )
self.haloDistance = SquadMenu.ValidateNumber( data.haloDistance, 8000, 500, 50000 )
self.pingKey = math.floor( SquadMenu.ValidateNumber( data.pingKey, KEY_B, 1, 159 ) )
end
function Config:Save( immediate )
if not immediate then
-- avoid spamming the file system
timer.Remove( "SquadMenu.SaveConfigDelay" )
timer.Create( "SquadMenu.SaveConfigDelay", 0.5, 1, function()
self:Save( true )
end )
return
end
local path = SquadMenu.DATA_FILE
local data = SquadMenu.TableToJSON( {
showMembers = self.showMembers,
showRings = self.showRings,
showHalos = self.showHalos,
enableSounds = self.enableSounds,
pingKey = self.pingKey
} )
SquadMenu.PrintF( "%s: writing %s", path, string.NiceSize( string.len( data ) ) )
file.Write( path, data )
if SquadMenu.mySquad then
SquadMenu:UpdateMembersHUD()
end
end
Config:Load()

View File

@@ -0,0 +1,344 @@
local squad
local nameDistance, haloDistance
function SquadMenu:RemoveMembersHUD()
if self.membersPanel then
self.membersPanel:Remove()
end
self.membersPanel = nil
hook.Remove( "PrePlayerDraw", "SquadMenu.DrawRing" )
hook.Remove( "PreDrawHalos", "SquadMenu.DrawHalos" )
hook.Remove( "HUDPaint", "SquadMenu.DrawHUD" )
hook.Remove( "HUDDrawTargetID", "SquadMenu.HideTargetInfo" )
hook.Remove( "PlayerButtonDown", "SquadMenu.PingKey" )
end
function SquadMenu:UpdateMembersHUD()
local config = self.Config
squad = self.mySquad
nameDistance = config.nameDistance * config.nameDistance
haloDistance = config.haloDistance * config.haloDistance
if config.showRings and self.mySquad.enableRings then
hook.Add( "PrePlayerDraw", "SquadMenu.DrawRing", self.DrawRing )
else
hook.Remove( "PrePlayerDraw", "SquadMenu.DrawRing" )
end
if config.showHalos then
hook.Add( "PreDrawHalos", "SquadMenu.DrawHalos", self.DrawHalos )
else
hook.Remove( "PreDrawHalos", "SquadMenu.DrawHalos" )
end
hook.Add( "HUDPaint", "SquadMenu.DrawHUD", self.DrawHUD )
hook.Add( "HUDDrawTargetID", "SquadMenu.HideTargetInfo", self.HideTargetInfo )
hook.Add( "PlayerButtonDown", "SquadMenu.PingKey", self.PingKey )
if self.membersPanel then
self.membersPanel:SetVisible( config.showMembers )
return
end
local panel = vgui.Create( "DPanel" )
panel:SetVisible( config.showMembers )
panel:SetPaintBackground( false )
panel:ParentToHUD()
panel._OriginalInvalidateLayout = panel.InvalidateLayout
panel.InvalidateLayout = function( s, layoutNow )
local screenW, screenH = ScrW(), ScrH()
local childCount = #s:GetChildren()
local childH = math.Round( screenH * 0.04 )
local childOffset = math.max( 1, math.Round( screenH * 0.002 ) )
local w = screenH * 0.22
local h = ( childH + childOffset ) * childCount
s.childH = childH
s.childOffset = childOffset
s:SetSize( w, h )
local position = math.Clamp( SquadMenu.GetMembersPosition(), 1, 9 )
local offset = screenH * 0.02
local x, y
if position == 1 or position == 4 or position == 7 then
-- left
x = offset
elseif position == 2 or position == 5 or position == 8 then
-- center
x = ( screenW * 0.5 ) - ( w * 0.5 )
else
-- right
x = screenW - w - offset
end
if position == 7 or position == 8 or position == 9 then
-- top
y = offset
elseif position == 4 or position == 5 or position == 6 then
-- center
y = ( screenH * 0.5 ) - ( h * 0.5 )
else
-- bottom
y = screenH - h - offset
end
s:SetPos( x, y )
s:_OriginalInvalidateLayout( layoutNow )
end
panel.PerformLayout = function( s, w )
local children = s:GetChildren()
if #children == 0 then return end
local offset = s.childOffset or 1
local height = s.childH or 24
local y = 0
for _, p in ipairs( children ) do
p:SetSize( w, height )
p:SetPos( 0, y )
y = y + height + offset
end
end
self.membersPanel = panel
end
function SquadMenu:AddMemberToHUD( member )
member.panel = vgui.Create( "Squad_MemberInfo", self.membersPanel )
member.panel:SetPlayer( member.id, member.name, squad )
self.membersPanel:InvalidateLayout()
end
function SquadMenu:RemoveMemberFromHUD( member )
if member.panel then
member.panel:Remove()
member.panel = nil
end
end
----------
local COLORS = {
WHITE = Color( 255, 255, 255, 255 ),
HEALTH = Color( 94, 253, 255, 255 ),
LOW_HEALTH = Color( 250, 20, 20, 255 ),
BOX_BG = Color( 0, 0, 0, 200 )
}
local SetColor = surface.SetDrawColor
local DrawRect = surface.DrawRect
local DrawOutlinedRect = surface.DrawOutlinedRect
local DrawHealthBar = function( x, y, w, h, health, armor )
if armor > 0 then
SetColor( 255, 255, 255, 255 )
DrawOutlinedRect( x - 1, y - 1, ( w + 2 ) * armor, h + 2, 1 )
end
SetColor( 20, 20, 20, 255 )
DrawRect( x, y, w, h )
x, y = x + 1, y + 1
w, h = w - 2, h - 2
local color = health < 0.3 and COLORS.LOW_HEALTH or COLORS.HEALTH
SetColor( color:Unpack() )
DrawRect( x, y, w * health, h )
end
SquadMenu.DrawHealthBar = DrawHealthBar
----------
local EyePos = EyePos
local Clamp = math.Clamp
local SetMaterial = surface.SetMaterial
local DrawTexturedRect = surface.DrawTexturedRect
local LocalPlayer = LocalPlayer
local PID = SquadMenu.GetPlayerId
do
local Start3D2D = cam.Start3D2D
local End3D2D = cam.End3D2D
local ringMaxDist = 3000 * 3000
local ringAngle = Angle( 0, 0, 0 )
local ringOffset = Vector( 0, 0, 5 )
local ringMat = Material( "squad_menu/ring.png" )
SquadMenu.DrawRing = function( ply, flags )
if flags == 1 then return end
if ply == LocalPlayer() then return end
if not squad.membersById[PID( ply )] then return end
local pos = ply:GetPos()
local mult = Clamp( pos:DistToSqr( EyePos() ) / ringMaxDist, 0, 1 )
local size = 300 + 1000 * mult
Start3D2D( pos + ringOffset * mult, ringAngle, 0.08 )
SetMaterial( ringMat )
SetColor( squad.color:Unpack() )
DrawTexturedRect( -size * 0.5, -size * 0.5, size, size )
End3D2D()
end
end
----------
local AllPlayersById = SquadMenu.AllPlayersById
SquadMenu.DrawHalos = function()
local origin = EyePos()
local me = LocalPlayer()
local byId = AllPlayersById()
local i, t, dist = 0, {}
for _, member in ipairs( squad.members ) do
local ply = byId[member.id]
if ply and ply ~= me and ply:Alive() and not ply:IsDormant() then
dist = origin:DistToSqr( ply:EyePos() )
if dist < haloDistance then
i = i + 1
t[i] = ply
end
end
end
if i > 0 then
halo.Add( t, squad.color, 2, 2, 1, true, true )
end
end
----------
SquadMenu.HideTargetInfo = function()
local trace = util.TraceLine( util.GetPlayerTrace( LocalPlayer() ) )
if not trace.Hit or not trace.HitNonWorld then return end
local ply = trace.Entity
if not ply:IsPlayer() then return end
if squad.membersById[PID( ply )] and EyePos():DistToSqr( ply:EyePos() ) < nameDistance then
return false
end
end
----------
local DrawSimpleText = draw.SimpleText
local GetTextSize = surface.GetTextSize
local SetAlphaMultiplier = surface.SetAlphaMultiplier
local function DrawTag( ply )
local nick = ply:Nick()
local isAlive = ply:Alive()
local pos = ply:EyePos():ToScreen()
local boxW, boxH = GetTextSize( nick )
local x = pos.x - boxW * 0.5
local y = pos.y - boxH
SetColor( COLORS.BOX_BG:Unpack() )
DrawRect( x - 2, y - 2, boxW + 4, boxH + 6 )
if isAlive then
DrawSimpleText( nick, "SquadMenuInfo", pos.x, y, COLORS.WHITE, 1, 0 )
DrawHealthBar( x - 2, y + boxH + 4, boxW + 4, 4, Clamp( ply:Health() / 100, 0, 1 ), ply:Armor() / 100 )
else
DrawSimpleText( nick, "SquadMenuInfo", pos.x, y, COLORS.LOW_HEALTH, 1, 0 )
end
end
local DrawOutlinedText = draw.SimpleTextOutlined
local pingMat = Material( "squad_menu/ping.png", "smooth ignorez nocull" )
local pings = SquadMenu.pings or {}
SquadMenu.pings = pings
SquadMenu.DrawHUD = function()
surface.SetFont( "SquadMenuInfo" )
-- Draw player tags
local origin = EyePos()
local me = LocalPlayer()
local byId = AllPlayersById()
local dist
for _, member in ipairs( squad.members ) do
local ply = byId[member.id]
if ply and ply ~= me and not ply:IsDormant() then
dist = origin:DistToSqr( ply:EyePos() )
if dist < nameDistance then
SetAlphaMultiplier( 1 - dist / nameDistance )
DrawTag( ply )
end
end
end
-- Draw pings
local t = RealTime()
local sw, sh = ScrW(), ScrH()
local r, g, b = squad.color:Unpack()
local size = sh * 0.025
local border = sh * 0.01
SetMaterial( pingMat )
for id, p in pairs( pings ) do
if t > p.start + p.lifetime then
pings[id] = nil
else
local showAnim = Clamp( ( t - p.start ) * 6, 0, 1 )
local hideAnim = Clamp( p.start + p.lifetime - t, 0, 1 )
SetAlphaMultiplier( showAnim * hideAnim )
local pos = p.pos:ToScreen()
local x = Clamp( pos.x, border, sw - border )
local y = Clamp( pos.y, sh * 0.05, sh - border )
local iconY = y - size - ( size * ( 1 - showAnim ) )
SetColor( 0, 0, 0 )
DrawTexturedRect( 1 + x - size * 0.5, 1 + iconY, size, size )
SetColor( r, g, b )
DrawTexturedRect( x - size * 0.5, iconY, size, size )
DrawOutlinedText( p.label, "SquadMenuInfo", x, y - size - sh * 0.006, squad.color, 1, 4, 1, color_black )
end
end
SetAlphaMultiplier( 1 )
end
SquadMenu.PingKey = function( ply, button )
if ply ~= LocalPlayer() then return end
if not IsFirstTimePredicted() then return end
if button ~= SquadMenu.Config.pingKey then return end
local tr = ply:GetEyeTrace()
if not tr.Hit then return end
SquadMenu.StartCommand( SquadMenu.PING )
net.WriteVector( tr.HitPos )
net.SendToServer()
end

View File

@@ -0,0 +1,380 @@
function SquadMenu.GetLanguageText( id )
return language.GetPhrase( "squad_menu." .. id ):Trim()
end
function SquadMenu:PlayUISound( path )
if self.Config.enableSounds then
sound.Play( path, Vector(), 0, 120, 0.75 )
end
end
local L = SquadMenu.GetLanguageText
function SquadMenu.GlobalMessage( ... )
chat.AddText( SquadMenu.THEME_COLOR, "[" .. L( "title" ) .. "] ", Color( 255, 255, 255 ), ... )
end
function SquadMenu.SquadMessage( ... )
local squad = SquadMenu.mySquad
if not squad then return end
local contents = { color_white, "[", squad.color, squad.name, color_white, "] ", ... }
if CustomChat then
CustomChat:AddMessage( contents, "squad" )
else
chat.AddText( unpack( contents ) )
end
end
function SquadMenu.LeaveMySquad( buttonToBlank, leaveNow )
local squad = SquadMenu.mySquad
if not squad then return end
if not leaveNow and squad.leaderId == SquadMenu.GetPlayerId( LocalPlayer() ) then
Derma_Query( L"leave_leader", L"leave_squad", L"yes", function()
SquadMenu.LeaveMySquad( buttonToBlank, true )
end, L"no" )
return
end
if IsValid( buttonToBlank ) then
buttonToBlank:SetEnabled( false )
buttonToBlank:SetText( "..." )
end
SquadMenu.StartCommand( SquadMenu.LEAVE_SQUAD )
net.SendToServer()
end
--- If GMinimap is installed, update squad members' blips.
function SquadMenu:UpdatePlayerBlips( icon, color )
if not self.mySquad then return end
local me = LocalPlayer()
local byId = self.AllPlayersById()
for _, member in ipairs( self.mySquad.members ) do
local ply = byId[member.id]
if ply and ply ~= me then
ply:SetBlipIcon( icon )
ply:SetBlipColor( color )
end
end
end
--- Set the current members of the local player's squad.
--- Updates the HUD and shows join/leave messages (if `printMessages` is `true`).
---
--- `newMembers` is an array where items are also arrays
--- with a number (player id) and a string (player name).
function SquadMenu:SetMembers( newMembers, printMessages )
local members = self.mySquad.members
local membersById = self.mySquad.membersById
local keep = {}
-- Add new members that we do not have on our end
for _, m in ipairs( newMembers ) do
local id = m[1]
local member = { id = id, name = m[2] }
keep[id] = true
if not membersById[id] then
membersById[id] = member
members[#members + 1] = member
self:AddMemberToHUD( member )
if printMessages then
self.SquadMessage( string.format( L"member_joined", member.name ) )
end
end
end
local byId = self.AllPlayersById()
-- Remove members that we have locally but do not exist on `newMembers`.
-- Backwards loop because we use `table.remove`
for i = #members, 1, -1 do
local member = members[i]
local id = member.id
if not keep[id] then
membersById[id] = nil
table.remove( members, i )
self:RemoveMemberFromHUD( member )
if printMessages then
self.SquadMessage( string.format( L"member_left", member.name ) )
end
local ply = byId[id]
if IsValid( ply ) and GMinimap then
ply:SetBlipIcon( nil )
ply:SetBlipColor( nil )
end
end
end
end
--- Set the local player's squad.
--- `data` is a table that comes from `squad:GetBasicInfo`.
function SquadMenu:SetupSquad( data )
local squad = self.mySquad or { id = -1 }
local isUpdate = data.id == squad.id
self.mySquad = squad
squad.id = data.id
squad.name = data.name
squad.icon = data.icon
squad.leaderId = data.leaderId
squad.leaderName = data.leaderName or ""
squad.enableRings = data.enableRings
squad.friendlyFire = data.friendlyFire
squad.isPublic = data.isPublic
squad.color = Color( data.r, data.g, data.b )
if CustomChat and squad.name then
CustomChat:CreateCustomChannel( "squad", squad.name, squad.icon )
end
if not isUpdate then
squad.requests = {}
squad.members = {}
squad.membersById = {}
self:PlayUISound( "buttons/combine_button3.wav" )
self.SquadMessage( L"squad_welcome", squad.color, " " .. squad.name )
self.SquadMessage( L"chat_tip", " " .. table.concat( self.CHAT_PREFIXES, ", " ) )
end
self:UpdateMembersHUD()
self:SetMembers( data.members, isUpdate )
if IsValid( self.frame ) then
self:RequestSquadListUpdate()
self:UpdateSquadStatePanel()
self:UpdateRequestsPanel()
self:UpdateSquadMembersPanel()
self:UpdateSquadPropertiesPanel()
self.frame:SetActiveTabByIndex( 3 ) -- squad members
end
if GMinimap then
self:UpdatePlayerBlips( "gminimap/blips/npc_default.png", squad.color )
hook.Add( "CanSeePlayerBlip", "ShowSquadBlips", function( ply )
if ply:GetSquadID() == squad.id then return true, 50000 end
end )
end
end
function SquadMenu:OnLeaveSquad( reason )
if GMinimap then
self:UpdatePlayerBlips( nil, nil )
hook.Remove( "CanSeePlayerBlip", "ShowSquadBlips" )
end
local reasonText = {
[self.LEAVE_REASON_DELETED] = "deleted_squad",
[self.LEAVE_REASON_KICKED] = "kicked_from_squad"
}
if self.mySquad then
self.GlobalMessage( L( reasonText[reason] or "left_squad" ) )
self:PlayUISound( "buttons/combine_button2.wav" )
end
self.mySquad = nil
self:RemoveMembersHUD()
if IsValid( self.frame ) then
self:RequestSquadListUpdate()
self:UpdateSquadStatePanel()
self:UpdateRequestsPanel()
self:UpdateSquadMembersPanel()
self:UpdateSquadPropertiesPanel()
if self.frame.lastTabIndex ~= 5 then -- not in settings
self.frame:SetActiveTabByIndex( 1 ) -- squad list
end
end
if CustomChat then
CustomChat:RemoveCustomChannel( "squad" )
end
end
----------
local commands = {}
commands[SquadMenu.SQUAD_LIST] = function()
SquadMenu:UpdateSquadList( SquadMenu.ReadTable() )
end
commands[SquadMenu.SETUP_SQUAD] = function()
local data = SquadMenu.ReadTable()
SquadMenu:SetupSquad( data )
end
commands[SquadMenu.LEAVE_SQUAD] = function()
local reason = net.ReadUInt( 3 )
SquadMenu:OnLeaveSquad( reason )
end
commands[SquadMenu.REQUESTS_LIST] = function()
local squad = SquadMenu.mySquad
if not squad then return end
local requests = squad.requests
-- Remember which players have requested before
local alreadyRequested = {}
for _, member in ipairs( requests ) do
alreadyRequested[member.id] = true
end
-- Compare the new requests against what we already got
local requestsById = SquadMenu.ReadTable()
local newCount = 0
for id, name in pairs( requestsById ) do
if not alreadyRequested[id] then
-- This is a new request for us
requests[#requests + 1] = { id = id, name = name }
newCount = newCount + 1
SquadMenu.SquadMessage( string.format( L"request_message", name ) )
end
end
if newCount > 0 then
SquadMenu:PlayUISound( "buttons/combine_button1.wav" )
end
-- Remove requests we already got if they aren't on the new requests list
for i = #requests, 1, -1 do
local member = requests[i]
if not requestsById[member.id] then
table.remove( requests, i )
end
end
SquadMenu:UpdateRequestsPanel()
end
commands[SquadMenu.PING] = function()
local pos = net.ReadVector()
local label = net.ReadString()
local id = net.ReadString()
local ping = SquadMenu.pings[id]
if not ping then
ping = {}
end
ping.pos = pos
ping.label = label
ping.start = RealTime()
ping.lifetime = 5
SquadMenu.pings[id] = ping
if not SquadMenu.Config.enableSounds then return end
local eyePos = EyePos()
local soundDir = pos - eyePos
soundDir:Normalize()
sound.Play( "friends/friend_join.wav", eyePos + soundDir * 500, 100, 120, 1 )
end
commands[SquadMenu.BROADCAST_EVENT] = function()
local data = SquadMenu.ReadTable()
local event = data.event
SquadMenu.PrintF( "Event received: %s", event )
if event == "open_menu" then
SquadMenu:OpenFrame()
elseif event == "squad_position_changed" then
if SquadMenu.membersPanel then
SquadMenu.membersPanel:InvalidateLayout()
end
elseif event == "player_joined_squad" then
local squad = SquadMenu.mySquad
if not squad then return end
-- Remove this player from my requests list
local requests = squad.requests
for i, member in ipairs( requests ) do
if data.playerId == member.id then
table.remove( requests, i )
break
end
end
SquadMenu:UpdateRequestsPanel()
elseif event == "squad_created" or event == "squad_deleted" then
SquadMenu:RequestSquadListUpdate()
if event == "squad_created" and data.name and SquadMenu.GetShowCreationMessage() then
local color = Color( data.r, data.g, data.b )
SquadMenu.GlobalMessage( string.format( L"squad_created", data.leaderName ), color, " " .. data.name )
end
elseif event == "members_chat" then
local squad = SquadMenu.mySquad
if not squad then return end
SquadMenu.SquadMessage( squad.color, data.senderName, color_white, ": ", data.text )
end
end
net.Receive( "squad_menu.command", function()
local cmd = net.ReadUInt( SquadMenu.COMMAND_SIZE )
if not commands[cmd] then
SquadMenu.PrintF( "Received a unknown network command! (%d)", cmd )
return
end
commands[cmd]( ply, ent )
end )
concommand.Add(
"squad_menu",
function() SquadMenu:OpenFrame() end,
nil,
"Opens the squad menu."
)
if engine.ActiveGamemode() == "sandbox" then
list.Set(
"DesktopWindows",
"SquadMenuDesktopIcon",
{
title = SquadMenu.GetLanguageText( "title" ),
icon = "materials/squad_menu/squad_menu.png",
init = function() SquadMenu:OpenFrame() end
}
)
end

View File

@@ -0,0 +1,598 @@
local PID = SquadMenu.GetPlayerId
local L = SquadMenu.GetLanguageText
local ScaleSize = StyledTheme.ScaleSize
function SquadMenu:CloseFrame()
if IsValid( self.frame ) then
self.frame:Close()
end
end
function SquadMenu:OpenFrame()
if IsValid( self.frame ) then
self:CloseFrame()
return
end
local frame = vgui.Create( "Styled_TabbedFrame" )
frame:SetTitle( L"title" )
frame:Center()
frame:MakePopup()
frame.OnClose = function()
self.frame = nil
end
local h = ScaleSize( 550 )
frame:SetTall( h )
frame:SetMinHeight( h )
self.frame = frame
local panels = {}
frame._panels = panels
-- Squad state
local separation = ScaleSize( 4 )
panels.squadState = vgui.Create( "DPanel", frame )
panels.squadState:SetTall( ScaleSize( 40 ) )
panels.squadState:Dock( BOTTOM )
panels.squadState:DockMargin( separation, separation, 0, 0 )
panels.squadState:DockPadding( separation, separation, separation, separation )
-- Tabs
panels.squadList = frame:AddTab( "styledstrike/icons/bullet_list.png", L"tab.squad_list" )
panels.squadProperties = frame:AddTab( "styledstrike/icons/flag_two_tone.png", L"tab.squad_properties", "DPanel" )
panels.squadMembers = frame:AddTab( "styledstrike/icons/users.png", L"tab.squad_members", "DPanel" )
panels.joinRequests = frame:AddTab( "styledstrike/icons/user_add.png", L"tab.join_requests", "DPanel" )
panels.settings = frame:AddTab( "styledstrike/icons/cog.png", L"tab.settings" )
self:RequestSquadListUpdate()
self:UpdateSquadStatePanel()
self:UpdateRequestsPanel()
self:UpdateSquadMembersPanel()
self:UpdateSquadPropertiesPanel()
local squad = self.mySquad
if squad then
if #squad.members < 2 then
frame:SetActiveTabByIndex( 4 ) -- Join requests
else
frame:SetActiveTabByIndex( 3 ) -- Squad members
end
end
-- Settings
StyledTheme.CreateFormHeader( panels.settings, L"tab.settings", 0 )
StyledTheme.CreateFormSlider( panels.settings, L"settings.name_draw_distance", self.Config.nameDistance, 500, 50000, 0, function( value )
self.Config.nameDistance = self.ValidateNumber( value, 2000, 500, 50000 )
self.Config:Save()
end )
StyledTheme.CreateFormSlider( panels.settings, L"settings.halo_draw_distance", self.Config.haloDistance, 500, 50000, 0, function( value )
self.Config.haloDistance = self.ValidateNumber( value, 8000, 500, 50000 )
self.Config:Save()
end )
local binderPing = StyledTheme.CreateFormBinder( panels.settings, L"settings.ping_key", self.Config.pingKey )
binderPing.OnChange = function( _, key )
self.Config.pingKey = key
self.Config:Save()
end
StyledTheme.CreateFormToggle( panels.settings, L"settings.show_members", self.Config.showMembers, function( checked )
self.Config.showMembers = checked
self.Config:Save()
end )
StyledTheme.CreateFormToggle( panels.settings, L"settings.show_rings", self.Config.showRings, function( checked )
self.Config.showRings = checked
self.Config:Save()
end )
StyledTheme.CreateFormToggle( panels.settings, L"settings.show_halos", self.Config.showHalos, function( checked )
self.Config.showHalos = checked
self.Config:Save()
end )
StyledTheme.CreateFormToggle( panels.settings, L"settings.enable_sounds", self.Config.enableSounds, function( checked )
self.Config.enableSounds = checked
self.Config:Save()
end )
end
function SquadMenu:GetPanel( id )
if IsValid( self.frame ) then
return self.frame._panels[id]
end
end
function SquadMenu:UpdateSquadStatePanel()
local statePanel = self:GetPanel( "squadState" )
if not statePanel then return end
statePanel:Clear()
local squad = self.mySquad
local squadColor = squad and squad.color or Color( 0, 0, 0 )
statePanel.Paint = function( _, w, h )
surface.SetDrawColor( 20, 20, 20 )
surface.DrawRect( 0, 0, w, h )
surface.SetDrawColor( squadColor:Unpack() )
surface.DrawOutlinedRect( 0, 0, w, h, 1 )
end
local imageIcon = vgui.Create( "DImage", statePanel )
imageIcon:Dock( LEFT )
imageIcon:SetWide( ScaleSize( 32 ) )
imageIcon:SetImage( squad and squad.icon or "vgui/avatar_default" )
local labelName = vgui.Create( "DLabel", statePanel )
labelName:Dock( FILL )
labelName:DockMargin( ScaleSize( 8 ), 0, 0, 0 )
labelName:SetText( squad and squad.name or L"not_in_a_squad" )
StyledTheme.Apply( labelName )
if not squad then return end
local buttonLeave = vgui.Create( "DButton", statePanel )
buttonLeave:SetText( L"leave_squad" )
buttonLeave:SetWide( ScaleSize( 180 ) )
buttonLeave:Dock( RIGHT )
StyledTheme.Apply( buttonLeave )
buttonLeave.DoClick = function()
SquadMenu.LeaveMySquad( buttonLeave )
end
end
function SquadMenu:RequestSquadListUpdate( immediate )
timer.Remove( "SquadMenu.RequestListUpdate" )
local listPanel = self:GetPanel( "squadList" )
if not listPanel then return end
listPanel:Clear()
StyledTheme.CreateFormHeader( listPanel, L"fetching_data", 0 )
if not immediate then
-- Don't spam when this function gets called in quick succession
timer.Create( "SquadMenu.RequestListUpdate", 1, 1, function()
SquadMenu:RequestSquadListUpdate( true )
end )
return
end
self.StartCommand( self.SQUAD_LIST )
net.SendToServer()
end
function SquadMenu:UpdateSquadList( squads )
local listPanel = self:GetPanel( "squadList" )
if not listPanel then return end
listPanel:Clear()
if #squads == 0 then
StyledTheme.CreateFormHeader( listPanel, L"no_available_squads", 0 )
return
end
StyledTheme.CreateFormHeader( listPanel, L"tab.squad_list", 0 )
local separation = ScaleSize( 6 )
for _, squad in ipairs( squads ) do
local line = vgui.Create( "Squad_ListRow", listPanel )
line:SetSquad( squad )
line:Dock( TOP )
line:DockMargin( 0, 0, 0, separation )
end
end
function SquadMenu:UpdateRequestsPanel()
local requestsPanel = self:GetPanel( "joinRequests" )
if not requestsPanel then return end
requestsPanel:Clear()
local padding = StyledTheme.dimensions.scrollPadding
requestsPanel:DockPadding( padding, 0, padding, padding )
requestsPanel:SetPaintBackground( true )
requestsPanel:SetBackgroundColor( StyledTheme.colors.scrollBackground )
self.frame:SetTabNotificationCountByIndex( 4, 0 ) -- Join requests tab
local squad = self.mySquad
if not squad then
StyledTheme.CreateFormHeader( requestsPanel, L"not_in_a_squad", 0 )
return
end
if squad.leaderId ~= PID( LocalPlayer() ) then
StyledTheme.CreateFormHeader( requestsPanel, L"not_squad_leader", 0 )
return
end
local memberLimit = self.GetMemberLimit() - #squad.members
if memberLimit < 1 then
StyledTheme.CreateFormHeader( requestsPanel, L"member_limit_reached", 0 )
return
end
local requestsHeaderLabel = StyledTheme.CreateFormHeader( requestsPanel, L"requests_list", 0 ):GetChildren()[1]
local function UpdateMemberCount( current )
requestsHeaderLabel:SetText( L( "slots" ) .. ": " .. current .. "/" .. self.GetMemberLimit() )
end
UpdateMemberCount( #squad.members )
if squad.isPublic then
StyledTheme.CreateFormHeader( requestsPanel, L"no_requests_needed", 0 )
return
end
if #squad.requests == 0 then
StyledTheme.CreateFormHeader( requestsPanel, L"no_requests_yet", 0 )
return
end
self.frame:SetTabNotificationCountByIndex( 4, #squad.requests ) -- Join requests tab
local scrollRequests = vgui.Create( "DScrollPanel", requestsPanel )
scrollRequests:Dock( FILL )
scrollRequests:SetPaintBackground( false )
local buttonAccept
local acceptedPlayers = {}
local function OnClickAccept()
local ids = table.GetKeys( acceptedPlayers )
self.StartCommand( self.ACCEPT_REQUESTS )
self.WriteTable( ids )
net.SendToServer()
end
local function UpdateAcceptedCount( count )
UpdateMemberCount( #squad.members + count )
if buttonAccept then
buttonAccept:Remove()
buttonAccept = nil
end
if count == 0 then return end
buttonAccept = vgui.Create( "DButton", requestsPanel )
buttonAccept:SetText( L"accept" )
buttonAccept:Dock( BOTTOM )
buttonAccept.DoClick = OnClickAccept
buttonAccept._themeHighlight = true
StyledTheme.Apply( buttonAccept )
end
UpdateAcceptedCount( 0 )
local function OnClickRow( row )
local id = row._id
local count = #table.GetKeys( acceptedPlayers )
if acceptedPlayers[id] then
acceptedPlayers[id] = nil
count = count - 1
else
if count < memberLimit then
acceptedPlayers[id] = true
count = count + 1
else
Derma_Message( L"cannot_accept_more", L"title", L"ok" )
end
end
row.isChecked = acceptedPlayers[id] ~= nil
UpdateAcceptedCount( count )
end
local rowHeight = ScaleSize( 48 )
local rowPadding = ScaleSize( 6 )
local nameColor = Color( 255, 255, 255 )
local function OnPaintRow( row, w, h )
row._OriginalPaint( row, w, h )
draw.SimpleText( row._name, "StyledTheme_Small", rowHeight + rowPadding, h * 0.5, nameColor, 0, 1 )
end
local byId = SquadMenu.AllPlayersById()
local dimensions = StyledTheme.dimensions
for _, member in ipairs( squad.requests ) do
local row = vgui.Create( "DButton", scrollRequests )
row:SetText( "" )
row:Dock( TOP )
row:DockMargin( dimensions.formPadding, 0, dimensions.formPadding, dimensions.formSeparator )
row:DockPadding( rowPadding, rowPadding, rowPadding, rowPadding )
StyledTheme.Apply( row )
row._id = member.id
row._name = member.name
row.isToggle = true
row.isChecked = false
row.DoClick = OnClickRow
row:SetTall( rowHeight )
row._OriginalPaint = row.Paint
row.Paint = OnPaintRow
local avatar = vgui.Create( "AvatarImage", row )
avatar:Dock( LEFT )
avatar:SetWide( rowHeight - rowPadding * 2 )
if byId[member.id] then
avatar:SetPlayer( byId[member.id], 64 )
end
end
end
function SquadMenu:UpdateSquadMembersPanel()
local membersPanel = self:GetPanel( "squadMembers" )
if not membersPanel then return end
membersPanel:Clear()
local padding = StyledTheme.dimensions.scrollPadding
membersPanel:DockPadding( padding, 0, padding, padding )
membersPanel:SetPaintBackground( true )
membersPanel:SetBackgroundColor( StyledTheme.colors.scrollBackground )
local squad = self.mySquad
if not squad then
StyledTheme.CreateFormHeader( membersPanel, L"not_in_a_squad", 0 )
return
end
local memberCount = #squad.members
StyledTheme.CreateFormHeader( membersPanel, L( "slots" ) .. ": " .. memberCount .. "/" .. self.GetMemberLimit(), 0 )
if memberCount < 2 then
StyledTheme.CreateFormHeader( membersPanel, L"no_members", 0 )
return
end
local localId = PID( LocalPlayer() )
local isLocalPlayerLeader = squad.leaderId == localId
local membersScroll = vgui.Create( "DScrollPanel", membersPanel )
membersScroll:Dock( FILL )
membersScroll:DockMargin( 0, padding, 0, padding )
local OnClickKick = function( s )
s:SetEnabled( false )
s:SetText( "..." )
self.StartCommand( self.KICK )
net.WriteString( s._id )
net.SendToServer()
end
local rowHeight = ScaleSize( 48 )
local rowPadding = ScaleSize( 6 )
local colors = StyledTheme.colors
local DrawRect = StyledTheme.DrawRect
local function OnPaintRow( row, w, h )
DrawRect( 0, 0, w, h, colors.buttonBorder )
DrawRect( 1, 1, w - 2, h - 2, colors.panelBackground )
draw.SimpleText( row._name, "StyledTheme_Small", rowHeight + rowPadding, h * 0.5, colors.labelText, 0, 1 )
end
local byId = SquadMenu.AllPlayersById()
local dimensions = StyledTheme.dimensions
for _, member in ipairs( squad.members ) do
local row = vgui.Create( "Panel", membersScroll )
row:Dock( TOP )
row:DockMargin( dimensions.formPadding, 0, dimensions.formPadding, dimensions.formSeparator )
row:DockPadding( rowPadding, rowPadding, rowPadding, rowPadding )
row._name = member.name
row.Paint = OnPaintRow
row:SetTall( rowHeight )
local avatar = vgui.Create( "AvatarImage", row )
avatar:Dock( LEFT )
avatar:SetWide( rowHeight - rowPadding * 2 )
if byId[member.id] then
avatar:SetPlayer( byId[member.id], 64 )
end
if isLocalPlayerLeader and member.id ~= localId then
local kick = vgui.Create( "DButton", row )
kick:SetText( L"kick" )
kick:SetWide( ScaleSize( 100 ) )
kick:Dock( RIGHT )
kick._id = member.id
kick.DoClick = OnClickKick
StyledTheme.Apply( kick )
end
end
end
function SquadMenu:UpdateSquadPropertiesPanel()
local propertiesPanel = self:GetPanel( "squadProperties" )
if not propertiesPanel then return end
propertiesPanel:Clear()
local padding = StyledTheme.dimensions.scrollPadding
propertiesPanel:DockPadding( padding, 0, padding, padding )
propertiesPanel:SetPaintBackground( true )
propertiesPanel:SetBackgroundColor( StyledTheme.colors.scrollBackground )
local squad = self.mySquad
if squad and squad.leaderId ~= PID( LocalPlayer() ) then
StyledTheme.CreateFormHeader( propertiesPanel, L"leave_first_create", 0 )
return
end
local isNew = squad == nil
local oldName = squad and squad.name or nil
local oldColor = squad and squad.color or nil
if not oldColor then
local c = HSVToColor( math.random( 0, 360 ), 1, 1 )
oldColor = Color( c.r, c.g, c.b ) -- Reconstruct color instance to avoid a bug
end
squad = squad or {
enableRings = true
}
StyledTheme.CreateFormHeader( propertiesPanel, L( isNew and "create_squad" or "edit_squad" ), 0 )
local data = {
name = squad.name or string.format( L"default_squad_name", LocalPlayer():Nick() ),
icon = squad.icon or "icon16/flag_blue.png",
enableRings = squad.enableRings == true,
friendlyFire = squad.friendlyFire == true,
isPublic = squad.isPublic == true,
r = oldColor.r,
g = oldColor.g,
b = oldColor.b
}
local buttonCreate = vgui.Create( "DButton", propertiesPanel )
buttonCreate:SetTall( 36 )
buttonCreate:SetText( L( isNew and "create_squad" or "edit_squad" ) )
buttonCreate:Dock( BOTTOM )
buttonCreate:DockMargin( 0, ScaleSize( 8 ), 0, 0 )
StyledTheme.Apply( buttonCreate )
buttonCreate.DoClick = function( s )
s:SetEnabled( false )
s:SetText( "..." )
self.StartCommand( self.SETUP_SQUAD )
self.WriteTable( data )
net.SendToServer()
end
local leftPanel = vgui.Create( "DPanel", propertiesPanel )
leftPanel:Dock( FILL )
StyledTheme.Apply( leftPanel )
StyledTheme.CreateFormHeader( leftPanel, L"squad_name", 0, 0 )
local separator = ScaleSize( 6 )
local rowHeight = StyledTheme.dimensions.buttonHeight
local entryName = vgui.Create( "DTextEntry", leftPanel )
entryName:SetTall( rowHeight )
entryName:Dock( TOP )
entryName:DockMargin( separator, separator, separator, separator )
entryName:SetMaximumCharCount( self.MAX_NAME_LENGTH )
entryName:SetValue( data.name )
entryName.OnChange = function()
local value = entryName:GetValue()
data.name = value:Trim() == "" and oldName or value
end
StyledTheme.Apply( entryName )
StyledTheme.CreateFormHeader( leftPanel, L"tab.squad_properties", 0, 0 )
local buttonIcon = vgui.Create( "DButton", leftPanel )
buttonIcon:SetTall( rowHeight )
buttonIcon:SetIcon( data.icon )
buttonIcon:SetText( L"choose_icon" )
buttonIcon:Dock( TOP )
buttonIcon:DockMargin( separator, separator, separator, 0 )
StyledTheme.Apply( buttonIcon )
buttonIcon.DoClick = function()
local iconBrowser = vgui.Create( "DIconBrowser" )
iconBrowser:SetSize( 300, 200 )
local m = DermaMenu()
m:AddPanel( iconBrowser )
m:SetPaintBackground( false )
m:Open( gui.MouseX() + 8, gui.MouseY() + 10 )
iconBrowser.OnChange = function( s )
local iconPath = s:GetSelectedIcon()
buttonIcon:SetIcon( iconPath )
data.icon = iconPath
CloseDermaMenus()
end
end
StyledTheme.CreateFormToggle( leftPanel, L"squad_is_public", data.isPublic, function( checked )
data.isPublic = checked
end ):DockMargin( separator, separator, separator, 0 )
local ffButton = StyledTheme.CreateFormToggle( leftPanel, L"squad_friendly_fire", data.friendlyFire, function( checked )
data.friendlyFire = checked
end )
ffButton:DockMargin( separator, separator, separator, 0 )
if SquadMenu.GetForceFriendlyFire() then
ffButton:SetEnabled( false )
ffButton:SetIcon( "icon16/accept.png" )
ffButton:SetText( L"squad_force_friendly_fire" )
end
StyledTheme.CreateFormToggle( leftPanel, L"squad_rings", data.enableRings, function( checked )
data.enableRings = checked
end ):DockMargin( separator, separator, separator, 0 )
local rightPanel = vgui.Create( "DPanel", propertiesPanel )
rightPanel:SetWide( ScaleSize( 260 ) )
rightPanel:Dock( RIGHT )
rightPanel:DockMargin( separator, 0, 0, 0 )
StyledTheme.Apply( rightPanel )
StyledTheme.CreateFormHeader( rightPanel, L"squad_color", 0, 0 )
local colorPicker = vgui.Create( "DColorMixer", rightPanel )
colorPicker:Dock( FILL )
colorPicker:SetPalette( true )
colorPicker:SetAlphaBar( false )
colorPicker:SetWangs( true )
colorPicker:SetColor( oldColor )
colorPicker.ValueChanged = function( _, color )
data.r = color.r
data.g = color.g
data.b = color.b
end
end

View File

@@ -0,0 +1,100 @@
local IsValid = IsValid
local Clamp = math.Clamp
local RealTime = RealTime
local FrameTime = FrameTime
local Approach = math.Approach
local SetColor = surface.SetDrawColor
local SetMaterial = surface.SetMaterial
local DrawOutlinedText = draw.SimpleTextOutlined
local DrawTexturedRect = surface.DrawTexturedRect
local DrawRect = surface.DrawRect
local DrawHealthBar = SquadMenu.DrawHealthBar
local matGradient = Material( "vgui/gradient-r" )
local COLORS = {
WHITE = Color( 255, 255, 255, 255 ),
LOW_HEALTH = Color( 250, 20, 20, 255 ),
OUTLINE = Color( 0, 0, 0, 255 )
}
local PANEL = {}
function PANEL:Init()
self.avatar = vgui.Create( "AvatarImage", self )
self:InvalidateLayout()
self:SetPlayer()
end
function PANEL:SetPlayer( id, name, squad )
self.squad = squad
self.playerId = id
self.validateTimer = 0
self.name = SquadMenu.ValidateString( name, "", 20 )
self.health = 1
self.armor = 0
self.alive = true
self.healthAnim = 0
self.armorAnim = 0
end
function PANEL:Think()
if IsValid( self.ply ) then
self.health = Clamp( self.ply:Health() / 100, 0, 1 )
self.armor = Clamp( self.ply:Armor() / 100, 0, 1 )
self.alive = self.ply:Alive()
return
end
-- Keep trying to get the player entity periodically
if RealTime() < self.validateTimer then return end
self.validateTimer = RealTime() + 1
local ply = SquadMenu.FindPlayerById( self.playerId )
if ply then
self.ply = ply
self.name = SquadMenu.ValidateString( ply:Nick(), "", 20 )
self.avatar:SetPlayer( ply, 64 )
end
end
function PANEL:Paint( w, h )
local split = h
if self.squad then
SetColor( self.squad.color:Unpack() )
DrawRect( w - split, 0, split, h )
end
SetColor( 0, 0, 0, 240 )
SetMaterial( matGradient )
DrawTexturedRect( 0, 0, w - split, h )
local dt = FrameTime()
self.healthAnim = Approach( self.healthAnim, self.health, dt * 2 )
self.armorAnim = Approach( self.armorAnim, self.armor, dt )
if self.alive then
local barH = h * 0.2
DrawHealthBar( 2, h - barH - 5, w - split - 6, barH, self.healthAnim, self.armorAnim )
end
DrawOutlinedText( self.name, "SquadMenuInfo", 2, 2 + h * 0.5,
self.alive and COLORS.WHITE or COLORS.LOW_HEALTH, 0, self.alive and 4 or 1, 1, COLORS.OUTLINE )
end
function PANEL:PerformLayout( w, h )
local size = h - 4
self.avatar:SetSize( size, size )
self.avatar:SetPos( w - size - 2, 2 )
end
vgui.Register( "Squad_MemberInfo", PANEL, "DPanel" )

View File

@@ -0,0 +1,199 @@
local L = SquadMenu.GetLanguageText
local ScaleSize = StyledTheme.ScaleSize
local UpdateButton = function( button, text, enabled )
button:SetEnabled( enabled )
button:SetText( L( text ) )
button:SizeToContentsX( ScaleSize( 12 ) )
button:GetParent():InvalidateLayout()
end
local PANEL = {}
local COLOR_BLACK = Color( 0, 0, 0, 255 )
function PANEL:Init()
self.squad = {
id = 0,
name = "-",
leaderName = "-",
color = COLOR_BLACK
}
self:SetCursor( "hand" )
self:SetExpanded( false )
self.animHover = 0
self.collapsedHeight = ScaleSize( 52 )
self.padding = ScaleSize( 6 )
self.iconSize = self.collapsedHeight - self.padding * 2
self.icon = vgui.Create( "DImage", self )
self.icon:SetSize( self.iconSize, self.iconSize )
self.buttonJoin = vgui.Create( "DButton", self )
StyledTheme.Apply( self.buttonJoin )
self.buttonJoin:SetTall( self.collapsedHeight - self.padding * 2 )
self.buttonJoin.DoClick = function()
if self.leaveOnClick then
SquadMenu.LeaveMySquad( self.buttonJoin )
else
UpdateButton( self.buttonJoin, "waiting_response", false )
SquadMenu.StartCommand( SquadMenu.JOIN_SQUAD )
net.WriteUInt( self.squad.id, 16 )
net.SendToServer()
end
end
self.memberCount = vgui.Create( "DPanel", self )
self.memberCount:SetTall( self.collapsedHeight - self.padding * 2 )
self.memberCount:SetPaintBackground( false )
self:SetTall( self.collapsedHeight )
end
function PANEL:PerformLayout( w )
self.icon:SetPos( self.padding, self.padding )
local joinWidth = self.buttonJoin:GetWide()
self.buttonJoin:SetPos( w - joinWidth - self.padding, self.padding )
self.memberCount:SetPos( w - joinWidth - self.memberCount:GetWide() - self.padding * 2, self.padding )
end
local colors = StyledTheme.colors
local DrawRect = StyledTheme.DrawRect
function PANEL:Paint( w, h )
self.animHover = Lerp( FrameTime() * 10, self.animHover, self:IsHovered() and 1 or 0 )
DrawRect( 0, 0, w, h, self.squad.color )
DrawRect( 1, 1, w - 2, h - 2, COLOR_BLACK )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonHover, self.animHover )
local x = self.iconSize + self.padding * 2
local y = self.collapsedHeight * 0.5
draw.SimpleText( self.squad.name, "StyledTheme_Small", x, y, colors.labelText, 0, 4 )
draw.SimpleText( self.squad.leaderName or "<Server>", "StyledTheme_Tiny", x, y, colors.buttonTextDisabled, 0, 3 )
end
function PANEL:OnMousePressed( keyCode )
if keyCode == MOUSE_LEFT then
self:SetExpanded( not self.isExpanded, true )
end
end
--- Set the squad data.
--- `squad` is a table that comes from `squad:GetBasicInfo`.
function PANEL:SetSquad( squad )
squad.color = Color( squad.r, squad.g, squad.b )
self.squad = squad
self.icon:SetImage( squad.icon )
local maxMembers = SquadMenu.GetMemberLimit()
local count = #squad.members
self.leaveOnClick = squad.id == ( SquadMenu.mySquad and SquadMenu.mySquad.id or -1 )
if self.leaveOnClick then
UpdateButton( self.buttonJoin, "leave_squad", true )
elseif count < maxMembers then
UpdateButton( self.buttonJoin, squad.isPublic and "join" or "request_to_join", true )
else
UpdateButton( self.buttonJoin, "full_squad", false )
end
self.memberCount:Clear()
local labelCount = vgui.Create( "DLabel", self.memberCount )
labelCount:SetText( count .. "/" .. maxMembers )
labelCount:SizeToContents()
labelCount:Dock( FILL )
local labelWide = labelCount:GetWide()
local iconCount = vgui.Create( "DImage", self.memberCount )
iconCount:SetImage( "styledstrike/icons/users.png" )
iconCount:SetWide( self.collapsedHeight - self.padding * 4 )
iconCount:Dock( LEFT )
iconCount:DockMargin( 0, self.padding, 0, self.padding )
self.memberCount:SetWide( labelWide + iconCount:GetWide() )
end
function PANEL:SetExpanded( expanded, scroll )
self.isExpanded = expanded
local height = self.collapsedHeight
local memberHeight = ScaleSize( 32 )
if expanded then
height = height + self.padding + memberHeight * math.min( #self.squad.members, 5 )
end
self:SetTall( height )
self:InvalidateLayout()
if expanded and scroll then
self:GetParent():GetParent():ScrollToChild( self )
end
if self.membersScroll then
self.membersScroll:Remove()
self.membersScroll = nil
end
if not expanded then return end
local membersScroll = vgui.Create( "DScrollPanel", self )
membersScroll:Dock( FILL )
membersScroll:DockMargin( 0, self.collapsedHeight, 0, 0 )
membersScroll.pnlCanvas:DockPadding( 0, 0, 0, 0 )
self.membersScroll = membersScroll
local byId = SquadMenu.AllPlayersById()
local separation = ScaleSize( 2 )
local padding = ScaleSize( 4 )
for _, m in ipairs( self.squad.members ) do
local id = m[1]
local row = vgui.Create( "DPanel", membersScroll )
row:SetBackgroundColor( colors.panelBackground )
row:SetTall( memberHeight - separation )
row:Dock( TOP )
row:DockMargin( self.padding, 0, self.padding, separation )
local name = vgui.Create( "DLabel", row )
name:SetText( m[2] )
name:Dock( FILL )
local avatar = vgui.Create( "AvatarImage", row )
avatar:SetWide( memberHeight - padding * 2 )
avatar:Dock( LEFT )
avatar:DockMargin( padding, padding, padding, padding )
if byId[id] then
avatar:SetPlayer( byId[id], 64 )
end
if id == self.squad.leaderId then
row:SetZPos( -1 )
local leaderIcon = vgui.Create( "DImage", row )
leaderIcon:SetWide( memberHeight - padding * 2 )
leaderIcon:SetImage( "icon16/award_star_gold_3.png" )
leaderIcon:Dock( RIGHT )
leaderIcon:DockMargin( 0, padding, padding, padding )
end
end
end
vgui.Register( "Squad_ListRow", PANEL, "DPanel" )

View File

@@ -0,0 +1,5 @@
local PlayerMeta = FindMetaTable( "Player" )
function PlayerMeta:GetSquadID()
return self:GetNWInt( "squad_menu.id", -1 )
end

View File

@@ -0,0 +1,196 @@
resource.AddWorkshop( "3207278246" )
SquadMenu.blockDamage = SquadMenu.blockDamage or {}
SquadMenu.squads = SquadMenu.squads or {}
SquadMenu.lastSquadId = SquadMenu.lastSquadId or 0
--- Find a squad by it's ID.
function SquadMenu:GetSquad( id )
return self.squads[id]
end
--- Find and remove a squad by it's ID.
function SquadMenu:DeleteSquad( id )
local squad = self.squads[id]
if squad then
squad:Delete()
end
end
--- Remove a player id from join requests on all squads.
function SquadMenu:CleanupRequests( id, dontSync )
for _, squad in pairs( self.squads ) do
if squad.requestsById[id] then
squad.requestsById[id] = nil
if not dontSync then
squad:SyncRequests()
end
end
end
end
-- Updates the player name everywhere it appears.
function SquadMenu:UpdatePlayerName( ply )
if not IsValid( ply ) then return end
local id = self.GetPlayerId( ply )
local name = ply:Nick()
for _, squad in pairs( self.squads ) do
-- Update leader name
if squad.leaderId == id then
squad.leaderName = name
end
-- Update join request
if squad.requestsById[id] then
squad.requestsById[id] = name
squad:SyncRequests()
end
-- Update member name
if squad.membersById[id] then
squad.membersById[id] = name
squad:SyncWithMembers()
end
end
end
--- Create a new squad.
function SquadMenu:CreateSquad()
local id = self.lastSquadId + 1
self.lastSquadId = id
self.squads[id] = setmetatable( {
id = id,
name = "",
icon = "",
enableRings = false,
friendlyFire = false,
isPublic = false,
r = 255,
g = 255,
b = 255,
-- Members key-value table. Do not modify directly,
-- instead use squad:AddMember/squad:RemoveMember.
--
-- Each key is a player id from SquadMenu.GetPlayerId,
-- and each value is the player name.
membersById = {},
-- Join Requests key-value table.
--
-- Each key is a player id from SquadMenu.GetPlayerId,
-- and each value is the player name.
requestsById = {}
}, self.Squad )
self.PrintF( "Created squad #%d", id )
return self.squads[id]
end
-- Callbacks on FCVAR_REPLICATED cvars don't work clientside so we need this
cvars.AddChangeCallback( "squad_members_position", function()
SquadMenu.StartEvent( "squad_position_changed" )
net.Broadcast()
end, "changed_squad_members_position" )
--- Update player names on change
gameevent.Listen( "player_changename" )
hook.Add( "player_changename", "SquadMenu.UpdatePlayerName", function( data )
SquadMenu:UpdatePlayerName( Player( data.userid ) )
end )
--- Update player names on first spawn
hook.Add( "PlayerInitialSpawn", "SquadMenu.UpdatePlayerName", function( ply )
SquadMenu:UpdatePlayerName( ply )
end )
-- On disconnect, remove join requests and this player from their squad, if they have one
hook.Add( "PlayerDisconnected", "SquadMenu.PlayerCleanup", function( ply )
SquadMenu:CleanupRequests( SquadMenu.GetPlayerId( ply ) )
local squad = SquadMenu:GetSquad( ply:GetSquadID() )
if squad then
squad:RemoveMember( ply )
end
end )
--- Block damage between squad members
local blockDamage = SquadMenu.blockDamage
hook.Add( "PlayerShouldTakeDamage", "SquadMenu.BlockFriendlyFire", function( ply, attacker )
if not attacker.GetSquadID then return end
local id = ply:GetSquadID()
if id ~= -1 and ply ~= attacker and blockDamage[id] and id == attacker:GetSquadID() then
return false
end
end )
--- Chat commands and squad-only chat messages
local prefixes = {}
for _, prefix in ipairs( SquadMenu.CHAT_PREFIXES ) do
prefixes[prefix] = true
end
hook.Add( "PlayerSay", "SquadMenu.RemovePrefix", function( sender, text, _, channel )
-- Check for commands to open the menu
if text[1] == "!" then
text = string.lower( string.Trim( text ) )
if text == "!squad" or text == "!party" then
SquadMenu.StartEvent( "open_menu" )
net.Send( sender )
return ""
end
end
-- Check if this is supposed to be a members-only message
local isCustomChatSquadChannel = channel == "squad"
local parts = string.Explode( " ", text, false )
if not isCustomChatSquadChannel and ( not parts[1] or not prefixes[parts[1]] ) then return end
local id = sender:GetSquadID()
if id == -1 then
sender:ChatPrint( "You're not in a squad." )
return ""
end
if not isCustomChatSquadChannel then
table.remove( parts, 1 )
end
text = table.concat( parts, " " )
if text:len() == 0 then
sender:ChatPrint( "Please type a message to send to your squad members." )
return ""
end
local override = hook.Run( "SquadPlayerSay", sender, text )
if override ~= nil and ( not override or override == "" ) then return "" end
if type( override ) == "string" then text = override end
local members = SquadMenu:GetSquad( id ):GetActiveMembers()
SquadMenu.StartEvent( "members_chat", {
senderName = sender:Nick(),
text = text
} )
net.Send( members )
return ""
end, HOOK_HIGH )

View File

@@ -0,0 +1,182 @@
util.AddNetworkString( "squad_menu.command" )
function SquadMenu.StartEvent( event, data )
data = data or {}
data.event = event
SquadMenu.StartCommand( SquadMenu.BROADCAST_EVENT )
SquadMenu.WriteTable( data )
end
local commands = {}
local PID = SquadMenu.GetPlayerId
commands[SquadMenu.SQUAD_LIST] = function( ply )
local data = {}
for _, squad in pairs( SquadMenu.squads ) do
data[#data + 1] = squad:GetBasicInfo()
end
SquadMenu.StartCommand( SquadMenu.SQUAD_LIST )
SquadMenu.WriteTable( data )
net.Send( ply )
end
commands[SquadMenu.SETUP_SQUAD] = function( ply )
local squadId = ply:GetSquadID()
local plyId = PID( ply )
local data = SquadMenu.ReadTable()
if type( data.name ) == "string" then
local shouldAllow, name = hook.Run( "ShouldAllowSquadName", data.name, ply )
if shouldAllow == false then
data.name = name or "?"
end
end
-- Update existing squad if this ply is the leader.
if squadId ~= -1 then
local squad = SquadMenu:GetSquad( squadId )
if not squad then return end
if squad.leaderId ~= plyId then return end
squad:SetBasicInfo( data )
squad:SyncWithMembers()
-- Litterally all I need is that lol
hook.Run("SquadMenu_SquadUpdated", squadId, ply, ply:SteamID())
SquadMenu.PrintF( "Edited squad #%d for %s", squadId, ply:SteamID() )
SquadMenu.StartEvent( "squad_created", { id = squadId } )
net.Broadcast()
return
end
local squad = SquadMenu:CreateSquad()
squad:SetBasicInfo( data )
squad:SetLeader( ply )
squad:AddMember( ply )
SquadMenu.StartEvent( "squad_created", {
id = squadId,
name = squad.name,
leaderName = squad.leaderName,
r = squad.r,
g = squad.g,
b = squad.b
} )
net.Broadcast()
end
commands[SquadMenu.JOIN_SQUAD] = function( ply )
local squadId = net.ReadUInt( 16 )
local squad = SquadMenu:GetSquad( squadId )
if squad then
squad:RequestToJoin( ply )
end
end
commands[SquadMenu.LEAVE_SQUAD] = function( ply )
local squadId = ply:GetSquadID()
if squadId == -1 then return end
local squad = SquadMenu:GetSquad( squadId )
if squad then
squad:RemoveMember( ply, SquadMenu.LEAVE_REASON_LEFT )
end
end
commands[SquadMenu.ACCEPT_REQUESTS] = function( ply )
local squadId = ply:GetSquadID()
if squadId == -1 then return end
local squad = SquadMenu:GetSquad( squadId )
local ids = SquadMenu.ReadTable()
if squad and squad.leaderId == PID( ply ) then
squad:AcceptRequests( ids )
end
end
commands[SquadMenu.KICK] = function( ply )
local squadId = ply:GetSquadID()
if squadId == -1 then return end
local plyId = PID( ply )
local squad = SquadMenu:GetSquad( squadId )
if squad and squad.leaderId == plyId then
local targetId = net.ReadString()
if targetId == plyId then return end
local byId = SquadMenu.AllPlayersById()
if not byId[targetId] then return end
squad:RemoveMember( byId[targetId], SquadMenu.LEAVE_REASON_KICKED )
end
end
commands[SquadMenu.PING] = function( ply )
local squadId = ply:GetSquadID()
if squadId == -1 then return end
local squad = SquadMenu:GetSquad( squadId )
if not squad then return end
local pos = net.ReadVector()
local members, count = squad:GetActiveMembers()
if count == 0 then return end
SquadMenu.StartCommand( SquadMenu.PING )
net.WriteVector( pos )
net.WriteString( ply:Nick() )
net.WriteString( ply:SteamID() )
net.Send( members )
end
-- Safeguard against spam
local cooldowns = {
[SquadMenu.SQUAD_LIST] = { interval = 0.5, players = {} },
[SquadMenu.SETUP_SQUAD] = { interval = 1, players = {} },
[SquadMenu.JOIN_SQUAD] = { interval = 0.1, players = {} },
[SquadMenu.LEAVE_SQUAD] = { interval = 1, players = {} },
[SquadMenu.ACCEPT_REQUESTS] = { interval = 0.2, players = {} },
[SquadMenu.KICK] = { interval = 0.1, players = {} },
[SquadMenu.PING] = { interval = 0.5, players = {} }
}
net.Receive( "squad_menu.command", function( _, ply )
local id = ply:SteamID()
local cmd = net.ReadUInt( SquadMenu.COMMAND_SIZE )
if not commands[cmd] then
SquadMenu.PrintF( "%s <%s> sent a unknown network command! (%d)", ply:Nick(), id, cmd )
return
end
local t = RealTime()
local players = cooldowns[cmd].players
if players[id] and players[id] > t then
SquadMenu.PrintF( "%s <%s> sent network commands too fast!", ply:Nick(), id )
return
end
players[id] = t + cooldowns[cmd].interval
commands[cmd]( ply )
end )
hook.Add( "PlayerDisconnected", "SquadMenu.NetCleanup", function( ply )
local id = ply:SteamID()
for _, c in pairs( cooldowns ) do
c.players[id] = nil
end
end )

View File

@@ -0,0 +1,278 @@
local IsValid = IsValid
local PID = SquadMenu.GetPlayerId
local FindByPID = SquadMenu.FindPlayerById
local Squad = SquadMenu.Squad or {}
SquadMenu.Squad = Squad
Squad.__index = Squad
--- Set the leader of this squad. `ply` can either be
--- a player entity, a ID string that came from `SquadMenu.GetPlayerId`,
--- or `nil` if you want to unset the current squad leader.
function Squad:SetLeader( ply, name )
if type( ply ) == "string" then
self.leaderId = ply -- this is a player ID
self.leaderName = name or "?"
elseif IsValid( ply ) then
self.leaderId = PID( ply )
self.leaderName = ply:Nick()
else
self.leaderId = nil
self.leaderName = nil
SquadMenu.PrintF( "Removed leader from squad #%d", self.id )
return
end
SquadMenu.PrintF( "New leader for squad #%d: %s <%s>", self.id, self.leaderName, self.leaderId )
end
--- Get a table containing a list of active squad members.
--- Returns an array of player entities that are currently on the server.
function Squad:GetActiveMembers()
local members, count = {}, 0
local byId = SquadMenu.AllPlayersById()
for id, _ in pairs( self.membersById ) do
if byId[id] then
count = count + 1
members[count] = byId[id]
end
end
return members, count
end
--- Get a ready-to-be-stringified table containing details from this squad.
function Squad:GetBasicInfo()
local info = {
id = self.id,
name = self.name,
icon = self.icon,
members = {},
enableRings = self.enableRings,
friendlyFire = self.friendlyFire,
isPublic = self.isPublic,
r = self.r,
g = self.g,
b = self.b
}
if self.leaderId then
info.leaderId = self.leaderId
info.leaderName = self.leaderName
end
local count = 0
for id, name in pairs( self.membersById ) do
count = count + 1
info.members[count] = { id, name }
end
return info
end
local ValidateString = SquadMenu.ValidateString
local ValidateNumber = SquadMenu.ValidateNumber
--- Set the details of this squad using a table.
function Squad:SetBasicInfo( info )
if SquadMenu.GetForceFriendlyFire() then
info.friendlyFire = true
end
self.name = ValidateString( info.name, "Unnamed", SquadMenu.MAX_NAME_LENGTH )
self.icon = ValidateString( info.icon, "icon16/flag_blue.png", 256 )
self.enableRings = info.enableRings == true
self.friendlyFire = info.friendlyFire == true
self.isPublic = info.isPublic == true
self.r = ValidateNumber( info.r, 255, 0, 255 )
self.g = ValidateNumber( info.g, 255, 0, 255 )
self.b = ValidateNumber( info.b, 255, 0, 255 )
SquadMenu.blockDamage[self.id] = Either( self.friendlyFire, nil, true )
end
--- Send details and a list of members to all squad members.
--- You should never set `immediate` to `true` unless you know what you're doing.
function Squad:SyncWithMembers( immediate )
if not immediate then
-- avoid spamming the networking system
timer.Remove( "SquadMenu.Sync" .. self.id )
timer.Create( "SquadMenu.Sync" .. self.id, 0.5, 1, function()
self:SyncWithMembers( true )
end )
return
end
local members, count = self:GetActiveMembers()
if count == 0 then return end
local data = self:GetBasicInfo()
SquadMenu.StartCommand( SquadMenu.SETUP_SQUAD )
SquadMenu.WriteTable( data )
net.Send( members )
end
--- Send the requests list to the squad leader.
function Squad:SyncRequests()
local leader = FindByPID( self.leaderId )
if not IsValid( leader ) then return end
SquadMenu.StartCommand( SquadMenu.REQUESTS_LIST )
SquadMenu.WriteTable( self.requestsById )
net.Send( leader )
end
--- Turns `p` into a id and player entity depending on what `p` is.
local function ParsePlayerArg( p )
if type( p ) == "string" then
return p, FindByPID( p )
end
return PID( p ), p
end
--- Add a player as a new member.
--- `p` can be a player id from `SquadMenu.GetPlayerId` or a player entity.
function Squad:AddMember( p )
local id, ply = ParsePlayerArg( p )
if self.membersById[id] then return end
local count = table.Count( self.membersById )
if count >= SquadMenu.GetMemberLimit() then return end
local name = id
if IsValid( ply ) then
local oldSquad = SquadMenu:GetSquad( ply:GetSquadID() )
if oldSquad then
oldSquad:RemoveMember( ply, SquadMenu.LEAVE_REASON_LEFT )
end
ply:SetNWInt( "squad_menu.id", self.id )
name = ply:Nick()
end
self.membersById[id] = name
self:SyncWithMembers()
-- We don't send the requests list to the squad leaders (2nd "true" parameter)
-- because "player_joined_squad" will already tell them to remove
-- this player from their own copies of the join requests list.
SquadMenu:CleanupRequests( id, true )
SquadMenu.StartEvent( "player_joined_squad", {
squadId = self.id,
playerId = id
} )
net.Broadcast()
hook.Run( "SquadMenu_OnJoinedSquad", self.id, ply, id )
end
--- Remove a player from this squad's members list.
--- `ply` can be a player id from `SquadMenu.GetPlayerId` or a player entity.
--- `reasonId` can be `nil` or one of the values from `SquadMenu.LEAVE_REASON_*`.
function Squad:RemoveMember( p, reasonId )
local id, ply = ParsePlayerArg( p )
if not self.membersById[id] then return end
if id == self.leaderId then
pcall( hook.Run, "SquadMenu_OnLeftSquad", self.id, ply, id )
self:Delete()
return
end
self.membersById[id] = nil
self:SyncWithMembers()
if not IsValid( ply ) then return end
ply:SetNWInt( "squad_menu.id", -1 )
if reasonId ~= nil then
SquadMenu.StartCommand( SquadMenu.LEAVE_SQUAD )
net.WriteUInt( reasonId, 3 )
net.Send( ply )
end
hook.Run( "SquadMenu_OnLeftSquad", self.id, ply, id )
end
--- Add a player to the list of players that
--- requested to join and notify the squad leader.
function Squad:RequestToJoin( ply )
if self.isPublic then
self:AddMember( ply )
return
end
local plyId = PID( ply )
if self.requestsById[plyId] then return end
self.requestsById[plyId] = ply:Nick()
self:SyncRequests()
end
--- Accept all the join requests from a list of player IDs.
function Squad:AcceptRequests( ids )
if not table.IsSequential( ids ) then return end
local limit = SquadMenu.GetMemberLimit()
local count = table.Count( self.membersById )
for _, id in ipairs( ids ) do
if count >= limit then
break
elseif self.requestsById[id] then
count = count + 1
self.requestsById[id] = nil
self:AddMember( id )
end
end
end
--- Remove all members from this squad and delete it.
function Squad:Delete()
timer.Remove( "SquadMenu.Sync" .. self.id )
local members, count = self:GetActiveMembers()
if count > 0 then
for _, ply in ipairs( members ) do
ply:SetNWInt( "squad_menu.id", -1 )
end
SquadMenu.StartCommand( SquadMenu.LEAVE_SQUAD )
net.WriteUInt( SquadMenu.LEAVE_REASON_DELETED, 3 )
net.Send( members )
end
self.membersById = nil
self.requestsById = nil
local id = self.id
SquadMenu.squads[id] = nil
SquadMenu.blockDamage[id] = nil
SquadMenu.PrintF( "Deleted squad #%d", id )
SquadMenu.StartEvent( "squad_deleted", { id = id } )
net.Broadcast()
end

View File

@@ -0,0 +1,152 @@
--- Squad Menu library. Contains functions for getting more information about squads.
-- @name squad
-- @class library
-- @libtbl squad_library
SF.RegisterLibrary( "squad" )
return function( instance )
local CheckType = instance.CheckType
local player_methods = instance.Types.Player.Methods
local ply_meta, punwrap = instance.Types.Player, instance.Types.Player.Unwrap
local function GetPlayer( this )
local ent = punwrap( this )
if ent:IsValid() then
return ent
end
SF.Throw( "Player is not valid.", 3 )
end
--- Returns true is this player is part of a squad.
-- @shared
-- @return boolean
function player_methods:isSquadMember()
CheckType( self, ply_meta )
local ply = GetPlayer( self )
return ply:GetSquadID() ~= -1
end
--- Get the ID of the squad this player is part of.
-- @shared
-- @return number Squad ID. -1 if this player is not in one.
function player_methods:getSquadID()
CheckType( self, ply_meta )
local ply = GetPlayer( self )
return ply:GetSquadID()
end
if SERVER then
local FindByPID = SquadMenu.FindPlayerById
local CheckLuaType = SF.CheckLuaType
local WrapColor = instance.Types.Color.Wrap
local WrapPlayer = instance.Types.Player.Wrap
local squad_library = instance.Libraries.squad
--- Returns true if the given ID points to a valid squad.
-- @server
-- @param number id The squad ID
-- @return boolean
function squad_library.exists( id )
CheckLuaType( id, TYPE_NUMBER )
return SquadMenu:GetSquad( id ) ~= nil
end
--- Returns a table with all available squads. Each item contains these keys:
--- - number id
--- - string name
--- - string icon
--- - Player? leader
--- - Color color
--- - boolean isPublic
--- - boolean friendlyFire
-- @server
-- @return table
function squad_library.getAll()
local all, count = {}, 0
for id, squad in pairs( SquadMenu.squads ) do
count = count + 1
local leader = FindByPID( squad.leaderId )
if IsValid( leader ) then
leader = WrapPlayer( leader )
end
all[count] = {
id = id,
name = squad.name,
icon = squad.icon,
leader = leader,
color = WrapColor( Color( squad.r, squad.g, squad.b ) ),
isPublic = squad.isPublic,
friendlyFire = squad.friendlyFire
}
end
return all
end
--- Finds a squad by it's ID and returns the name. Returns nil if the squad does not exist.
-- @server
-- @param number id The squad ID
-- @return string?
function squad_library.getName( id )
CheckLuaType( id, TYPE_NUMBER )
local squad = SquadMenu:GetSquad( id )
return squad and squad.name or nil
end
--- Finds a squad by it's ID and returns the color. Returns nil if the squad does not exist.
-- @server
-- @param number id The squad ID
-- @return Color?
function squad_library.getColor( id )
CheckLuaType( id, TYPE_NUMBER )
local squad = SquadMenu:GetSquad( id )
return squad and WrapColor( Color( squad.r, squad.g, squad.b ) ) or nil
end
--- Returns the number of members in a squad. Returns nil if the squad does not exist.
-- @server
-- @param number id The squad ID
-- @return number?
function squad_library.getMemberCount( id )
CheckLuaType( id, TYPE_NUMBER )
local squad = SquadMenu:GetSquad( id )
if not squad then return end
local _, count = squad:GetActiveMembers()
return count
end
--- Returns an table of players in the squad. Returns nil if the squad does not exist.
-- @server
-- @param number id The squad ID
-- @return table?
function squad_library.getMembers( id )
CheckLuaType( id, TYPE_NUMBER )
local squad = SquadMenu:GetSquad( id )
if not squad then return end
local members, count = squad:GetActiveMembers()
for i = 1, count do
members[i] = WrapPlayer( members[i] )
end
return members
end
end
end