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

29
LICENSE
View File

@@ -1,18 +1,21 @@
MIT License MIT License
Copyright (c) 2025 raizen Copyright (c) 2024 StyledStrike
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Permission is hereby granted, free of charge, to any person obtaining a copy
associated documentation files (the "Software"), to deal in the Software without restriction, including of this software and associated documentation files (the "Software"), to deal
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell in the Software without restriction, including without limitation the rights
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
following conditions: copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial The above copyright notice and this permission notice shall be included in all
portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
USE OR OTHER DEALINGS IN THE SOFTWARE. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

123
README.md
View File

@@ -1,4 +1,121 @@
# Simple-Squad-Menu # Simple Squad menu
Forked version of Simple Squad Menu [![GLuaLint](https://github.com/StyledStrike/gmod-squad-menu/actions/workflows/glualint.yml/badge.svg)](https://github.com/FPtje/GLuaFixer)
(https://steamcommunity.com/sharedfiles/filedetails/?id=3207278246) [![Workshop Page](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-steam-workshop.jross.me%2F3207278246%2Fsubscriptions-text)](https://steamcommunity.com/sharedfiles/filedetails/?id=3207278246)
A basic squad/party creation addon for Garry's Mod. Forked because I needed one hook.Run call.
## Features
* Create a private or public squad with a custom name, icon and color
* Players can view all available squads and join/request to join them
* Squad leaders can choose if your members can damage eachother (friendly fire)
* Squad members can see the health/armor of all members
* Squad members have indicators in the world so you can tell who is on your squad
## For developers
You can check a Player's squad by calling this function:
```lua
-- Available both on SERVER and CLIENT.
-- Will be -1 if this player is not in a squad.
local id = Player:GetSquadID()
```
**On the server only**, you can use this function to get a specific squad instance:
```lua
local squad = SquadMenu:GetSquad( id )
--[[
This allows you to access things like:
squad.name - string
squad.icon - string
squad.leader - Player
squad.enableRings - boolean
squad.friendlyFire - boolean
squad.isPublic - boolean
]]
-- You can get the player entities that are part of the squad with:
local players = squad:GetActiveMembers()
-- "p" represents a player Entity or a string you can get from SquadMenu.GetPlayerId:
squad:AddMember( p )
squad:RemoveMember( p, reason ) -- reason is a number from SquadMenu.LEAVE_REASON_*
```
### Hook: `ShouldAllowSquadName`
You can also filter the squad name before it's assigned by using the `ShouldAllowSquadName` hook **on the server**.
```lua
hook.Add( "ShouldAllowSquadName", "BlockWordsExample", function( name, leader )
-- When you only return false, the squad will be "Unamed".
if string.find( name, "amogus" ) then
return false
end
-- Optionally you can also return a second value for the name
if string.find( name, "sus" ) then
return false, string.Replace( name, "sus", "nope" )
end
end )
```
### Hook: `SquadPlayerSay`
You can also override/filter squad-only messages by using the `SquadPlayerSay` hook **on the server**.
```lua
hook.Add( "SquadPlayerSay", "BlockMessagesExample", function( sender, text )
-- When you return false, the message will not be sent.
if string.find( text, "amogus" ) then
return false
end
-- You can return a string to override the message.
if string.find( text, "sus" ) then
return string.Replace( text, "sus", "nope" )
end
end )
```
### Hooks: `SquadMenu_OnJoinedSquad` and `SquadMenu_OnLeftSquad`
You can use these hooks to detect when a player has joined/left a squad **on the server**.
```lua
hook.Add( "SquadMenu_OnJoinedSquad", "JoinedSquadExample", function( squadId, ply, plySteamID )
-- Get the squad instance's name
local squadName = SquadMenu:GetSquad( squadId ).name
-- Get the player name, if the player is not valid then just use their SteamID
local playerName = IsValid( ply ) and ply:Nick() or plySteamID
-- Print a message on everyone's chat
for _, p in ipairs( player.GetAll() ) do
p:ChatPrint( playerName .. " joined the squad: " .. squadName )
end
end )
hook.Add( "SquadMenu_OnLeftSquad", "LeftSquadExample", function( squadId, ply, plySteamID )
-- Get the squad instance's name
local squadName = SquadMenu:GetSquad( squadId ).name
-- Get the player name, if the player is not valid then just use their SteamID
local playerName = IsValid( ply ) and ply:Nick() or plySteamID
-- Print a message on everyone's chat
for _, p in ipairs( player.GetAll() ) do
p:ChatPrint( playerName .. " left the squad: " .. squadName )
end
end )
```
# Contributing
Before you open a pull request, if it deals with Lua code, please read [this](https://github.com/StyledStrike/gmod-squad-menu/blob/main/.github/pull_request_template.md).

13
addon.json Normal file
View File

@@ -0,0 +1,13 @@
{
"title": "Simple Squad Menu",
"type": "ServerContent",
"tags": ["realism"],
"ignore": [
".git*",
"*.txt",
"*.md",
"glualint.json",
".editorconfig",
"LICENSE"
]
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,57 @@
squad_menu.title=Squad Menu
squad_menu.yes=Yes
squad_menu.no=No
squad_menu.ok=OK
squad_menu.tab.squad_list=Squad list
squad_menu.tab.squad_properties=Squad properties
squad_menu.tab.squad_members=Squad members
squad_menu.tab.join_requests=Join requests
squad_menu.tab.settings=Settings
squad_menu.create_squad=Create Squad
squad_menu.edit_squad=Edit Squad
squad_menu.leave_squad=Leave Squad
squad_menu.leave_leader=You are the squad leader. Leaving will disband it. Are you sure?
squad_menu.leave_first=You are on a squad already. Leave it if you want to join others.
squad_menu.leave_first_create=You are on a squad already. Leave it if you want to create one.
squad_menu.join=Join
squad_menu.request_to_join=Request to join
squad_menu.waiting_response=Request sent...
squad_menu.fetching_data=Fetching data...
squad_menu.no_available_squads=No squads are available right now.
squad_menu.not_in_a_squad=You are not in a squad.
squad_menu.not_squad_leader=You are not the squad leader.
squad_menu.slots=Slots
squad_menu.full_squad=This squad is full
squad_menu.member_limit_reached=This squad has reached the member limit.
squad_menu.no_requests_yet=No players have requested to join your squad yet.
squad_menu.no_requests_needed=This is a public squad, nobody needs to request to enter.
squad_menu.no_members=No players have joined your squad yet.
squad_menu.requests_list=These players are waiting for your response.
squad_menu.cannot_accept_more=Squad members limit reached, you cannot add more players!
squad_menu.accept=Accept
squad_menu.kick=Kick
squad_menu.squad_name=Squad name
squad_menu.choose_icon=Choose an icon...
squad_menu.squad_color=Squad color
squad_menu.squad_is_public=Allow anyone to join
squad_menu.squad_friendly_fire=Allow friendly fire
squad_menu.squad_force_friendly_fire=The server enforced friendly fire
squad_menu.squad_rings=Show ring around members
squad_menu.member_joined=%s joined the squad!
squad_menu.member_left=%s left the squad!
squad_menu.squad_welcome=Welcome to the squad:
squad_menu.squad_created=%s created a squad:
squad_menu.chat_tip=Type any of these commands to only chat with your squad:
squad_menu.request_message=%s requested to join your squad.
squad_menu.left_squad=You left the squad.
squad_menu.deleted_squad=Your squad has been disbanded.
squad_menu.kicked_from_squad=You were kicked from the squad.
squad_menu.default_squad_name=%s's Squad
squad_menu.settings.show_members=Show members list on screen
squad_menu.settings.show_halos=Show halos around members
squad_menu.settings.show_rings=Show rings around members
squad_menu.settings.enable_sounds=Play UI sounds
squad_menu.settings.name_draw_distance=Name draw distance
squad_menu.settings.halo_draw_distance=Halo draw distance
squad_menu.settings.ping_key=Ping key

View File

@@ -0,0 +1,57 @@
squad_menu.title=Menu do Esquadrão
squad_menu.yes=Sim
squad_menu.no=Não
squad_menu.ok=OK
squad_menu.tab.squad_list=Lista de esquadrões
squad_menu.tab.squad_properties=Propriedades do esquadrão
squad_menu.tab.squad_members=Membros do esquadrão
squad_menu.tab.join_requests=Pedidos para entrar
squad_menu.tab.settings=Configurações
squad_menu.create_squad=Criar Esquadrão
squad_menu.edit_squad=Editar Esquadrão
squad_menu.leave_squad=Sair
squad_menu.leave_leader=Você é o líder do esquadrão. Sair irá dissolvê-lo. Tem certeza?
squad_menu.leave_first=Você já está em um esquadrão. Deixe-o se quiser entrar em outros.
squad_menu.leave_first_create=Você já está em um esquadrão. Deixe-o se quiser criar um novo.
squad_menu.join=Entrar
squad_menu.request_to_join=Pedir para entrar
squad_menu.waiting_response=Pedido enviado...
squad_menu.fetching_data=Buscando dados...
squad_menu.no_available_squads=Nenhum esquadrão está disponível no momento.
squad_menu.not_in_a_squad=Voçê não está em um esquadrão.
squad_menu.not_squad_leader=Voçê não é o lider do esquadrão.
squad_menu.slots=Vagas
squad_menu.full_squad=Esquadrão cheio
squad_menu.member_limit_reached=Este esquadrão atingiu o limite de membros.
squad_menu.no_requests_yet=Nenhum jogador pediu para entrar no seu esquadrão ainda.
squad_menu.no_requests_needed=Este é um esquadrão público, ninguém precisa pedir para entrar.
squad_menu.no_members=Nenhum jogador se juntou ao seu esquadrão ainda.
squad_menu.requests_list=Esses jogadores estão aguardando sua resposta.
squad_menu.cannot_accept_more=Limite de membros atingido, você não pode aceitar mais jogadores!
squad_menu.accept=Aceitar
squad_menu.kick=Expulsar
squad_menu.squad_name=Nome do esquadrão
squad_menu.choose_icon=Escolher ícone...
squad_menu.squad_color=Cor do esquadrão
squad_menu.squad_is_public=Permitir que qualquer um entre
squad_menu.squad_friendly_fire=Permitir fogo amigo
squad_menu.squad_force_friendly_fire=O servidor impôs fogo amigo
squad_menu.squad_rings=Mostre anel ao redor dos membros
squad_menu.member_joined=%s entrou no esquadrão!
squad_menu.member_left=%s saiu do esquadrão!
squad_menu.squad_welcome=Bem-vindo ao esquadrão:
squad_menu.squad_created=%s criou o esquadrão:
squad_menu.chat_tip=Use um destes comandos para conversar apenas com seu esquadrão:
squad_menu.request_message=%s pediu para entrar no seu esquadrão.
squad_menu.left_squad=Você saiu do esquadrão.
squad_menu.deleted_squad=Seu esquadrão foi dissolvido.
squad_menu.kicked_from_squad=Você foi expulso do esquadrão.
squad_menu.default_squad_name=Esquadrão do %s
squad_menu.settings.show_members=Mostrar membros na tela
squad_menu.settings.show_halos=Mostrar aréolas nos membros
squad_menu.settings.show_rings=Mostrar anéis nos membros
squad_menu.settings.enable_sounds=Tocar sons da interface
squad_menu.settings.name_draw_distance=Distância de renderização dos nomes
squad_menu.settings.halo_draw_distance=Distância de renderização das aréolas
squad_menu.settings.ping_key=Tecla de ping

View File

@@ -0,0 +1,57 @@
squad_menu.title=Takım Menüsü
squad_menu.yes=Evet
squad_menu.no=Hayır
squad_menu.ok=TAMAM
squad_menu.tab.squad_list=Takım Listesi
squad_menu.tab.squad_properties=Takım özellikleri
squad_menu.tab.squad_members=Takım üyeleri
squad_menu.tab.join_requests=Katılma istekleri
squad_menu.tab.settings=Ayarlar
squad_menu.create_squad=Takım Oluştur
squad_menu.edit_squad=Takım Düzenle
squad_menu.leave_squad=Takımdan Ayrıl
squad_menu.leave_leader=Takım lideri sensin. Takımdan ayrılırsan, takım dağılır. Emin misin?
squad_menu.leave_first=Zaten bir takımdasın. Başkalarına katılmak istiyorsanız ayrılın.
squad_menu.leave_first_create=Zaten bir takımdasın. Takım oluşturmak istiyorsanız ayrılın.
squad_menu.join=Katıl
squad_menu.request_to_join=Katılma isteği gönder
squad_menu.waiting_response=İstek gönderildi...
squad_menu.fetching_data=Veriler alınıyor...
squad_menu.no_available_squads=Şu anda uygun takım yok.
squad_menu.not_in_a_squad=Bir takımda değilsin.
squad_menu.not_squad_leader=Takım lideri değilsin.
squad_menu.slots=Yuvalar
squad_menu.full_squad=Takım dolu
squad_menu.member_limit_reached=Bu takım üye sınırına ulaştı.
squad_menu.no_requests_yet=Henüz hiçbir oyuncu takımınıza katılmak için istekte bulunmadı.
squad_menu.no_requests_needed=Bu herkese açık bir takım, kimsenin girme isteğinde bulunmasına gerek yok.
squad_menu.no_members=Takımınıza henüz hiçbir oyuncu katılmadı.
squad_menu.requests_list=Bu oyuncular yanıtınızı bekliyor.
squad_menu.cannot_accept_more=Takım üye sınırına ulaşıldı, daha fazla oyuncu ekleyemezsin!
squad_menu.accept=Kabul Et
squad_menu.kick=At
squad_menu.squad_name=Takım adı
squad_menu.choose_icon=Simge Seç...
squad_menu.squad_color=Takım rengi
squad_menu.squad_is_public=Herkesin katılmasına izin ver
squad_menu.squad_friendly_fire=Dost ateşine izin ver
squad_menu.squad_force_friendly_fire=Sunucuda dost ateşi uygulandı
squad_menu.squad_rings=Üyelerin etrafında halka göster
squad_menu.member_joined=%s takıma katıldı!
squad_menu.member_left=%s takımdan ayrıldı!
squad_menu.squad_welcome=Şu takıma hoş geldin:
squad_menu.squad_created=%s bir takım oluşturdu:
squad_menu.chat_tip=Yalnızca takımınızla sohbet etmek için şu komutlardan birini yazın:
squad_menu.request_message=%s takımına katılma isteği gönderdi.
squad_menu.left_squad=Takımdan ayrıldın.
squad_menu.deleted_squad=Takımın dağıtıldı.
squad_menu.kicked_from_squad=Takımdan atıldın.
squad_menu.default_squad_name=%s Oyuncusunun Takımı
squad_menu.settings.show_members=Üye listesini ekranda göster
squad_menu.settings.show_halos=Üyelerin etrafında parıltılı halka göster
squad_menu.settings.show_rings=Üyelerin etrafındaki halkaları göster
squad_menu.settings.enable_sounds=Kullanıcı arayüzü seslerini çal
squad_menu.settings.name_draw_distance=İsim gösterme mesafesi
squad_menu.settings.halo_draw_distance=Halka gösterme mesafesi
squad_menu.settings.ping_key=İşaret tuşu