diff --git a/LICENSE b/LICENSE index b579509..7ffc21f 100644 --- a/LICENSE +++ b/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. diff --git a/README.md b/README.md index 57b231a..04ddbac 100644 --- a/README.md +++ b/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) \ No newline at end of file +[![GLuaLint](https://github.com/StyledStrike/gmod-squad-menu/actions/workflows/glualint.yml/badge.svg)](https://github.com/FPtje/GLuaFixer) +[![Workshop Page](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-steam-workshop.jross.me%2F3207278246%2Fsubscriptions-text)](https://steamcommunity.com/sharedfiles/filedetails/?id=3207278246) + +A basic squad/party creation addon for Garry's Mod. Forked because I needed one hook.Run call. + +## Features + +* Create a private or public squad with a custom name, icon and color +* Players can view all available squads and join/request to join them +* Squad leaders can choose if your members can damage eachother (friendly fire) +* Squad members can see the health/armor of all members +* Squad members have indicators in the world so you can tell who is on your squad + +## For developers + +You can check a Player's squad by calling this function: + +```lua +-- Available both on SERVER and CLIENT. +-- Will be -1 if this player is not in a squad. +local id = Player:GetSquadID() +``` + +**On the server only**, you can use this function to get a specific squad instance: + +```lua +local squad = SquadMenu:GetSquad( id ) + +--[[ + This allows you to access things like: + + squad.name - string + squad.icon - string + squad.leader - Player + + squad.enableRings - boolean + squad.friendlyFire - boolean + squad.isPublic - boolean +]] + +-- You can get the player entities that are part of the squad with: +local players = squad:GetActiveMembers() + +-- "p" represents a player Entity or a string you can get from SquadMenu.GetPlayerId: +squad:AddMember( p ) +squad:RemoveMember( p, reason ) -- reason is a number from SquadMenu.LEAVE_REASON_* +``` + +### Hook: `ShouldAllowSquadName` + +You can also filter the squad name before it's assigned by using the `ShouldAllowSquadName` hook **on the server**. + +```lua +hook.Add( "ShouldAllowSquadName", "BlockWordsExample", function( name, leader ) + -- When you only return false, the squad will be "Unamed". + if string.find( name, "amogus" ) then + return false + end + + -- Optionally you can also return a second value for the name + if string.find( name, "sus" ) then + return false, string.Replace( name, "sus", "nope" ) + end +end ) +``` + +### Hook: `SquadPlayerSay` + +You can also override/filter squad-only messages by using the `SquadPlayerSay` hook **on the server**. + +```lua +hook.Add( "SquadPlayerSay", "BlockMessagesExample", function( sender, text ) + -- When you return false, the message will not be sent. + if string.find( text, "amogus" ) then + return false + end + + -- You can return a string to override the message. + if string.find( text, "sus" ) then + return string.Replace( text, "sus", "nope" ) + end +end ) +``` + +### Hooks: `SquadMenu_OnJoinedSquad` and `SquadMenu_OnLeftSquad` + +You can use these hooks to detect when a player has joined/left a squad **on the server**. + +```lua +hook.Add( "SquadMenu_OnJoinedSquad", "JoinedSquadExample", function( squadId, ply, plySteamID ) + -- Get the squad instance's name + local squadName = SquadMenu:GetSquad( squadId ).name + + -- Get the player name, if the player is not valid then just use their SteamID + local playerName = IsValid( ply ) and ply:Nick() or plySteamID + + -- Print a message on everyone's chat + for _, p in ipairs( player.GetAll() ) do + p:ChatPrint( playerName .. " joined the squad: " .. squadName ) + end +end ) + +hook.Add( "SquadMenu_OnLeftSquad", "LeftSquadExample", function( squadId, ply, plySteamID ) + -- Get the squad instance's name + local squadName = SquadMenu:GetSquad( squadId ).name + + -- Get the player name, if the player is not valid then just use their SteamID + local playerName = IsValid( ply ) and ply:Nick() or plySteamID + + -- Print a message on everyone's chat + for _, p in ipairs( player.GetAll() ) do + p:ChatPrint( playerName .. " left the squad: " .. squadName ) + end +end ) +``` + +# Contributing + +Before you open a pull request, if it deals with Lua code, please read [this](https://github.com/StyledStrike/gmod-squad-menu/blob/main/.github/pull_request_template.md). diff --git a/addon.json b/addon.json new file mode 100644 index 0000000..2d7bc5b --- /dev/null +++ b/addon.json @@ -0,0 +1,13 @@ +{ + "title": "Simple Squad Menu", + "type": "ServerContent", + "tags": ["realism"], + "ignore": [ + ".git*", + "*.txt", + "*.md", + "glualint.json", + ".editorconfig", + "LICENSE" + ] +} \ No newline at end of file diff --git a/lua/autorun/sh_squad_menu.lua b/lua/autorun/sh_squad_menu.lua new file mode 100644 index 0000000..411fda2 --- /dev/null +++ b/lua/autorun/sh_squad_menu.lua @@ -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 diff --git a/lua/entities/gmod_wire_expression2/core/custom/cl_squad_menu.lua b/lua/entities/gmod_wire_expression2/core/custom/cl_squad_menu.lua new file mode 100644 index 0000000..036388f --- /dev/null +++ b/lua/entities/gmod_wire_expression2/core/custom/cl_squad_menu.lua @@ -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." diff --git a/lua/entities/gmod_wire_expression2/core/custom/squad_menu.lua b/lua/entities/gmod_wire_expression2/core/custom/squad_menu.lua new file mode 100644 index 0000000..2bd694f --- /dev/null +++ b/lua/entities/gmod_wire_expression2/core/custom/squad_menu.lua @@ -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 diff --git a/lua/includes/modules/styled_theme.lua b/lua/includes/modules/styled_theme.lua new file mode 100644 index 0000000..bd97200 --- /dev/null +++ b/lua/includes/modules/styled_theme.lua @@ -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 diff --git a/lua/includes/modules/styled_theme_tabbed_frame.lua b/lua/includes/modules/styled_theme_tabbed_frame.lua new file mode 100644 index 0000000..8be3806 --- /dev/null +++ b/lua/includes/modules/styled_theme_tabbed_frame.lua @@ -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" ) diff --git a/lua/squad_menu/client/config.lua b/lua/squad_menu/client/config.lua new file mode 100644 index 0000000..73d98f9 --- /dev/null +++ b/lua/squad_menu/client/config.lua @@ -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() diff --git a/lua/squad_menu/client/hud.lua b/lua/squad_menu/client/hud.lua new file mode 100644 index 0000000..3a499b2 --- /dev/null +++ b/lua/squad_menu/client/hud.lua @@ -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 diff --git a/lua/squad_menu/client/main.lua b/lua/squad_menu/client/main.lua new file mode 100644 index 0000000..f8ca2df --- /dev/null +++ b/lua/squad_menu/client/main.lua @@ -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 diff --git a/lua/squad_menu/client/menu.lua b/lua/squad_menu/client/menu.lua new file mode 100644 index 0000000..80ffd8c --- /dev/null +++ b/lua/squad_menu/client/menu.lua @@ -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 \ No newline at end of file diff --git a/lua/squad_menu/client/vgui/member_status.lua b/lua/squad_menu/client/vgui/member_status.lua new file mode 100644 index 0000000..911a074 --- /dev/null +++ b/lua/squad_menu/client/vgui/member_status.lua @@ -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" ) diff --git a/lua/squad_menu/client/vgui/squad_list_row.lua b/lua/squad_menu/client/vgui/squad_list_row.lua new file mode 100644 index 0000000..1e77415 --- /dev/null +++ b/lua/squad_menu/client/vgui/squad_list_row.lua @@ -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 "", "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" ) diff --git a/lua/squad_menu/player.lua b/lua/squad_menu/player.lua new file mode 100644 index 0000000..c8d06a3 --- /dev/null +++ b/lua/squad_menu/player.lua @@ -0,0 +1,5 @@ +local PlayerMeta = FindMetaTable( "Player" ) + +function PlayerMeta:GetSquadID() + return self:GetNWInt( "squad_menu.id", -1 ) +end diff --git a/lua/squad_menu/server/main.lua b/lua/squad_menu/server/main.lua new file mode 100644 index 0000000..a18005c --- /dev/null +++ b/lua/squad_menu/server/main.lua @@ -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 ) diff --git a/lua/squad_menu/server/network.lua b/lua/squad_menu/server/network.lua new file mode 100644 index 0000000..d2a368b --- /dev/null +++ b/lua/squad_menu/server/network.lua @@ -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 ) diff --git a/lua/squad_menu/server/squad.lua b/lua/squad_menu/server/squad.lua new file mode 100644 index 0000000..963a98c --- /dev/null +++ b/lua/squad_menu/server/squad.lua @@ -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 diff --git a/lua/starfall/libs_sh/squad_menu.lua b/lua/starfall/libs_sh/squad_menu.lua new file mode 100644 index 0000000..471873f --- /dev/null +++ b/lua/starfall/libs_sh/squad_menu.lua @@ -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 diff --git a/materials/squad_menu/ping.png b/materials/squad_menu/ping.png new file mode 100644 index 0000000..7eb74bb Binary files /dev/null and b/materials/squad_menu/ping.png differ diff --git a/materials/squad_menu/ring.png b/materials/squad_menu/ring.png new file mode 100644 index 0000000..483bf78 Binary files /dev/null and b/materials/squad_menu/ring.png differ diff --git a/materials/squad_menu/squad_menu.png b/materials/squad_menu/squad_menu.png new file mode 100644 index 0000000..954060c Binary files /dev/null and b/materials/squad_menu/squad_menu.png differ diff --git a/materials/styledstrike/icons/bullet_list.png b/materials/styledstrike/icons/bullet_list.png new file mode 100644 index 0000000..1c5f47a Binary files /dev/null and b/materials/styledstrike/icons/bullet_list.png differ diff --git a/materials/styledstrike/icons/cog.png b/materials/styledstrike/icons/cog.png new file mode 100644 index 0000000..4fe7d40 Binary files /dev/null and b/materials/styledstrike/icons/cog.png differ diff --git a/materials/styledstrike/icons/flag_two_tone.png b/materials/styledstrike/icons/flag_two_tone.png new file mode 100644 index 0000000..4469191 Binary files /dev/null and b/materials/styledstrike/icons/flag_two_tone.png differ diff --git a/materials/styledstrike/icons/user_add.png b/materials/styledstrike/icons/user_add.png new file mode 100644 index 0000000..8fa07ee Binary files /dev/null and b/materials/styledstrike/icons/user_add.png differ diff --git a/materials/styledstrike/icons/users.png b/materials/styledstrike/icons/users.png new file mode 100644 index 0000000..5c8a031 Binary files /dev/null and b/materials/styledstrike/icons/users.png differ diff --git a/resource/localization/en/squad_menu.properties b/resource/localization/en/squad_menu.properties new file mode 100644 index 0000000..f0ab568 --- /dev/null +++ b/resource/localization/en/squad_menu.properties @@ -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 \ No newline at end of file diff --git a/resource/localization/pt-br/squad_menu.properties b/resource/localization/pt-br/squad_menu.properties new file mode 100644 index 0000000..2104a77 --- /dev/null +++ b/resource/localization/pt-br/squad_menu.properties @@ -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 \ No newline at end of file diff --git a/resource/localization/tr/squad_menu.properties b/resource/localization/tr/squad_menu.properties new file mode 100644 index 0000000..1816797 --- /dev/null +++ b/resource/localization/tr/squad_menu.properties @@ -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