Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
dc2835b11b
|
|||
|
cf747dd454
|
|||
|
2b56b15886
|
|||
|
6f579fb2db
|
|||
|
490d5a2194
|
|||
|
5f35c70286
|
|||
|
5fafde5e31
|
|||
|
e357313204
|
|||
|
05864e1ed4
|
|||
|
e40893a290
|
|||
|
b0ab2e3e31
|
|||
|
49a6a085af
|
|||
|
00eb696e7f
|
|||
|
2c5a73cf49
|
|||
|
8695419876
|
|||
|
fd5a9703bc
|
|||
|
7248b8c434
|
|||
|
904cf3566c
|
|||
|
b220f41c4a
|
|||
|
b183517e9f
|
|||
|
ba9e71e6ae
|
|||
|
f6ec6b1236
|
|||
| 75428ef21d | |||
| 875954ac63 |
@@ -1,55 +1,340 @@
|
||||
xyghtBladWinRPMenu = xyghtBladWinRPMenu or {}
|
||||
-- ============================================================
|
||||
-- AUTOLOADER nocode — Dual-path récursif
|
||||
-- Fichier : lua/autorun/bladWinRpMenu_loader.lua
|
||||
-- ============================================================
|
||||
|
||||
AddCSLuaFile("bladw_deathscreen/config/sh_config.lua")
|
||||
local NoCode = {}
|
||||
|
||||
if SERVER then
|
||||
AddCSLuaFile("bladw_hud/client/cl_hud.lua")
|
||||
AddCSLuaFile("bladw_cMenu/client/cl_cMenu.lua")
|
||||
AddCSLuaFile("bladw_deathscreen/client/cl_interface_ds.lua")
|
||||
NoCode.config = {
|
||||
-- Dossier(s)-module(s) à charger.
|
||||
-- • sans wildcard -> nom exact d'UN dossier : "bladw_announcement"
|
||||
-- • avec wildcard -> glob, charge TOUS ceux qui matchent :
|
||||
-- "bladw_*" (préfixe) / "*_menu" (suffixe) / "bladw_*_v2" (milieu)
|
||||
-- • "none" (ou vide/nil) -> loader désactivé, rien n'est chargé
|
||||
root = "none",
|
||||
-- Dossiers de bibliothèques : seulement envoyés au client (AddCSLuaFile),
|
||||
-- jamais auto-inclus. Ils sont chargés à la demande via include("lib/...").
|
||||
libs = { "lib" },
|
||||
safe_load = true,
|
||||
verbose = true,
|
||||
exclude = { "template.lua", "disabled", "_dev" },
|
||||
}
|
||||
|
||||
include("bladw_deathscreen/server/sv_hooks.lua")
|
||||
-- ============================================================
|
||||
-- Logs
|
||||
-- ============================================================
|
||||
|
||||
resource.AddSingleFile("resource/fonts/Poppins-SemiBold.ttf")
|
||||
resource.AddSingleFile("resource/fonts/Poppins-Medium.ttf")
|
||||
resource.AddSingleFile("resource/fonts/Poppins-Bold.ttf")
|
||||
resource.AddSingleFile("resource/fonts/Poppins-Regular.ttf")
|
||||
local C = {
|
||||
ok = Color(100, 220, 100),
|
||||
err = Color(255, 80, 80),
|
||||
info = Color(120, 180, 255),
|
||||
warn = Color(255, 200, 50),
|
||||
dim = Color(160, 160, 160),
|
||||
title = Color(200, 140, 255),
|
||||
}
|
||||
|
||||
local function Log(col, msg)
|
||||
MsgC(col, msg .. "\n")
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Utilitaires
|
||||
-- ============================================================
|
||||
|
||||
local function StartsWith(str, prefix)
|
||||
return str:sub(1, #prefix) == prefix
|
||||
end
|
||||
|
||||
local function IsExcluded(name)
|
||||
for _, ex in ipairs(NoCode.config.exclude) do
|
||||
if name == ex then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function SortFiles(files)
|
||||
table.sort(files, function(a, b)
|
||||
local function rank(n)
|
||||
if StartsWith(n, "sh_") then return 1 end
|
||||
if StartsWith(n, "sv_") then return 2 end
|
||||
if StartsWith(n, "cl_") then return 3 end
|
||||
return 4
|
||||
end
|
||||
return rank(a) < rank(b)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Fusionne deux tables en évitant les doublons
|
||||
local function Merge(t1, t2)
|
||||
local seen = {}
|
||||
local result = {}
|
||||
for _, v in ipairs(t1 or {}) do
|
||||
if not seen[v] then seen[v] = true; table.insert(result, v) end
|
||||
end
|
||||
for _, v in ipairs(t2 or {}) do
|
||||
if not seen[v] then seen[v] = true; table.insert(result, v) end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Compteurs
|
||||
-- ============================================================
|
||||
|
||||
local stats = { loaded = 0, sent = 0, errors = 0 }
|
||||
|
||||
-- ============================================================
|
||||
-- Chargement d'un fichier unique
|
||||
-- ============================================================
|
||||
|
||||
local function LoadFile(path)
|
||||
local filename = path:match("([^/]+)$")
|
||||
local realm, shouldLoad, shouldSend
|
||||
|
||||
if StartsWith(filename, "sh_") then
|
||||
realm = "SHARED"
|
||||
shouldSend = SERVER
|
||||
shouldLoad = true
|
||||
|
||||
elseif StartsWith(filename, "sv_") then
|
||||
realm = "SERVER"
|
||||
shouldLoad = SERVER
|
||||
|
||||
elseif StartsWith(filename, "cl_") then
|
||||
realm = "CLIENT"
|
||||
shouldSend = SERVER
|
||||
shouldLoad = not SERVER
|
||||
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
if shouldSend then
|
||||
AddCSLuaFile(path)
|
||||
stats.sent = stats.sent + 1
|
||||
if NoCode.config.verbose and not shouldLoad then
|
||||
Log(C.info, (" [→] [%s] %s"):format(realm, path))
|
||||
end
|
||||
end
|
||||
|
||||
if not shouldLoad then return end
|
||||
|
||||
if NoCode.config.safe_load then
|
||||
local ok, err = pcall(include, path)
|
||||
if ok then
|
||||
stats.loaded = stats.loaded + 1
|
||||
if NoCode.config.verbose then
|
||||
Log(C.ok, (" [✓] [%s] %s"):format(realm, path))
|
||||
end
|
||||
else
|
||||
stats.errors = stats.errors + 1
|
||||
Log(C.err, (" [✗] [%s] %s"):format(realm, path))
|
||||
Log(C.err, (" ↳ %s"):format(tostring(err)))
|
||||
end
|
||||
else
|
||||
include(path)
|
||||
stats.loaded = stats.loaded + 1
|
||||
if NoCode.config.verbose then
|
||||
Log(C.ok, (" [✓] [%s] %s"):format(realm, path))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Scan récursif — cherche TOUJOURS dans LUA et GAME en même temps
|
||||
-- ============================================================
|
||||
|
||||
local function LoadFolder(dir, depth)
|
||||
depth = depth or 0
|
||||
|
||||
local basename = dir:match("([^/]+)$") or dir
|
||||
if IsExcluded(basename) then
|
||||
Log(C.warn, (" [~] Dossier ignoré : %s"):format(dir))
|
||||
return
|
||||
end
|
||||
|
||||
-- On cherche dans les deux paths et on fusionne les résultats
|
||||
local files1, dirs1 = file.Find(dir .. "/*", "LUA")
|
||||
local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")
|
||||
|
||||
local files = Merge(files1, files2)
|
||||
local dirs = Merge(dirs1, dirs2)
|
||||
|
||||
if #files == 0 and #dirs == 0 then
|
||||
if NoCode.config.verbose and depth > 0 then
|
||||
Log(C.warn, (" [!] Dossier vide ou introuvable : %s"):format(dir))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if NoCode.config.verbose and depth > 0 then
|
||||
Log(C.dim, string.rep(" ", depth) .. "📂 " .. basename .. "/")
|
||||
end
|
||||
|
||||
SortFiles(files)
|
||||
|
||||
for _, fname in ipairs(files) do
|
||||
if fname:EndsWith(".lua") and not IsExcluded(fname) then
|
||||
LoadFile(dir .. "/" .. fname)
|
||||
end
|
||||
end
|
||||
|
||||
-- Récursion sur tous les sous-dossiers sans limite de profondeur
|
||||
for _, subdir in ipairs(dirs) do
|
||||
if not IsExcluded(subdir) then
|
||||
LoadFolder(dir .. "/" .. subdir, depth + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Envoi récursif d'une bibliothèque au client (sans l'inclure)
|
||||
-- Les fichiers de lib/ ne suivent pas le préfixe sh_/sv_/cl_ ;
|
||||
-- on les expédie simplement pour que include("lib/...") fonctionne.
|
||||
-- ============================================================
|
||||
|
||||
local function SendFolder(dir)
|
||||
if not SERVER then return end
|
||||
|
||||
local files1, dirs1 = file.Find(dir .. "/*", "LUA")
|
||||
local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")
|
||||
local files = Merge(files1, files2)
|
||||
local dirs = Merge(dirs1, dirs2)
|
||||
|
||||
for _, fname in ipairs(files) do
|
||||
if fname:EndsWith(".lua") and not IsExcluded(fname) then
|
||||
local path = dir .. "/" .. fname
|
||||
AddCSLuaFile(path)
|
||||
stats.sent = stats.sent + 1
|
||||
if NoCode.config.verbose then
|
||||
Log(C.info, (" [→] [LIB] %s"):format(path))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, subdir in ipairs(dirs) do
|
||||
if not IsExcluded(subdir) then
|
||||
SendFolder(dir .. "/" .. subdir)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Découverte des dossiers-modules
|
||||
-- (file.Find ne supporte le wildcard que sur le dernier segment,
|
||||
-- donc on liste la racine lua/ puis on filtre nous-mêmes.)
|
||||
-- • pattern sans "*" -> match exact (un seul dossier)
|
||||
-- • pattern avec "*" -> glob converti en motif Lua (préfixe/suffixe/milieu)
|
||||
-- ============================================================
|
||||
|
||||
--- Échappe les caractères magiques d'un motif Lua.
|
||||
local function EscapePattern(s)
|
||||
return (s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"))
|
||||
end
|
||||
|
||||
--- Transforme un glob simple ("bladw_*") en motif Lua ancré ("^bladw_.*$").
|
||||
local function GlobToPattern(glob)
|
||||
return "^" .. EscapePattern(glob):gsub("%%%*", ".*") .. "$"
|
||||
end
|
||||
|
||||
local function FindRootFolders(pattern)
|
||||
local roots = {}
|
||||
|
||||
-- Construit la fonction de test selon la présence ou non d'un wildcard.
|
||||
local matches
|
||||
if pattern:find("*", 1, true) then
|
||||
local lua_pat = GlobToPattern(pattern)
|
||||
matches = function(d) return d:match(lua_pat) ~= nil end
|
||||
else
|
||||
matches = function(d) return d == pattern end
|
||||
end
|
||||
|
||||
local _, dirs1 = file.Find("*", "LUA")
|
||||
local _, dirs2 = file.Find("lua/*", "GAME")
|
||||
|
||||
for _, d in ipairs(Merge(dirs1, dirs2)) do
|
||||
if matches(d) and not IsExcluded(d) then
|
||||
table.insert(roots, d)
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(roots)
|
||||
return roots
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Polices client (Poppins)
|
||||
-- Créées quel que soit le root, donc hors du flux de chargement.
|
||||
-- ============================================================
|
||||
|
||||
if CLIENT then
|
||||
local fonts = {
|
||||
{ "bladw_text", "Poppins-SemiBold", 25, 600 },
|
||||
{ "bladw_text_Medium", "Poppins-SemiBold", 20, 800 },
|
||||
{ "bladw_text_Bold", "Poppins-Bold", 70, 700 },
|
||||
{ "bladw_text_Regular", "Poppins-Regular", 35, 400 },
|
||||
{ "bladw_text2", "Poppins-SemiBold", 21.5, 800 },
|
||||
}
|
||||
|
||||
for _, f in ipairs(fonts) do
|
||||
surface.CreateFont(f[1], {
|
||||
font = f[2],
|
||||
size = f[3],
|
||||
weight = f[4],
|
||||
antialias = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Point d'entrée
|
||||
-- ============================================================
|
||||
|
||||
Log(C.title, "")
|
||||
Log(C.title, " ╔══════════════════════════════════════╗")
|
||||
Log(C.title, " ║ NoCode — Autoloader ║")
|
||||
Log(C.title, " ╚══════════════════════════════════════╝")
|
||||
|
||||
local rootPattern = NoCode.config.root
|
||||
|
||||
-- root "none" (ou vide/nil) -> loader désactivé, on ne charge rien
|
||||
if not rootPattern or rootPattern == "" or rootPattern:lower() == "none" then
|
||||
Log(C.warn, " Root = none -> autoloader désactivé, rien n'est chargé.")
|
||||
Log(C.dim, "")
|
||||
return
|
||||
end
|
||||
|
||||
Log(C.info, " Dossier racine : lua/" .. rootPattern)
|
||||
Log(C.dim, "")
|
||||
|
||||
-- 1) Expédie les bibliothèques partagées au client (rndx, etc.)
|
||||
for _, lib in ipairs(NoCode.config.libs or {}) do
|
||||
SendFolder(lib)
|
||||
end
|
||||
|
||||
-- 2) Charge chaque module racine correspondant au préfixe
|
||||
local roots = FindRootFolders(rootPattern)
|
||||
|
||||
if #roots == 0 then
|
||||
Log(C.warn, (" [!] Aucun module trouvé pour le préfixe : %s"):format(rootPattern))
|
||||
else
|
||||
include("bladw_hud/client/cl_hud.lua")
|
||||
include("bladw_cMenu/client/cl_cMenu.lua")
|
||||
include("bladw_deathscreen/client/cl_interface_ds.lua")
|
||||
for _, root in ipairs(roots) do
|
||||
if NoCode.config.verbose then
|
||||
Log(C.title, " ▶ " .. root)
|
||||
end
|
||||
LoadFolder(root)
|
||||
end
|
||||
end
|
||||
|
||||
surface.CreateFont("bladw_text", {
|
||||
font = "Poppins-SemiBold",
|
||||
size = 25,
|
||||
weight = 600,
|
||||
antialias = true
|
||||
})
|
||||
|
||||
surface.CreateFont("bladw_text_Medium", {
|
||||
font = "Poppins-SemiBold",
|
||||
size = 20,
|
||||
weight = 800,
|
||||
antialias = true
|
||||
})
|
||||
|
||||
surface.CreateFont("bladw_text_Bold", {
|
||||
font = "Poppins-Bold",
|
||||
size = 70,
|
||||
weight = 700,
|
||||
antialias = true
|
||||
})
|
||||
|
||||
surface.CreateFont("bladw_text_Regular", {
|
||||
font = "Poppins-Regular",
|
||||
size = 35,
|
||||
weight = 400,
|
||||
antialias = true
|
||||
})
|
||||
|
||||
surface.CreateFont("bladw_text2", {
|
||||
font = "Poppins-SemiBold",
|
||||
size = 21.5,
|
||||
weight = 800,
|
||||
antialias = true
|
||||
})
|
||||
end
|
||||
Log(C.dim, "")
|
||||
Log(C.title, " ╔══════════════════════════════════════╗")
|
||||
if SERVER then
|
||||
Log(C.ok, (" ║ ✓ %d chargé(s) / %d envoyé(s) au client"):format(stats.loaded, stats.sent))
|
||||
else
|
||||
Log(C.ok, (" ║ ✓ %d fichier(s) chargé(s) [CLIENT]"):format(stats.loaded))
|
||||
end
|
||||
if stats.errors > 0 then
|
||||
Log(C.err, (" ║ ✗ %d erreur(s)"):format(stats.errors))
|
||||
end
|
||||
Log(C.title, " ╚══════════════════════════════════════╝")
|
||||
Log(C.dim, "")
|
||||
@@ -0,0 +1,367 @@
|
||||
local rndx = include("lib/rndx.lua")
|
||||
local logo = Material("nocode/announcement/logo.png")
|
||||
|
||||
BladW = BladW or {}
|
||||
BladW.Announcement = BladW.Announcement or {}
|
||||
|
||||
-- mêmes couleurs que les annonces
|
||||
local COL_BG = Color(0, 0, 0, 200)
|
||||
local COL_PANEL = Color(0, 0, 0, 160)
|
||||
local COL_ACCENT = Color(76, 119, 200)
|
||||
local COL_ACCENT_DIM = Color(60, 90, 150)
|
||||
local COL_TEXT = Color(255, 255, 255)
|
||||
local COL_RED = Color(255, 80, 80)
|
||||
local COL_GRAY = Color(54, 54, 54, 255)
|
||||
local COL_MUTED = Color(160, 160, 160)
|
||||
local COL_WARN = Color(255, 200, 50)
|
||||
local COL_LINE = Color(255, 255, 255, 60)
|
||||
|
||||
-- nb de caractères d'un champ (UTF-8, un accent = 1)
|
||||
local function EntryLen(entry)
|
||||
local v = entry:GetValue()
|
||||
return utf8.len(v) or #v
|
||||
end
|
||||
|
||||
-- petit triangle warning dessiné à la main (comme ça pas besoin de font emoji)
|
||||
local function DrawWarning(x, y, size)
|
||||
draw.NoTexture()
|
||||
surface.SetDrawColor(COL_WARN)
|
||||
surface.DrawPoly({
|
||||
{ x = x + size * 0.5, y = y },
|
||||
{ x = x + size, y = y + size },
|
||||
{ x = x, y = y + size },
|
||||
})
|
||||
draw.SimpleText("!", "DermaDefaultBold", x + size * 0.5, y + size * 0.62, Color(30, 30, 30), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
-- mon style pour les champs de texte
|
||||
local function StyleEntry(entry, placeholder, default)
|
||||
entry:SetFont("bladw_text_Medium")
|
||||
entry:SetText(default or "")
|
||||
entry:SetPlaceholderText(placeholder or "")
|
||||
entry:SetTextColor(COL_TEXT)
|
||||
entry:SetPaintBackground(false)
|
||||
entry.Paint = function(self, w, h)
|
||||
rndx.Draw(10, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||
local outline = self:IsEditing() and COL_ACCENT or COL_LINE
|
||||
rndx.DrawOutlined(10, 0, 0, w, h, outline, 2, rndx.SHAPE_FIGMA)
|
||||
self:DrawTextEntryText(COL_TEXT, COL_ACCENT, COL_TEXT)
|
||||
end
|
||||
end
|
||||
|
||||
-- petite flèche bas
|
||||
local function DrawArrow(w, h)
|
||||
draw.NoTexture()
|
||||
surface.SetDrawColor(COL_MUTED)
|
||||
local ax = w - 24
|
||||
surface.DrawPoly({
|
||||
{ x = ax, y = h * 0.5 - 2 },
|
||||
{ x = ax + 10, y = h * 0.5 - 2 },
|
||||
{ x = ax + 5, y = h * 0.5 + 4 },
|
||||
})
|
||||
end
|
||||
|
||||
local LIST_H = 130 -- hauteur du panneau de cases une fois ouvert
|
||||
local LIST_Y = 440 -- haut du panneau
|
||||
|
||||
local function OpenAAdminMenu()
|
||||
|
||||
-- si le menu est déjà ouvert je le vire
|
||||
if IsValid(BladW_AdminMenu) then BladW_AdminMenu:Remove() end
|
||||
|
||||
local selectedPreset = ""
|
||||
local maxLen = (BladW.Announcement.Conf or {}).announcement_max_length or 120
|
||||
|
||||
-- destinataires : scope + sélection multiple
|
||||
local scope, scopeLabel = "all", "Tout le monde"
|
||||
local selGroups, selPlayers = {}, {}
|
||||
local expand = 0 -- 0 = replié, 1 = ouvert (animé)
|
||||
|
||||
local frame = vgui.Create("DFrame")
|
||||
BladW_AdminMenu = frame
|
||||
frame:SetSize(500, 640) -- taille max -> sert au centrage, on démarre replié
|
||||
frame:Center()
|
||||
frame:SetTall(498)
|
||||
frame:SetTitle("")
|
||||
frame:ShowCloseButton(false)
|
||||
frame:MakePopup()
|
||||
|
||||
frame.Paint = function(self, w, h)
|
||||
rndx.Draw(20, 0, 0, w, h, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
|
||||
rndx.Draw(20, 0, 0, w, h, COL_BG, rndx.SHAPE_FIGMA)
|
||||
rndx.DrawOutlined(20, 0, 0, w, h, COL_TEXT, 3, rndx.SHAPE_FIGMA)
|
||||
|
||||
rndx.DrawMaterial(0, w * 0.5 - 30, 20, 60, 60, COL_TEXT, logo)
|
||||
draw.SimpleText("Envoyer une annonce", "bladw_text", w * 0.5, 94, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
draw.SimpleText("Presets", "bladw_text_Medium", 40, 118, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
draw.SimpleText("Destinataires", "bladw_text_Medium", 40, 372, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
-- bouton fermer
|
||||
local close = vgui.Create("DButton", frame)
|
||||
close:SetSize(30, 30)
|
||||
close:SetPos(frame:GetWide() - 42, 14)
|
||||
close:SetText("")
|
||||
close.Paint = function(self, w, h)
|
||||
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and COL_RED or COL_GRAY, rndx.SHAPE_FIGMA)
|
||||
draw.SimpleText("X", "bladw_text_Medium", w * 0.5, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
close.DoClick = function() frame:Remove() end
|
||||
|
||||
-- champs
|
||||
local titleEntry = vgui.Create("DTextEntry", frame)
|
||||
titleEntry:SetPos(40, 240)
|
||||
titleEntry:SetSize(420, 40)
|
||||
StyleEntry(titleEntry, "Titre de l'annonce", "Annonce")
|
||||
|
||||
local msgEntry = vgui.Create("DTextEntry", frame)
|
||||
msgEntry:SetPos(40, 288)
|
||||
msgEntry:SetSize(420, 40)
|
||||
StyleEntry(msgEntry, "Message de l'annonce")
|
||||
msgEntry.Paint = function(self, w, h)
|
||||
local over = EntryLen(self) > maxLen
|
||||
rndx.Draw(10, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||
local outline = over and COL_RED or (self:IsEditing() and COL_ACCENT or COL_LINE)
|
||||
rndx.DrawOutlined(10, 0, 0, w, h, outline, over and 3 or 2, rndx.SHAPE_FIGMA)
|
||||
self:DrawTextEntryText(COL_TEXT, COL_ACCENT, COL_TEXT)
|
||||
end
|
||||
|
||||
-- ligne du bas : warning à gauche, compteur à droite
|
||||
local status = vgui.Create("DPanel", frame)
|
||||
status:SetPos(40, 332)
|
||||
status:SetSize(420, 26)
|
||||
status:SetPaintBackground(false)
|
||||
status.Paint = function(self, w, h)
|
||||
local len = EntryLen(msgEntry)
|
||||
local over = len > maxLen
|
||||
|
||||
if frame.sendFeedback and (frame.sendFeedbackUntil or 0) > SysTime() then
|
||||
DrawWarning(0, h * 0.5 - 8, 16)
|
||||
draw.SimpleText(frame.sendFeedback, "bladw_text_Medium", 24, h * 0.5, COL_WARN, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
elseif over then
|
||||
DrawWarning(0, h * 0.5 - 8, 16)
|
||||
draw.SimpleText("Trop de caractères", "bladw_text_Medium", 24, h * 0.5, COL_RED, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
draw.SimpleText(len .. " / " .. maxLen, "bladw_text_Medium", w, h * 0.5,
|
||||
over and COL_RED or COL_MUTED, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
-- panneau de cases (groupes ou joueurs), hauteur animée
|
||||
local list = vgui.Create("DScrollPanel", frame)
|
||||
list:SetPos(40, LIST_Y)
|
||||
list:SetSize(420, 1)
|
||||
list.Paint = function(self, w, h)
|
||||
rndx.Draw(8, 0, 0, w, h, Color(0, 0, 0, 90), rndx.SHAPE_FIGMA)
|
||||
end
|
||||
local vbar = list:GetVBar()
|
||||
vbar:SetWide(8)
|
||||
vbar.Paint = function() end
|
||||
vbar.btnUp.Paint = function() end
|
||||
vbar.btnDown.Paint = function() end
|
||||
vbar.btnGrip.Paint = function(self, w, h)
|
||||
rndx.Draw(4, 0, 0, w, h, COL_LINE, rndx.SHAPE_FIGMA)
|
||||
end
|
||||
|
||||
local function AddRow(label, isChecked, onToggle)
|
||||
local row = list:Add("DButton")
|
||||
row:Dock(TOP)
|
||||
row:DockMargin(4, 4, 8, 0)
|
||||
row:SetTall(28)
|
||||
row:SetText("")
|
||||
row.Paint = function(self, w, h)
|
||||
rndx.Draw(6, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||
local bs, bx = 18, 8
|
||||
local by = (h - bs) * 0.5
|
||||
rndx.DrawOutlined(4, bx, by, bs, bs, isChecked() and COL_ACCENT or COL_LINE, 2, rndx.SHAPE_FIGMA)
|
||||
if isChecked() then
|
||||
rndx.Draw(3, bx + 4, by + 4, bs - 8, bs - 8, COL_ACCENT, rndx.SHAPE_FIGMA)
|
||||
end
|
||||
draw.SimpleText(label, "bladw_text_Medium", bx + bs + 10, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
row.DoClick = function() onToggle() end
|
||||
end
|
||||
|
||||
local function RebuildList()
|
||||
list:Clear()
|
||||
|
||||
if scope == "group" then
|
||||
local seen = {}
|
||||
for _, p in ipairs(player.GetAll()) do
|
||||
local g = p:GetUserGroup()
|
||||
if g and g ~= "" and not seen[g] then
|
||||
seen[g] = true
|
||||
AddRow(g, function() return selGroups[g] end, function()
|
||||
selGroups[g] = (not selGroups[g]) or nil
|
||||
end)
|
||||
end
|
||||
end
|
||||
elseif scope == "players" then
|
||||
for _, p in ipairs(player.GetAll()) do
|
||||
AddRow(p:Nick(), function() return selPlayers[p] end, function()
|
||||
selPlayers[p] = (not selPlayers[p]) or nil
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- dropdown du scope
|
||||
local scopeBtn = vgui.Create("DButton", frame)
|
||||
scopeBtn:SetPos(40, 392)
|
||||
scopeBtn:SetSize(420, 40)
|
||||
scopeBtn:SetText("")
|
||||
scopeBtn.Paint = function(self, w, h)
|
||||
rndx.Draw(10, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||
rndx.DrawOutlined(10, 0, 0, w, h, self:IsHovered() and COL_ACCENT or COL_LINE, 2, rndx.SHAPE_FIGMA)
|
||||
draw.SimpleText(scopeLabel, "bladw_text_Medium", 12, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
DrawArrow(w, h)
|
||||
end
|
||||
scopeBtn.DoClick = function(self)
|
||||
local menu = DermaMenu()
|
||||
menu:SetMinimumWidth(self:GetWide())
|
||||
menu.Paint = function(s, w, h)
|
||||
rndx.Draw(8, 0, 0, w, h, Color(18, 18, 18, 252), rndx.SHAPE_FIGMA)
|
||||
rndx.DrawOutlined(8, 0, 0, w, h, COL_LINE, 1, rndx.SHAPE_FIGMA)
|
||||
end
|
||||
|
||||
local function opt(label, id)
|
||||
local o = menu:AddOption(label, function()
|
||||
scope, scopeLabel = id, label
|
||||
RebuildList()
|
||||
end)
|
||||
o:SetTall(30)
|
||||
o:SetText("")
|
||||
o.Paint = function(s, w, h)
|
||||
if s:IsHovered() then rndx.Draw(6, 3, 1, w - 6, h - 2, COL_ACCENT, rndx.SHAPE_FIGMA) end
|
||||
draw.SimpleText(label, "bladw_text_Medium", 12, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
end
|
||||
|
||||
opt("Tout le monde", "all")
|
||||
opt("Groupe", "group")
|
||||
opt("Joueurs", "players")
|
||||
|
||||
local mx, my = self:LocalToScreen(0, self:GetTall() + 2)
|
||||
menu:Open(mx, my)
|
||||
end
|
||||
|
||||
-- bouton envoyer (position gérée par le Think selon l'ouverture)
|
||||
local send = vgui.Create("DButton", frame)
|
||||
send:SetPos(40, LIST_Y)
|
||||
send:SetSize(420, 44)
|
||||
send:SetText("")
|
||||
send.Paint = function(self, w, h)
|
||||
local over = EntryLen(msgEntry) > maxLen
|
||||
local col = over and COL_GRAY or (self:IsHovered() and COL_ACCENT or COL_ACCENT_DIM)
|
||||
rndx.Draw(12, 0, 0, w, h, col, rndx.SHAPE_FIGMA)
|
||||
draw.SimpleText("Envoyer", "bladw_text", w * 0.5, h * 0.5, over and COL_MUTED or COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
send.DoClick = function()
|
||||
local function fail(msg)
|
||||
frame.sendFeedback = msg
|
||||
frame.sendFeedbackUntil = SysTime() + 4
|
||||
surface.PlaySound("buttons/button10.wav")
|
||||
end
|
||||
|
||||
if EntryLen(msgEntry) > maxLen then return fail("Trop de caractères") end
|
||||
if msgEntry:GetValue() == "" and selectedPreset == "" then return fail("Message vide") end
|
||||
|
||||
local groups, players = {}, {}
|
||||
if scope == "group" then
|
||||
for g in pairs(selGroups) do groups[#groups + 1] = g end
|
||||
if #groups == 0 then return fail("Choisis au moins un groupe") end
|
||||
elseif scope == "players" then
|
||||
for p in pairs(selPlayers) do if IsValid(p) then players[#players + 1] = p end end
|
||||
if #players == 0 then return fail("Choisis au moins un joueur") end
|
||||
end
|
||||
|
||||
net.Start("bladWSendAnnouncement")
|
||||
net.WriteString(selectedPreset)
|
||||
net.WriteString(titleEntry:GetValue())
|
||||
net.WriteString(msgEntry:GetValue())
|
||||
net.WriteString(scope)
|
||||
if scope == "group" then
|
||||
net.WriteUInt(#groups, 8)
|
||||
for _, g in ipairs(groups) do net.WriteString(g) end
|
||||
elseif scope == "players" then
|
||||
net.WriteUInt(#players, 8)
|
||||
for _, p in ipairs(players) do net.WriteEntity(p) end
|
||||
end
|
||||
net.SendToServer()
|
||||
end
|
||||
|
||||
-- ESC ferme le menu (sans menu pause) + animation d'ouverture du panneau
|
||||
frame.Think = function(self)
|
||||
if gui.IsGameUIVisible() then
|
||||
gui.HideGameUI()
|
||||
self:Remove()
|
||||
return
|
||||
end
|
||||
|
||||
local target = (scope == "all") and 0 or 1
|
||||
expand = Lerp(FrameTime() * 12, expand, target)
|
||||
if math.abs(expand - target) < 0.002 then expand = target end
|
||||
|
||||
local lh = math.Round(expand * LIST_H)
|
||||
list:SetVisible(lh > 2)
|
||||
list:SetTall(math.max(lh, 1))
|
||||
|
||||
local sy = LIST_Y + lh + (lh > 0 and 12 or 0)
|
||||
send:SetPos(40, sy)
|
||||
self:SetTall(sy + send:GetTall() + 14)
|
||||
end
|
||||
|
||||
-- boutons preset : remplissent les champs, retiennent le preset et règlent les destinataires
|
||||
local presets = vgui.Create("DIconLayout", frame)
|
||||
presets:SetPos(40, 134)
|
||||
presets:SetSize(420, 90)
|
||||
presets:SetSpaceX(8)
|
||||
presets:SetSpaceY(8)
|
||||
|
||||
for key, preset in SortedPairs(BladW.Announcement.Presets or {}) do
|
||||
local b = presets:Add("DButton")
|
||||
b:SetSize(134, 34)
|
||||
b:SetText("")
|
||||
b.Paint = function(self, w, h)
|
||||
local on = (selectedPreset == key) or self:IsHovered()
|
||||
rndx.Draw(8, 0, 0, w, h, on and COL_ACCENT or COL_GRAY, rndx.SHAPE_FIGMA)
|
||||
draw.SimpleText(preset.title or key, "bladw_text_Medium", w * 0.5, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
b.DoClick = function()
|
||||
selectedPreset = key
|
||||
titleEntry:SetText(preset.title or "")
|
||||
msgEntry:SetText(preset.text or "")
|
||||
|
||||
for k in pairs(selGroups) do selGroups[k] = nil end
|
||||
for k in pairs(selPlayers) do selPlayers[k] = nil end
|
||||
local t = preset.target
|
||||
if not t or t == "" or t == "all" then
|
||||
scope, scopeLabel = "all", "Tout le monde"
|
||||
else
|
||||
scope, scopeLabel = "group", "Groupe"
|
||||
selGroups[t] = true
|
||||
end
|
||||
RebuildList()
|
||||
end
|
||||
end
|
||||
|
||||
RebuildList()
|
||||
|
||||
end
|
||||
|
||||
-- réponse du serveur après un envoi : publié -> on ferme, refusé -> on garde le menu + on dit pourquoi
|
||||
net.Receive("bladWAnnouncementResult", function()
|
||||
local ok = net.ReadBool()
|
||||
local left = net.ReadUInt(8)
|
||||
|
||||
if not IsValid(BladW_AdminMenu) then return end
|
||||
|
||||
if ok then
|
||||
BladW_AdminMenu:Remove()
|
||||
else
|
||||
BladW_AdminMenu.sendFeedback = left > 0 and ("Annonce déjà en cours (" .. left .. "s)") or "Envoi impossible"
|
||||
BladW_AdminMenu.sendFeedbackUntil = SysTime() + 5
|
||||
end
|
||||
end)
|
||||
|
||||
return OpenAAdminMenu
|
||||
@@ -0,0 +1,9 @@
|
||||
local OpenAAdminMenu = include("bladw_announcement/client/admin_menu/cl_menu.lua")
|
||||
|
||||
|
||||
-- le serveur me dit d'ouvrir le menu
|
||||
net.Receive("bladWOpenAnnouncementMenu", function(len, ply)
|
||||
|
||||
OpenAAdminMenu()
|
||||
|
||||
end)
|
||||
@@ -0,0 +1,236 @@
|
||||
local rndx = include("lib/rndx.lua")
|
||||
local logo = Material("nocode/announcement/logo.png")
|
||||
|
||||
-- client/ charge avant shared/, donc je m'assure que la table est là
|
||||
BladW = BladW or {}
|
||||
BladW.Announcement = BladW.Announcement or {}
|
||||
|
||||
local COL_ACCENT = Color(76, 119, 200)
|
||||
local COL_WHITE = Color(255, 255, 255)
|
||||
local COL_BG = Color(0, 0, 0, 200)
|
||||
local COL_TRACK = Color(54, 54, 54, 200)
|
||||
|
||||
-- fonts responsives (refaites quand la résolution change)
|
||||
local function AnnScale()
|
||||
return math.min(ScrW() / 1920, ScrH() / 1080)
|
||||
end
|
||||
|
||||
-- durées + easing de l'anim d'apparition/disparition
|
||||
local INTRO_TIME = 0.45
|
||||
local OUTRO_TIME = 0.35
|
||||
local function EaseOutCubic(t) return 1 - (1 - t) ^ 3 end
|
||||
|
||||
local function BuildFonts()
|
||||
local s = AnnScale()
|
||||
surface.CreateFont("bladw_ann_title", {
|
||||
font = "Poppins-SemiBold", size = math.max(1, math.Round(25 * s)), weight = 600, antialias = true,
|
||||
})
|
||||
surface.CreateFont("bladw_ann_text", {
|
||||
font = "Poppins-SemiBold", size = math.max(1, math.Round(20 * s)), weight = 800, antialias = true,
|
||||
})
|
||||
end
|
||||
BuildFonts()
|
||||
hook.Add("OnScreenSizeChanged", "BladeWin:AnnouncementFonts", BuildFonts)
|
||||
|
||||
-- coupe le texte en lignes qui rentrent dans maxW (px) pour la police donnée
|
||||
local function WrapText(text, font, maxW)
|
||||
surface.SetFont(font)
|
||||
local lines, line = {}, ""
|
||||
|
||||
for _, word in ipairs(string.Explode(" ", text or "")) do
|
||||
local test = (line == "") and word or (line .. " " .. word)
|
||||
if surface.GetTextSize(test) > maxW and line ~= "" then
|
||||
lines[#lines + 1] = line
|
||||
line = word
|
||||
else
|
||||
line = test
|
||||
end
|
||||
end
|
||||
if line ~= "" then lines[#lines + 1] = line end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
-- le contour du cadre sert de jauge : reste en colRemain, ce qui est passé en colDone (1 = plein, 0 = fini)
|
||||
local function DrawBorderTimer(x, y, w, h, r, remaining, colRemain, colDone, thickness)
|
||||
thickness = math.max(1, thickness or 3)
|
||||
|
||||
local pts = {}
|
||||
local function line(x1, y1, x2, y2)
|
||||
local steps = math.max(1, math.floor(math.Distance(x1, y1, x2, y2) / 4))
|
||||
for i = 0, steps do
|
||||
pts[#pts + 1] = { Lerp(i / steps, x1, x2), Lerp(i / steps, y1, y2) }
|
||||
end
|
||||
end
|
||||
local function arc(cx, cy, a0, a1)
|
||||
for i = 0, 6 do
|
||||
local a = math.rad(Lerp(i / 6, a0, a1))
|
||||
pts[#pts + 1] = { cx + math.cos(a) * r, cy + math.sin(a) * r }
|
||||
end
|
||||
end
|
||||
|
||||
-- sens horaire à partir du bord haut
|
||||
line(x + r, y, x + w - r, y)
|
||||
arc (x + w - r, y + r, 270, 360)
|
||||
line(x + w, y + r, x + w, y + h - r)
|
||||
arc (x + w - r, y + h - r, 0, 90)
|
||||
line(x + w - r, y + h, x + r, y + h)
|
||||
arc (x + r, y + h - r, 90, 180)
|
||||
line(x, y + h - r, x, y + r)
|
||||
arc (x + r, y + r, 180, 270)
|
||||
|
||||
local total = 0
|
||||
for i = 1, #pts - 1 do
|
||||
total = total + math.Distance(pts[i][1], pts[i][2], pts[i + 1][1], pts[i + 1][2])
|
||||
end
|
||||
if total == 0 then return end
|
||||
|
||||
local consumedLen = (1 - remaining) * total
|
||||
draw.NoTexture()
|
||||
|
||||
local acc = 0
|
||||
for i = 1, #pts - 1 do
|
||||
local x1, y1 = pts[i][1], pts[i][2]
|
||||
local x2, y2 = pts[i + 1][1], pts[i + 1][2]
|
||||
local segLen = math.Distance(x1, y1, x2, y2)
|
||||
|
||||
surface.SetDrawColor(acc < consumedLen and colDone or colRemain)
|
||||
surface.DrawTexturedRectRotated((x1 + x2) * 0.5, (y1 + y2) * 0.5,
|
||||
segLen + thickness, thickness, -math.deg(math.atan2(y2 - y1, x2 - x1)))
|
||||
|
||||
acc = acc + segLen
|
||||
end
|
||||
end
|
||||
|
||||
-- état de l'annonce en cours (survit au reload)
|
||||
BladW.Announcement.Active = BladW.Announcement.Active or false
|
||||
BladW.Announcement.Title = BladW.Announcement.Title or "Annonce"
|
||||
BladW.Announcement.Text = BladW.Announcement.Text or ""
|
||||
BladW.Announcement.Style = BladW.Announcement.Style or "loader"
|
||||
BladW.Announcement.Duration = BladW.Announcement.Duration or 10
|
||||
BladW.Announcement.StartTime = BladW.Announcement.StartTime or 0
|
||||
|
||||
-- lance une annonce. style/duration vides = valeurs de la config
|
||||
function BladW.Announcement.Show(title, text, style, duration)
|
||||
local Conf = BladW.Announcement.Conf or {}
|
||||
|
||||
BladW.Announcement.Title = (title and title ~= "") and title or "Annonce"
|
||||
BladW.Announcement.Text = text or ""
|
||||
BladW.Announcement.Style = (style and style ~= "") and style or (Conf.announcement_style or "loader")
|
||||
BladW.Announcement.Duration = (duration and duration > 0) and duration or (Conf.announcement_duration or 10)
|
||||
BladW.Announcement.StartTime = CurTime()
|
||||
BladW.Announcement.Active = true
|
||||
|
||||
if Conf.announcement_sound and Conf.announcement_sound ~= "" then
|
||||
surface.PlaySound(Conf.announcement_sound)
|
||||
end
|
||||
end
|
||||
|
||||
-- le serveur m'envoie une annonce à afficher
|
||||
net.Receive("bladWShowAnnouncement", function()
|
||||
local title = net.ReadString()
|
||||
local text = net.ReadString()
|
||||
local style = net.ReadString()
|
||||
local duration = net.ReadUInt(16)
|
||||
BladW.Announcement.Show(title, text, style, duration)
|
||||
end)
|
||||
|
||||
-- affichage (responsive + s'adapte au message)
|
||||
hook.Add("HUDPaint", "BladeWin:Announcement", function()
|
||||
if not BladW.Announcement.Active then return end
|
||||
|
||||
local ply = LocalPlayer()
|
||||
if not IsValid(ply) then return end
|
||||
|
||||
local duration = BladW.Announcement.Duration or 10
|
||||
local elapsed = CurTime() - BladW.Announcement.StartTime
|
||||
|
||||
-- temps écoulé -> on cache
|
||||
if elapsed >= duration then
|
||||
BladW.Announcement.Active = false
|
||||
return
|
||||
end
|
||||
|
||||
local style = BladW.Announcement.Style or "loader"
|
||||
local title = BladW.Announcement.Title
|
||||
local text = BladW.Announcement.Text
|
||||
local remaining = math.Clamp(1 - (elapsed / duration), 0, 1)
|
||||
|
||||
-- échelle responsive
|
||||
local s = AnnScale()
|
||||
local padX = 26 * s
|
||||
local padY = 18 * s
|
||||
local gap = 8 * s
|
||||
local logoS = 62 * s
|
||||
local barW = 200 * s
|
||||
local barH = 18 * s
|
||||
local radius = 14 * s
|
||||
|
||||
-- wrap du message (largeur interne responsive, la vraie limite c'est le nb de caractères)
|
||||
local wrapW = ScrW() * 0.8
|
||||
local lines = WrapText(text, "bladw_ann_text", wrapW)
|
||||
|
||||
surface.SetFont("bladw_ann_title")
|
||||
local titleW, titleH = surface.GetTextSize(title)
|
||||
|
||||
surface.SetFont("bladw_ann_text")
|
||||
local _, lineH = surface.GetTextSize("Ay")
|
||||
local msgW = 0
|
||||
for _, l in ipairs(lines) do
|
||||
msgW = math.max(msgW, (surface.GetTextSize(l)))
|
||||
end
|
||||
|
||||
-- taille de la box selon le contenu
|
||||
local contentW = math.max(titleW, msgW, style == "loader" and barW or 0)
|
||||
local boxW = math.Clamp(contentW + padX * 2, 260 * s, ScrW() * 0.92)
|
||||
local msgH = #lines * lineH
|
||||
local boxH = padY + logoS + gap + titleH + gap + msgH
|
||||
+ (style == "loader" and (gap + barH) or 0) + padY
|
||||
|
||||
-- anim : fondu + glisse du haut (et l'inverse quand ça part)
|
||||
local appear = math.Clamp(elapsed / INTRO_TIME, 0, 1)
|
||||
local disappear = math.Clamp((duration - elapsed) / OUTRO_TIME, 0, 1)
|
||||
local anim = math.min(EaseOutCubic(appear), EaseOutCubic(disappear)) -- 0 -> 1 -> 0
|
||||
local slide = (1 - anim) * 26 * s
|
||||
|
||||
local boxX = (ScrW() - boxW) * 0.5
|
||||
local boxY = ScrH() * 0.035 - slide
|
||||
local cx = boxX + boxW * 0.5
|
||||
|
||||
surface.SetAlphaMultiplier(anim)
|
||||
|
||||
-- fond
|
||||
rndx.Draw(radius, boxX, boxY, boxW, boxH, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
|
||||
rndx.Draw(radius, boxX, boxY, boxW, boxH, COL_BG, rndx.SHAPE_FIGMA)
|
||||
|
||||
-- le loader garde son contour blanc (le simple lui a sa jauge sur le bord)
|
||||
if style == "loader" then
|
||||
rndx.DrawOutlined(radius, boxX, boxY, boxW, boxH, COL_WHITE, 3 * s, rndx.SHAPE_FIGMA)
|
||||
end
|
||||
|
||||
-- contenu empilé et centré
|
||||
local y = boxY + padY
|
||||
rndx.DrawMaterial(0, cx - logoS * 0.5, y, logoS, logoS, COL_WHITE, logo)
|
||||
y = y + logoS + gap
|
||||
|
||||
draw.SimpleText(title, "bladw_ann_title", cx, y, COL_WHITE, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP)
|
||||
y = y + titleH + gap
|
||||
|
||||
for _, l in ipairs(lines) do
|
||||
draw.SimpleText(l, "bladw_ann_text", cx, y, COL_WHITE, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP)
|
||||
y = y + lineH
|
||||
end
|
||||
|
||||
-- jauge de temps
|
||||
if style == "loader" then
|
||||
y = y + gap
|
||||
local bx = cx - barW * 0.5
|
||||
rndx.Draw(barH * 0.5, bx, y, barW, barH, COL_TRACK, rndx.SHAPE_FIGMA)
|
||||
rndx.Draw(barH * 0.5, bx, y, barW * remaining, barH, COL_ACCENT, rndx.SHAPE_FIGMA)
|
||||
else
|
||||
-- le bord bleu qui se vide en blanc
|
||||
DrawBorderTimer(boxX, boxY, boxW, boxH, radius, remaining, COL_ACCENT, COL_WHITE, 3 * s)
|
||||
end
|
||||
|
||||
surface.SetAlphaMultiplier(1)
|
||||
end)
|
||||
@@ -0,0 +1,141 @@
|
||||
BladW = BladW or {}
|
||||
BladW.Announcement = BladW.Announcement or {}
|
||||
|
||||
-- pour que les clients téléchargent le son
|
||||
resource.AddFile("sound/announce.mp3")
|
||||
|
||||
-- coupe une chaîne à maxChars caractères (propre avec les accents)
|
||||
local function LimitChars(str, maxChars)
|
||||
str = tostring(str or "")
|
||||
local n = utf8.len(str)
|
||||
if not n or n <= maxChars then return str end
|
||||
local endByte = utf8.offset(str, maxChars + 1)
|
||||
return endByte and string.sub(str, 1, endByte - 1) or str
|
||||
end
|
||||
|
||||
-- ouvre le menu pour le joueur (faut avoir le droit)
|
||||
concommand.Add("bladw_amenu", function(ply, cmd)
|
||||
if not IsValid(ply) or not BladW.Announcement.HasAccess(ply) then return end
|
||||
|
||||
net.Start("bladWOpenAnnouncementMenu")
|
||||
net.Send(ply)
|
||||
end)
|
||||
|
||||
-- /bannonce (ou !bannonce) dans le chat = ouvre le menu
|
||||
hook.Add("PlayerSay", "BladW:AnnouncementChatCmd", function(ply, text)
|
||||
local cmd = string.lower(string.Trim(text))
|
||||
if cmd ~= "/bannonce" and cmd ~= "!bannonce" then return end
|
||||
|
||||
-- que ceux qui ont le droit ouvrent le menu, mais on cache la commande pour tout le monde
|
||||
if IsValid(ply) and BladW.Announcement.HasAccess(ply) then
|
||||
net.Start("bladWOpenAnnouncementMenu")
|
||||
net.Send(ply)
|
||||
end
|
||||
|
||||
return "" -- commande reconnue -> on la retire du chat
|
||||
end)
|
||||
|
||||
-- reconstruit l'annonce finale : le preset sert de base, title/text passent devant
|
||||
function BladW.Announcement.Resolve(entry)
|
||||
local presets = BladW.Announcement.Presets or {}
|
||||
local base = (entry.preset and presets[entry.preset]) or {}
|
||||
|
||||
return {
|
||||
title = (entry.title and entry.title ~= "" and entry.title) or base.title or "Annonce",
|
||||
text = (entry.text and entry.text ~= "" and entry.text ) or base.text or "",
|
||||
style = entry.style or base.style or "",
|
||||
duration = tonumber(entry.duration) or tonumber(base.duration) or 0,
|
||||
target = entry.target or base.target, -- usergroup ciblé (nil = tout le monde)
|
||||
}
|
||||
end
|
||||
|
||||
-- envoie l'annonce à tout le monde. false si rien à envoyer ou si une annonce est déjà en cours
|
||||
function BladW.Announcement.Broadcast(entry)
|
||||
-- déjà une annonce à l'écran ? on n'en empile pas une autre
|
||||
if CurTime() < (BladW.Announcement.activeUntil or 0) then return false end
|
||||
|
||||
local a = BladW.Announcement.Resolve(entry)
|
||||
if a.text == "" then return false end
|
||||
|
||||
-- destinataires (nil = tout le monde) :
|
||||
-- liste de joueurs (menu) > liste de groupes (menu) + target unique (preset/schedule)
|
||||
local recipients
|
||||
if istable(entry.players) and #entry.players > 0 then
|
||||
recipients = {}
|
||||
for _, p in ipairs(entry.players) do
|
||||
if IsValid(p) and p:IsPlayer() then recipients[#recipients + 1] = p end
|
||||
end
|
||||
elseif (istable(entry.groups) and #entry.groups > 0)
|
||||
or (a.target and a.target ~= "" and a.target ~= "all") then
|
||||
local set = {}
|
||||
if istable(entry.groups) then
|
||||
for _, g in ipairs(entry.groups) do set[g] = true end
|
||||
end
|
||||
if a.target and a.target ~= "" and a.target ~= "all" then set[a.target] = true end
|
||||
|
||||
recipients = {}
|
||||
for _, p in ipairs(player.GetAll()) do
|
||||
if set[p:GetUserGroup()] then recipients[#recipients + 1] = p end
|
||||
end
|
||||
end
|
||||
|
||||
if recipients and #recipients == 0 then return false end -- personne à qui envoyer
|
||||
|
||||
local Conf = BladW.Announcement.Conf or {}
|
||||
local maxLen = tonumber(Conf.announcement_max_length) or 120
|
||||
local dur = a.duration > 0 and a.duration or (tonumber(Conf.announcement_duration) or 10)
|
||||
|
||||
-- on bloque les suivantes le temps que celle-ci s'affiche
|
||||
BladW.Announcement.activeUntil = CurTime() + dur
|
||||
|
||||
net.Start("bladWShowAnnouncement")
|
||||
net.WriteString(LimitChars(a.title, 64))
|
||||
net.WriteString(LimitChars(a.text, maxLen))
|
||||
net.WriteString(a.style or "")
|
||||
net.WriteUInt(math.Clamp(a.duration, 0, 65535), 16)
|
||||
if recipients then net.Send(recipients) else net.Broadcast() end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- un admin veut poster une annonce -> je la diffuse
|
||||
net.Receive("bladWSendAnnouncement", function(len, ply)
|
||||
if not IsValid(ply) or not BladW.Announcement.HasAccess(ply) then return end
|
||||
|
||||
local preset = net.ReadString()
|
||||
local title = net.ReadString()
|
||||
local text = net.ReadString()
|
||||
|
||||
-- destinataires : "all" / "group" (liste de groupes) / "players" (liste de joueurs)
|
||||
-- le choix du menu est explicite -> il prime sur le target du preset
|
||||
local scope = net.ReadString()
|
||||
local target, groups, players = "all", nil, nil
|
||||
if scope == "group" then
|
||||
target = "" -- vide = on ignore le target du preset, on prend la liste
|
||||
groups = {}
|
||||
for _ = 1, net.ReadUInt(8) do groups[#groups + 1] = net.ReadString() end
|
||||
elseif scope == "players" then
|
||||
target = ""
|
||||
players = {}
|
||||
for _ = 1, net.ReadUInt(8) do
|
||||
local p = net.ReadEntity()
|
||||
if IsValid(p) and p:IsPlayer() then players[#players + 1] = p end
|
||||
end
|
||||
end
|
||||
|
||||
local ok = BladW.Announcement.Broadcast({
|
||||
preset = preset ~= "" and preset or nil,
|
||||
title = title,
|
||||
text = text,
|
||||
target = target,
|
||||
groups = groups,
|
||||
players = players,
|
||||
})
|
||||
|
||||
-- je réponds à l'admin : publié -> il ferme le menu, refusé -> il garde son texte
|
||||
local left = math.Clamp(math.ceil((BladW.Announcement.activeUntil or 0) - CurTime()), 0, 255)
|
||||
net.Start("bladWAnnouncementResult")
|
||||
net.WriteBool(ok)
|
||||
net.WriteUInt(left, 8)
|
||||
net.Send(ply)
|
||||
end)
|
||||
@@ -0,0 +1,4 @@
|
||||
util.AddNetworkString("bladWOpenAnnouncementMenu") -- ouvre le menu chez le client
|
||||
util.AddNetworkString("bladWSendAnnouncement") -- l'admin m'envoie son annonce
|
||||
util.AddNetworkString("bladWShowAnnouncement") -- je balance l'annonce à tout le monde
|
||||
util.AddNetworkString("bladWAnnouncementResult") -- je dis à l'admin si son annonce est passée
|
||||
@@ -0,0 +1,54 @@
|
||||
BladW = BladW or {}
|
||||
BladW.Announcement = BladW.Announcement or {}
|
||||
|
||||
-- le droit géré par l'admin mod (SAM / ULX / FAdmin)
|
||||
local PRIV = "bladw_announce"
|
||||
BladW.Announcement.Privilege = PRIV
|
||||
|
||||
-- j'enregistre le droit dans l'admin mod présent, pour qu'il apparaisse dans son menu
|
||||
local function Register()
|
||||
-- SAM
|
||||
if sam and sam.permissions and sam.permissions.add then
|
||||
pcall(sam.permissions.add, PRIV, "BladW Annonce", "admin")
|
||||
end
|
||||
|
||||
-- ULX / ULib
|
||||
if ULib and ULib.ucl and ULib.ucl.registerAccess then
|
||||
pcall(ULib.ucl.registerAccess, PRIV, "admin", "Gérer les annonces BladW", "BladW")
|
||||
end
|
||||
|
||||
-- FAdmin (DarkRP) : 2 = admin
|
||||
if FAdmin and FAdmin.Access and FAdmin.Access.AddPrivilege then
|
||||
pcall(FAdmin.Access.AddPrivilege, PRIV, 2)
|
||||
end
|
||||
end
|
||||
|
||||
-- SAM veut qu'on (ré)enregistre ses permissions sur ce hook
|
||||
hook.Add("SAM.LoadPermissions", "BladW:AnnouncePriv", Register)
|
||||
-- les autres mods sont parfois chargés après le serveur -> on retente
|
||||
hook.Add("InitPostEntity", "BladW:AnnouncePriv", Register)
|
||||
-- et un coup direct au cas où le mod est déjà là (hot-reload)
|
||||
Register()
|
||||
|
||||
-- est-ce que ce joueur a le droit de gérer les annonces ?
|
||||
function BladW.Announcement.HasAccess(ply)
|
||||
if not IsValid(ply) then return false end
|
||||
|
||||
-- SAM
|
||||
if sam and sam.player and sam.player.has_permission then
|
||||
return sam.player.has_permission(ply, PRIV)
|
||||
end
|
||||
|
||||
-- ULX / ULib
|
||||
if ULib and ULib.ucl and ULib.ucl.query then
|
||||
return ULib.ucl.query(ply, PRIV)
|
||||
end
|
||||
|
||||
-- FAdmin (DarkRP)
|
||||
if FAdmin and FAdmin.Access and FAdmin.Access.PlayerHasPrivilege then
|
||||
return FAdmin.Access.PlayerHasPrivilege(ply, PRIV)
|
||||
end
|
||||
|
||||
-- pas d'admin mod -> on retombe sur l'admin de base
|
||||
return ply:IsAdmin()
|
||||
end
|
||||
@@ -0,0 +1,145 @@
|
||||
BladW = BladW or {}
|
||||
BladW.Announcement = BladW.Announcement or {}
|
||||
|
||||
-- planificateur d'annonces (intervalle + cron), lit BladW.Announcement.Schedules
|
||||
|
||||
local TIMER_PREFIX = "BladW_Announcement_Sched_"
|
||||
local CRON_TIMER = "BladW_Announcement_Cron"
|
||||
|
||||
-- je garde les noms des timers sur la table pour bien les nettoyer au reload
|
||||
BladW.Announcement._timers = BladW.Announcement._timers or {}
|
||||
|
||||
local function Warn(msg)
|
||||
MsgC(Color(255, 200, 50), "[BladW Annonce] " .. msg .. "\n")
|
||||
end
|
||||
|
||||
-- "30m" / "90s" / "2h" / "1d" -> secondes
|
||||
local function ParseInterval(str)
|
||||
if isnumber(str) then return str end
|
||||
if not isstring(str) then return nil end
|
||||
|
||||
local num = tonumber(string.match(str, "^%s*([%d%.]+)"))
|
||||
if not num then return nil end
|
||||
|
||||
local unit = string.lower(string.match(str, "([smhd])%s*$") or "s")
|
||||
local mult = ({ s = 1, m = 60, h = 3600, d = 86400 })[unit] or 1
|
||||
return num * mult
|
||||
end
|
||||
|
||||
-- un champ cron ("*", "*/5", "1-5", "1,3,5", "7") -> fonction(v) qui dit si ça matche
|
||||
local function CompileField(field, lo, hi)
|
||||
local ranges = {}
|
||||
|
||||
for part in string.gmatch(field, "[^,]+") do
|
||||
local base, step = part, 1
|
||||
|
||||
local slash = string.find(part, "/", 1, true)
|
||||
if slash then
|
||||
base = string.sub(part, 1, slash - 1)
|
||||
step = tonumber(string.sub(part, slash + 1)) or 1
|
||||
end
|
||||
|
||||
local a, b
|
||||
if base == "*" then
|
||||
a, b = lo, hi
|
||||
else
|
||||
local dash = string.find(base, "-", 1, true)
|
||||
if dash then
|
||||
a = tonumber(string.sub(base, 1, dash - 1))
|
||||
b = tonumber(string.sub(base, dash + 1))
|
||||
else
|
||||
a = tonumber(base)
|
||||
b = a
|
||||
end
|
||||
end
|
||||
|
||||
if a and b then
|
||||
ranges[#ranges + 1] = { a = a, b = b, step = math.max(step, 1) }
|
||||
end
|
||||
end
|
||||
|
||||
return function(v)
|
||||
for _, r in ipairs(ranges) do
|
||||
if v >= r.a and v <= r.b and ((v - r.a) % r.step == 0) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- "min heure jour mois jour-semaine" -> les matchers, nil si c'est pas 5 champs
|
||||
local function CompileCron(str)
|
||||
local fields = {}
|
||||
for tok in string.gmatch(str, "%S+") do fields[#fields + 1] = tok end
|
||||
if #fields ~= 5 then return nil end
|
||||
|
||||
return {
|
||||
min = CompileField(fields[1], 0, 59),
|
||||
hour = CompileField(fields[2], 0, 23),
|
||||
day = CompileField(fields[3], 1, 31),
|
||||
mon = CompileField(fields[4], 1, 12),
|
||||
wday = CompileField(fields[5], 0, 6), -- 0 = dimanche
|
||||
}
|
||||
end
|
||||
|
||||
local function CronMatches(c, t)
|
||||
local wday = t.wday - 1 -- os.date : 1=dim..7=sam, moi je veux 0..6
|
||||
return c.min(t.min) and c.hour(t.hour) and c.day(t.day) and c.mon(t.month) and c.wday(wday)
|
||||
end
|
||||
|
||||
-- (re)fabrique tous les timers depuis la config
|
||||
function BladW.Announcement.RebuildSchedules()
|
||||
-- on vire les anciens
|
||||
for _, name in ipairs(BladW.Announcement._timers) do timer.Remove(name) end
|
||||
timer.Remove(CRON_TIMER)
|
||||
BladW.Announcement._timers = {}
|
||||
|
||||
local schedules = BladW.Announcement.Schedules or {}
|
||||
local crons = {}
|
||||
|
||||
for i, entry in ipairs(schedules) do
|
||||
if entry.cron then
|
||||
local compiled = CompileCron(entry.cron)
|
||||
if compiled then
|
||||
crons[#crons + 1] = { entry = entry, cron = compiled, lastMinute = -1 }
|
||||
else
|
||||
Warn('cron invalide (attendu "m h jm mo js") : "' .. tostring(entry.cron) .. '"')
|
||||
end
|
||||
|
||||
elseif entry.every then
|
||||
local secs = ParseInterval(entry.every)
|
||||
if secs and secs > 0 then
|
||||
local name = TIMER_PREFIX .. i
|
||||
timer.Create(name, secs, 0, function()
|
||||
BladW.Announcement.Broadcast(entry)
|
||||
end)
|
||||
BladW.Announcement._timers[#BladW.Announcement._timers + 1] = name
|
||||
else
|
||||
Warn('intervalle invalide (ex "30m") : "' .. tostring(entry.every) .. '"')
|
||||
end
|
||||
|
||||
else
|
||||
Warn("entrée #" .. i .. " ignorée : ni 'every' ni 'cron'.")
|
||||
end
|
||||
end
|
||||
|
||||
-- un seul timer check tous les cron (tick 20s, une fois par minute qui matche)
|
||||
if #crons > 0 then
|
||||
timer.Create(CRON_TIMER, 20, 0, function()
|
||||
local now = os.time()
|
||||
local minuteKey = math.floor(now / 60)
|
||||
local t = os.date("*t", now)
|
||||
|
||||
for _, c in ipairs(crons) do
|
||||
if c.lastMinute ~= minuteKey and CronMatches(c.cron, t) then
|
||||
c.lastMinute = minuteKey
|
||||
BladW.Announcement.Broadcast(c.entry)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- le config (shared) charge après server/, du coup j'attends une frame que Presets/Schedules soient là
|
||||
timer.Simple(0, function()
|
||||
BladW.Announcement.RebuildSchedules()
|
||||
end)
|
||||
@@ -0,0 +1,10 @@
|
||||
BladW = BladW or {}
|
||||
BladW.Announcement = BladW.Announcement or {}
|
||||
BladW.Announcement.Conf = BladW.Announcement.Conf or {}
|
||||
|
||||
BladW.Announcement.Conf = {
|
||||
announcement_style = "simple", -- "simple" ou "loader"
|
||||
announcement_duration = 10, -- durée d'affichage en secondes
|
||||
announcement_max_length = 80, -- max de caractères pour le message
|
||||
announcement_sound = "announce.mp3", -- son à l'apparition ("" = rien), fichier dans sound/ | ex : garrysmod/content_downloaded.wav
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
BladW = BladW or {}
|
||||
BladW.Announcement = BladW.Announcement or {}
|
||||
|
||||
-- mes presets d'annonce (réutilisables dans le menu et par le planificateur)
|
||||
-- title / text, et en option style ("simple"/"loader") + duration (sinon valeurs de sh_config)
|
||||
-- target : à qui c'est envoyé -> "all" (tout le monde) ou un usergroup ("vip", "admin"...)
|
||||
-- la clé (restart, shutdown...) sert d'identifiant dans sh_schedule.lua
|
||||
BladW.Announcement.Presets = {
|
||||
|
||||
restart = {
|
||||
title = "Redémarrage",
|
||||
text = "Le serveur va redémarrer dans quelques instants. Reconnexion sous peu !",
|
||||
style = "loader",
|
||||
duration = 15,
|
||||
target = "all",
|
||||
},
|
||||
|
||||
shutdown = {
|
||||
title = "Arrêt du serveur",
|
||||
text = "Le serveur va s'arrêter. Merci d'avoir joué avec nous !",
|
||||
style = "loader",
|
||||
duration = 15,
|
||||
target = "all",
|
||||
},
|
||||
|
||||
maintenance = {
|
||||
title = "Maintenance",
|
||||
text = "Une maintenance va débuter, le serveur peut lag ou redémarrer.",
|
||||
style = "simple",
|
||||
duration = 12,
|
||||
target = "all",
|
||||
},
|
||||
|
||||
event = {
|
||||
title = "Événement",
|
||||
text = "Un événement démarre bientôt, rejoignez-nous !",
|
||||
style = "loader",
|
||||
duration = 12,
|
||||
target = "all",
|
||||
},
|
||||
|
||||
discord = {
|
||||
title = "Discord",
|
||||
text = "Rejoignez notre Discord pour rester informé des nouveautés !",
|
||||
style = "simple",
|
||||
duration = 10,
|
||||
target = "all",
|
||||
},
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
BladW = BladW or {}
|
||||
BladW.Announcement = BladW.Announcement or {}
|
||||
|
||||
-- ─────────────────────────────────────────────
|
||||
-- Annonces automatiques (mon petit cron maison)
|
||||
-- Chaque ligne = une annonce qui part toute seule.
|
||||
--
|
||||
-- QUAND ? une seule des deux clés :
|
||||
--
|
||||
-- every = "30m" -> ça répète tous les X. Unités : s (sec), m (min), h (heure), d (jour)
|
||||
-- ex : "45s", "10m", "2h", "1d"
|
||||
--
|
||||
-- cron = "0 3 * * *" -> comme sur Linux, 5 champs séparés par un espace :
|
||||
-- 1) minute 0-59
|
||||
-- 2) heure 0-23
|
||||
-- 3) jour du mois 1-31
|
||||
-- 4) mois 1-12
|
||||
-- 5) jour semaine 0-6 (0 = dimanche)
|
||||
--
|
||||
-- dans chaque champ je peux mettre :
|
||||
-- * -> tout le temps
|
||||
-- */5 -> tous les 5 (0, 5, 10...)
|
||||
-- 1-5 -> une plage (de 1 à 5)
|
||||
-- 1,3,5 -> une liste
|
||||
--
|
||||
-- quelques exemples qui parlent :
|
||||
-- "*/15 * * * *" -> toutes les 15 min
|
||||
-- "0 * * * *" -> à chaque heure pile
|
||||
-- "0 */2 * * *" -> toutes les 2 heures
|
||||
-- "0 20 * * 5" -> tous les vendredis à 20h
|
||||
-- "30 4 1 * *" -> le 1er du mois à 4h30
|
||||
-- (c'est l'heure du serveur qui compte)
|
||||
--
|
||||
-- QUOI ? soit un preset, soit du sur-mesure :
|
||||
-- preset = "restart" -> reprend un preset (voir sh_presets.lua)
|
||||
-- title = "...", text = "..." -> annonce perso
|
||||
-- (si je mets les deux, title/text passent devant le preset)
|
||||
--
|
||||
-- À QUI ? en option :
|
||||
-- target = "vip" -> seulement l'usergroup "vip" (sinon tout le monde)
|
||||
--
|
||||
-- Pour couper une ligne : je la commente (--) ou je la dégage.
|
||||
-- ─────────────────────────────────────────────
|
||||
BladW.Announcement.Schedules = {
|
||||
|
||||
-- rappel Discord toutes les 30 min
|
||||
{ every = "30m", preset = "discord" },
|
||||
|
||||
-- petite astuce à chaque heure pile
|
||||
{ cron = "0 * * * *", title = "Astuce", text = "Tapez /bannonce pour gérer les annonces." },
|
||||
|
||||
-- exemples à décommenter au besoin
|
||||
-- { cron = "0 5 * * *", preset = "restart" }, -- reboot tous les jours à 5h
|
||||
-- { cron = "0 20 * * 5", preset = "event" }, -- event le vendredi à 20h
|
||||
-- { every = "2h", preset = "discord" }, -- rappel Discord toutes les 2h
|
||||
|
||||
}
|
||||
@@ -54,6 +54,30 @@ local function HoveredButton2(button, label, font)
|
||||
end
|
||||
end
|
||||
|
||||
local function CreateMenuButton(parent, y, label, command)
|
||||
local btn = vgui.Create("DButton", parent)
|
||||
btn:SetSize(RX(400), RY(122))
|
||||
btn:SetPos(RX(0), RY(y))
|
||||
btn:SetText("")
|
||||
HoveredButton2(btn, label, "bladw_text")
|
||||
btn.DoClick = function()
|
||||
ply:ConCommand(command)
|
||||
end
|
||||
return btn
|
||||
end
|
||||
|
||||
local function CreateLinkButton(parent, x, y, icon, label, url)
|
||||
local btn = vgui.Create("DButton", parent)
|
||||
btn:SetSize(RX(150), RY(51))
|
||||
btn:SetPos(RX(x), RY(y))
|
||||
btn:SetText("")
|
||||
HoveredButton(btn, icon, label, "bladw_text_Medium")
|
||||
btn.DoClick = function()
|
||||
gui.OpenURL(url)
|
||||
end
|
||||
return btn
|
||||
end
|
||||
|
||||
local function CloseCMenu()
|
||||
if IsValid(CMenu) then
|
||||
CMenu:Remove()
|
||||
@@ -62,6 +86,20 @@ local function CloseCMenu()
|
||||
end
|
||||
end
|
||||
|
||||
local cMenuButtons = {
|
||||
{ y = 250, label = "3ème personne", command = "bladw_3pers" },
|
||||
{ y = 325, label = "Stopsound", command = "bladw_sound" },
|
||||
{ y = 400, label = "Appelez un staff", command = "bladw_staff" },
|
||||
{ y = 475, label = "Jeter de l'argent",command = "bladw_money" },
|
||||
{ y = 550, label = "Jeter une arme", command = "bladw_weapon" },
|
||||
}
|
||||
|
||||
local linkButtons = {
|
||||
{ x = 50, y = 942, icon = bladw_materials.discord, label = "Discord", url = "https://discord.gg/KsE4Xef6vg" },
|
||||
{ x = 207, y = 942, icon = bladw_materials.steam, label = "Steam", url = "" },
|
||||
{ x = 125, y = 1005, icon = bladw_materials.website, label = "Website", url = "" },
|
||||
}
|
||||
|
||||
local function GetStats()
|
||||
local nbplayer = player.GetCount()
|
||||
local nbpolice = team.NumPlayers(TEAM_POLICE)
|
||||
@@ -70,10 +108,9 @@ local function GetStats()
|
||||
return nbplayer, nbpolice, nbpompiers
|
||||
end
|
||||
|
||||
local nbplayer, nbpolice, nbpompiers = GetStats()
|
||||
|
||||
function OpenCMenu()
|
||||
if IsValid(CMenu) then return end
|
||||
local nbplayer, nbpolice, nbpompiers = GetStats()
|
||||
|
||||
local ply = LocalPlayer()
|
||||
|
||||
@@ -93,7 +130,7 @@ function OpenCMenu()
|
||||
|
||||
surface.SetFont("bladw_text")
|
||||
local before = "Il y a "
|
||||
local count = tostring(nbplayer)
|
||||
local count = nbplayer
|
||||
local after = " joueurs sur le serveur"
|
||||
|
||||
local beforeW, _ = surface.GetTextSize(before)
|
||||
@@ -106,49 +143,8 @@ function OpenCMenu()
|
||||
draw.SimpleText(after, "bladw_text", startX + beforeW + countW, RY(900), Color(255, 255, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
button3pers = vgui.Create("DButton", CMenu)
|
||||
button3pers:SetSize(RX(400), RY(122))
|
||||
button3pers:SetPos(RX(0), RY(250))
|
||||
button3pers:SetText("")
|
||||
HoveredButton2(button3pers, "3ème personne", "bladw_text")
|
||||
button3pers.DoClick = function()
|
||||
ply:ConCommand("bladw_3pers")
|
||||
end
|
||||
|
||||
SoundButton = vgui.Create("DButton", CMenu)
|
||||
SoundButton:SetSize(RX(400), RY(122))
|
||||
SoundButton:SetPos(RX(0), RY(325))
|
||||
SoundButton:SetText("")
|
||||
HoveredButton2(SoundButton, "Stopsound", "bladw_text")
|
||||
SoundButton.DoClick = function()
|
||||
ply:ConCommand("bladw_sound")
|
||||
end
|
||||
|
||||
staffbutton = vgui.Create("DButton", CMenu)
|
||||
staffbutton:SetSize(RX(400), RY(122))
|
||||
staffbutton:SetPos(RX(0), RY(400))
|
||||
staffbutton:SetText("")
|
||||
HoveredButton2(staffbutton, "Appelez un staff", "bladw_text")
|
||||
staffbutton.DoClick = function()
|
||||
ply:ConCommand("bladw_staff")
|
||||
end
|
||||
|
||||
moneybutton = vgui.Create("DButton", CMenu)
|
||||
moneybutton:SetSize(RX(400), RY(122))
|
||||
moneybutton:SetPos(RX(0), RY(475))
|
||||
moneybutton:SetText("")
|
||||
HoveredButton2(moneybutton, "Jeter de l'argent", "bladw_text")
|
||||
moneybutton.DoClick = function()
|
||||
ply:ConCommand("bladw_money")
|
||||
end
|
||||
|
||||
weaponbutton = vgui.Create("DButton", CMenu)
|
||||
weaponbutton:SetSize(RX(400), RY(122))
|
||||
weaponbutton:SetPos(RX(0), RY(550))
|
||||
weaponbutton:SetText("")
|
||||
HoveredButton2(weaponbutton, "Jeter une arme", "bladw_text")
|
||||
weaponbutton.DoClick = function()
|
||||
ply:ConCommand("bladw_weapon")
|
||||
for _, btnData in ipairs(cMenuButtons) do
|
||||
CreateMenuButton(CMenu, btnData.y, btnData.label, btnData.command)
|
||||
end
|
||||
|
||||
PoliceButton = vgui.Create("DPanel", CMenu)
|
||||
@@ -191,32 +187,8 @@ function OpenCMenu()
|
||||
draw.SimpleText(after, "bladw_text_Medium", startX + countW, h / 2, Color(255, 255, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
DiscordButton = vgui.Create("DButton", CMenu)
|
||||
DiscordButton:SetSize(RX(150), RY(51))
|
||||
DiscordButton:SetPos(RX(50), RY(942))
|
||||
DiscordButton:SetText("")
|
||||
HoveredButton(DiscordButton, bladw_materials.discord, "Discord", "bladw_text_Medium")
|
||||
|
||||
DiscordButton.DoClick = function()
|
||||
gui.OpenURL("https://discord.gg/KsE4Xef6vg")
|
||||
end
|
||||
|
||||
SteamButton = vgui.Create("DButton", CMenu)
|
||||
SteamButton:SetSize(RX(150), RY(51))
|
||||
SteamButton:SetPos(RX(207), RY(942))
|
||||
SteamButton:SetText("")
|
||||
HoveredButton(SteamButton, bladw_materials.steam, "Steam", "bladw_text_Medium")
|
||||
SteamButton.DoClick = function()
|
||||
gui.OpenURL("")
|
||||
end
|
||||
|
||||
WebButton = vgui.Create("DButton", CMenu)
|
||||
WebButton:SetSize(RX(150), RY(51))
|
||||
WebButton:SetPos(RX(125), RY(1005))
|
||||
WebButton:SetText("")
|
||||
HoveredButton(WebButton, bladw_materials.website, "Website", "bladw_text_Medium")
|
||||
WebButton.DoClick = function()
|
||||
gui.OpenURL("")
|
||||
for _, btnData in ipairs(linkButtons) do
|
||||
CreateLinkButton(CMenu, btnData.x, btnData.y, btnData.icon, btnData.label, btnData.url)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -232,8 +204,4 @@ hook.Add("PlayerButtonUp", "bladw_CMenu_close", function(ply, key)
|
||||
if key == KEY_C then
|
||||
CloseCMenu()
|
||||
end
|
||||
end)
|
||||
|
||||
hook.Add("OnContextMenuOpen", "bladw_blockC", function()
|
||||
return true
|
||||
end)
|
||||
@@ -1,3 +1,5 @@
|
||||
local RNDX = include("lib/rndx.lua")
|
||||
|
||||
local function RX(x) return x * (ScrW() / 1920) end
|
||||
local function RY(y) return y * (ScrH() / 1080) end
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
-- sv_deathscreen.lua
|
||||
|
||||
util.AddNetworkString("bladw_deathscreen_open")
|
||||
util.AddNetworkString("bladw_deathscreen_close")
|
||||
|
||||
@@ -31,10 +29,11 @@ hook.Add("PlayerDeath", "bladw_ds_open", function(victim, inflictor, attacker)
|
||||
killerName = string.upper(attacker:Nick())
|
||||
end
|
||||
|
||||
-- Adjusts respawn delay based on players in a specified team.
|
||||
local hasResponder = false
|
||||
for _, p in ipairs(player.GetAll()) do
|
||||
local job = p:getDarkRPVar("job")
|
||||
if job == "Citizen" or job == "Citizen" then
|
||||
if job == "Citizen" or job == "Citizen" then
|
||||
hasResponder = true
|
||||
break
|
||||
end
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
local function RX(x) return x * (ScrW() / 1920) end
|
||||
|
||||
local function RY(y) return y * (ScrH() / 1080) end
|
||||
|
||||
function DrawDisc(x, y, iRadius, iProgress, iOffset)
|
||||
|
||||
iX = iX or 0
|
||||
iY = iY or 0
|
||||
iOffset = iOffset or 0
|
||||
iRadius = iRadius or 25
|
||||
iProgress = iProgress or 360
|
||||
|
||||
local tDisc = {
|
||||
{ x = x, y = y }
|
||||
}
|
||||
local tDisc = {
|
||||
{ x = x, y = y }
|
||||
}
|
||||
|
||||
for i = 0, iProgress do
|
||||
local iRad = math.rad(-i + iOffset)
|
||||
@@ -23,7 +21,6 @@ function DrawDisc(x, y, iRadius, iProgress, iOffset)
|
||||
|
||||
draw.NoTexture()
|
||||
surface.DrawPoly(tDisc)
|
||||
|
||||
end
|
||||
|
||||
local function DrawVerticalDisc(x, y, outerR, innerR, progress)
|
||||
@@ -61,12 +58,12 @@ local function DrawVerticalDisc(x, y, outerR, innerR, progress)
|
||||
end
|
||||
|
||||
local materials = {
|
||||
heart = Material("materials/xyoss/hud/heartIcon.png"),
|
||||
hunger = Material("materials/xyoss/hud/hungerIcon.png"),
|
||||
job = Material("materials/xyoss/hud/jobIcon.png"),
|
||||
money = Material("materials/xyoss/hud/moneyIcon.png"),
|
||||
name = Material("materials/xyoss/hud/nameIcon.png"),
|
||||
shield = Material("materials/xyoss/hud/shieldIcon.png"),
|
||||
heart = Material("xyoss/hud/heartIcon.png"),
|
||||
hunger = Material("xyoss/hud/hungerIcon.png"),
|
||||
job = Material("xyoss/hud/jobIcon.png"),
|
||||
money = Material("xyoss/hud/moneyIcon.png"),
|
||||
name = Material("xyoss/hud/nameIcon.png"),
|
||||
shield = Material("xyoss/hud/shieldIcon.png"),
|
||||
}
|
||||
|
||||
local displayMoney = 0
|
||||
@@ -104,8 +101,9 @@ hook.Add("HUDPaint", "bladw_HUD", function()
|
||||
DrawDisc(x + RX(180), y + RY(65), RX(35), 360)
|
||||
|
||||
-- icônes
|
||||
surface.SetDrawColor(255, 255, 255)
|
||||
surface.SetDrawColor(255,255,255,255)
|
||||
surface.SetMaterial(materials.heart)
|
||||
surface.DrawTexturedRect(x - RX(12.5), y + RY(65) - RY(12.5), RX(25), RY(25))
|
||||
surface.DrawTexturedRect(x - RX(12.5), y + RY(50), RX(25), RY(25))
|
||||
surface.SetMaterial(materials.shield)
|
||||
surface.DrawTexturedRect(x + RX(90) - RX(12.5), y + RY(50), RX(25), RY(27))
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
Binary file not shown.
Reference in New Issue
Block a user