init commit
29
LICENSE
@@ -1,18 +1,21 @@
|
||||
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
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
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
|
||||
portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 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.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
@@ -1,4 +1,121 @@
|
||||
# Simple-Squad-Menu
|
||||
# Simple Squad menu
|
||||
|
||||
Forked version of Simple Squad Menu
|
||||
(https://steamcommunity.com/sharedfiles/filedetails/?id=3207278246)
|
||||
[](https://github.com/FPtje/GLuaFixer)
|
||||
[](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
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Simple Squad Menu",
|
||||
"type": "ServerContent",
|
||||
"tags": ["realism"],
|
||||
"ignore": [
|
||||
".git*",
|
||||
"*.txt",
|
||||
"*.md",
|
||||
"glualint.json",
|
||||
".editorconfig",
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
||||
230
lua/autorun/sh_squad_menu.lua
Normal 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
|
||||
@@ -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."
|
||||
@@ -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
|
||||
681
lua/includes/modules/styled_theme.lua
Normal 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
|
||||
160
lua/includes/modules/styled_theme_tabbed_frame.lua
Normal 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" )
|
||||
63
lua/squad_menu/client/config.lua
Normal 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()
|
||||
344
lua/squad_menu/client/hud.lua
Normal 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
|
||||
380
lua/squad_menu/client/main.lua
Normal 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
|
||||
598
lua/squad_menu/client/menu.lua
Normal 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
|
||||
100
lua/squad_menu/client/vgui/member_status.lua
Normal 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" )
|
||||
199
lua/squad_menu/client/vgui/squad_list_row.lua
Normal 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" )
|
||||
5
lua/squad_menu/player.lua
Normal file
@@ -0,0 +1,5 @@
|
||||
local PlayerMeta = FindMetaTable( "Player" )
|
||||
|
||||
function PlayerMeta:GetSquadID()
|
||||
return self:GetNWInt( "squad_menu.id", -1 )
|
||||
end
|
||||
196
lua/squad_menu/server/main.lua
Normal 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 )
|
||||
182
lua/squad_menu/server/network.lua
Normal 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 )
|
||||
278
lua/squad_menu/server/squad.lua
Normal 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
|
||||
152
lua/starfall/libs_sh/squad_menu.lua
Normal 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
|
||||
BIN
materials/squad_menu/ping.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
materials/squad_menu/ring.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
materials/squad_menu/squad_menu.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
materials/styledstrike/icons/bullet_list.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
materials/styledstrike/icons/cog.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
materials/styledstrike/icons/flag_two_tone.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
materials/styledstrike/icons/user_add.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
materials/styledstrike/icons/users.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
57
resource/localization/en/squad_menu.properties
Normal 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
|
||||
57
resource/localization/pt-br/squad_menu.properties
Normal 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
|
||||
57
resource/localization/tr/squad_menu.properties
Normal 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
|
||||