Compare commits
36 Commits
d3fae3aff5
..
Warns
| Author | SHA1 | Date | |
|---|---|---|---|
|
e0a5424909
|
|||
|
4e436a05c3
|
|||
|
63e35a534c
|
|||
|
fad2323ab8
|
|||
|
6c95e7ed6f
|
|||
|
9ab34fe802
|
|||
|
c0a3e57a89
|
|||
|
41cc85a9e5
|
|||
|
448baba2c1
|
|||
|
e794c49c84
|
|||
|
f9773bd8ba
|
|||
|
082ff3eedd
|
|||
|
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,378 @@
|
|||||||
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
|
NoCode.config = {
|
||||||
AddCSLuaFile("bladw_hud/client/cl_hud.lua")
|
-- Dossier(s)-module(s) à charger.
|
||||||
AddCSLuaFile("bladw_cMenu/client/cl_cMenu.lua")
|
-- • sans wildcard -> nom exact d'UN dossier : "bladw_announcement"
|
||||||
AddCSLuaFile("bladw_deathscreen/client/cl_interface_ds.lua")
|
-- • avec wildcard -> glob, charge TOUS ceux qui matchent :
|
||||||
|
-- "bladw_*" (préfixe) / "*_menu" (suffixe) / "bladw_*_v2" (milieu)
|
||||||
|
-- • groupes "(a+b)" -> alternatives séparées par + :
|
||||||
|
-- "bladw_(ann*+warn)" = bladw_ann* OU bladw_warn (les alt peuvent avoir des *)
|
||||||
|
-- • "none" (ou vide/nil) -> loader désactivé, rien n'est chargé
|
||||||
|
root = "bladw_(ann*+warn)",
|
||||||
|
-- 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")
|
local C = {
|
||||||
resource.AddSingleFile("resource/fonts/Poppins-Medium.ttf")
|
ok = Color(100, 220, 100),
|
||||||
resource.AddSingleFile("resource/fonts/Poppins-Bold.ttf")
|
err = Color(255, 80, 80),
|
||||||
resource.AddSingleFile("resource/fonts/Poppins-Regular.ttf")
|
info = Color(120, 180, 255),
|
||||||
else
|
warn = Color(255, 200, 50),
|
||||||
include("bladw_hud/client/cl_hud.lua")
|
dim = Color(160, 160, 160),
|
||||||
include("bladw_cMenu/client/cl_cMenu.lua")
|
title = Color(200, 140, 255),
|
||||||
include("bladw_deathscreen/client/cl_interface_ds.lua")
|
}
|
||||||
|
|
||||||
surface.CreateFont("bladw_text", {
|
local function Log(col, msg)
|
||||||
font = "Poppins-SemiBold",
|
MsgC(col, msg .. "\n")
|
||||||
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
|
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
|
||||||
|
|
||||||
|
--- Étend les groupes "(a+b)" en plusieurs motifs plats.
|
||||||
|
-- "bladw_(ann*+warn)" -> { "bladw_ann*", "bladw_warn" }
|
||||||
|
-- Gère plusieurs groupes (produit cartésien) via récursion sur le suffixe.
|
||||||
|
local function ExpandPattern(pattern)
|
||||||
|
local open = pattern:find("(", 1, true)
|
||||||
|
if not open then return { pattern } end
|
||||||
|
|
||||||
|
local close = pattern:find(")", open + 1, true)
|
||||||
|
if not close then return { pattern } end -- parenthèse non fermée -> littéral
|
||||||
|
|
||||||
|
local prefix = pattern:sub(1, open - 1)
|
||||||
|
local group = pattern:sub(open + 1, close - 1)
|
||||||
|
local suffix = pattern:sub(close + 1)
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
for _, alt in ipairs(string.Explode("+", group)) do
|
||||||
|
for _, exp in ipairs(ExpandPattern(prefix .. alt .. suffix)) do
|
||||||
|
out[#out + 1] = exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Construit un testeur (dossier -> bool) pour un motif plat (avec ou sans "*").
|
||||||
|
local function Matcher(p)
|
||||||
|
if p:find("*", 1, true) then
|
||||||
|
local lua_pat = GlobToPattern(p)
|
||||||
|
return function(d) return d:match(lua_pat) ~= nil end
|
||||||
|
end
|
||||||
|
return function(d) return d == p end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FindRootFolders(pattern)
|
||||||
|
local roots = {}
|
||||||
|
|
||||||
|
-- On étend les groupes puis on teste chaque motif : un dossier matche si l'un d'eux matche.
|
||||||
|
local matchers = {}
|
||||||
|
for _, p in ipairs(ExpandPattern(pattern)) do
|
||||||
|
matchers[#matchers + 1] = Matcher(p)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function matches(d)
|
||||||
|
for _, m in ipairs(matchers) do
|
||||||
|
if m(d) then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
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
|
||||||
|
for _, root in ipairs(roots) do
|
||||||
|
if NoCode.config.verbose then
|
||||||
|
Log(C.title, " ▶ " .. root)
|
||||||
|
end
|
||||||
|
LoadFolder(root)
|
||||||
|
end
|
||||||
|
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,372 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
-- couleur des contours extérieurs selon la config ("blue" ou "white")
|
||||||
|
local function OuterCol()
|
||||||
|
return (BladW.Announcement.Conf or {}).announcement_theme == "white" and COL_TEXT or COL_ACCENT
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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, OuterCol(), 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,241 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
-- couleur des contours extérieurs selon la config ("blue" ou "white")
|
||||||
|
local function OuterCol()
|
||||||
|
return (BladW.Announcement.Conf or {}).announcement_theme == "white" and COL_WHITE or COL_ACCENT
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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 un contour fixe (couleur selon le thème config ; le simple a sa jauge sur le bord)
|
||||||
|
if style == "loader" then
|
||||||
|
rndx.DrawOutlined(radius, boxX, boxY, boxW, boxH, OuterCol(), 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,11 @@
|
|||||||
|
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_theme = "white", -- couleur des contours extérieurs : "blue" ou "white"
|
||||||
|
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
|
||||||
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()
|
local function CloseCMenu()
|
||||||
if IsValid(CMenu) then
|
if IsValid(CMenu) then
|
||||||
CMenu:Remove()
|
CMenu:Remove()
|
||||||
@@ -62,6 +86,20 @@ local function CloseCMenu()
|
|||||||
end
|
end
|
||||||
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 function GetStats()
|
||||||
local nbplayer = player.GetCount()
|
local nbplayer = player.GetCount()
|
||||||
local nbpolice = team.NumPlayers(TEAM_POLICE)
|
local nbpolice = team.NumPlayers(TEAM_POLICE)
|
||||||
@@ -70,10 +108,9 @@ local function GetStats()
|
|||||||
return nbplayer, nbpolice, nbpompiers
|
return nbplayer, nbpolice, nbpompiers
|
||||||
end
|
end
|
||||||
|
|
||||||
local nbplayer, nbpolice, nbpompiers = GetStats()
|
|
||||||
|
|
||||||
function OpenCMenu()
|
function OpenCMenu()
|
||||||
if IsValid(CMenu) then return end
|
if IsValid(CMenu) then return end
|
||||||
|
local nbplayer, nbpolice, nbpompiers = GetStats()
|
||||||
|
|
||||||
local ply = LocalPlayer()
|
local ply = LocalPlayer()
|
||||||
|
|
||||||
@@ -93,7 +130,7 @@ function OpenCMenu()
|
|||||||
|
|
||||||
surface.SetFont("bladw_text")
|
surface.SetFont("bladw_text")
|
||||||
local before = "Il y a "
|
local before = "Il y a "
|
||||||
local count = tostring(nbplayer)
|
local count = nbplayer
|
||||||
local after = " joueurs sur le serveur"
|
local after = " joueurs sur le serveur"
|
||||||
|
|
||||||
local beforeW, _ = surface.GetTextSize(before)
|
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)
|
draw.SimpleText(after, "bladw_text", startX + beforeW + countW, RY(900), Color(255, 255, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
end
|
end
|
||||||
|
|
||||||
button3pers = vgui.Create("DButton", CMenu)
|
for _, btnData in ipairs(cMenuButtons) do
|
||||||
button3pers:SetSize(RX(400), RY(122))
|
CreateMenuButton(CMenu, btnData.y, btnData.label, btnData.command)
|
||||||
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")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
PoliceButton = vgui.Create("DPanel", CMenu)
|
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)
|
draw.SimpleText(after, "bladw_text_Medium", startX + countW, h / 2, Color(255, 255, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
end
|
end
|
||||||
|
|
||||||
DiscordButton = vgui.Create("DButton", CMenu)
|
for _, btnData in ipairs(linkButtons) do
|
||||||
DiscordButton:SetSize(RX(150), RY(51))
|
CreateLinkButton(CMenu, btnData.x, btnData.y, btnData.icon, btnData.label, btnData.url)
|
||||||
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("")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -233,7 +205,3 @@ hook.Add("PlayerButtonUp", "bladw_CMenu_close", function(ply, key)
|
|||||||
CloseCMenu()
|
CloseCMenu()
|
||||||
end
|
end
|
||||||
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 RX(x) return x * (ScrW() / 1920) end
|
||||||
local function RY(y) return y * (ScrH() / 1080) 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_open")
|
||||||
util.AddNetworkString("bladw_deathscreen_close")
|
util.AddNetworkString("bladw_deathscreen_close")
|
||||||
|
|
||||||
@@ -31,6 +29,7 @@ hook.Add("PlayerDeath", "bladw_ds_open", function(victim, inflictor, attacker)
|
|||||||
killerName = string.upper(attacker:Nick())
|
killerName = string.upper(attacker:Nick())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Adjusts respawn delay based on players in a specified team.
|
||||||
local hasResponder = false
|
local hasResponder = false
|
||||||
for _, p in ipairs(player.GetAll()) do
|
for _, p in ipairs(player.GetAll()) do
|
||||||
local job = p:getDarkRPVar("job")
|
local job = p:getDarkRPVar("job")
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
local function RX(x) return x * (ScrW() / 1920) end
|
local function RX(x) return x * (ScrW() / 1920) end
|
||||||
|
|
||||||
local function RY(y) return y * (ScrH() / 1080) end
|
local function RY(y) return y * (ScrH() / 1080) end
|
||||||
|
|
||||||
function DrawDisc(x, y, iRadius, iProgress, iOffset)
|
function DrawDisc(x, y, iRadius, iProgress, iOffset)
|
||||||
|
|
||||||
iX = iX or 0
|
iX = iX or 0
|
||||||
iY = iY or 0
|
iY = iY or 0
|
||||||
iOffset = iOffset or 0
|
iOffset = iOffset or 0
|
||||||
iRadius = iRadius or 25
|
iRadius = iRadius or 25
|
||||||
iProgress = iProgress or 360
|
iProgress = iProgress or 360
|
||||||
|
|
||||||
local tDisc = {
|
local tDisc = {
|
||||||
{ x = x, y = y }
|
{ x = x, y = y }
|
||||||
}
|
}
|
||||||
|
|
||||||
for i = 0, iProgress do
|
for i = 0, iProgress do
|
||||||
local iRad = math.rad(-i + iOffset)
|
local iRad = math.rad(-i + iOffset)
|
||||||
@@ -23,7 +21,6 @@ function DrawDisc(x, y, iRadius, iProgress, iOffset)
|
|||||||
|
|
||||||
draw.NoTexture()
|
draw.NoTexture()
|
||||||
surface.DrawPoly(tDisc)
|
surface.DrawPoly(tDisc)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function DrawVerticalDisc(x, y, outerR, innerR, progress)
|
local function DrawVerticalDisc(x, y, outerR, innerR, progress)
|
||||||
@@ -61,12 +58,12 @@ local function DrawVerticalDisc(x, y, outerR, innerR, progress)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local materials = {
|
local materials = {
|
||||||
heart = Material("materials/xyoss/hud/heartIcon.png"),
|
heart = Material("xyoss/hud/heartIcon.png"),
|
||||||
hunger = Material("materials/xyoss/hud/hungerIcon.png"),
|
hunger = Material("xyoss/hud/hungerIcon.png"),
|
||||||
job = Material("materials/xyoss/hud/jobIcon.png"),
|
job = Material("xyoss/hud/jobIcon.png"),
|
||||||
money = Material("materials/xyoss/hud/moneyIcon.png"),
|
money = Material("xyoss/hud/moneyIcon.png"),
|
||||||
name = Material("materials/xyoss/hud/nameIcon.png"),
|
name = Material("xyoss/hud/nameIcon.png"),
|
||||||
shield = Material("materials/xyoss/hud/shieldIcon.png"),
|
shield = Material("xyoss/hud/shieldIcon.png"),
|
||||||
}
|
}
|
||||||
|
|
||||||
local displayMoney = 0
|
local displayMoney = 0
|
||||||
@@ -104,8 +101,9 @@ hook.Add("HUDPaint", "bladw_HUD", function()
|
|||||||
DrawDisc(x + RX(180), y + RY(65), RX(35), 360)
|
DrawDisc(x + RX(180), y + RY(65), RX(35), 360)
|
||||||
|
|
||||||
-- icônes
|
-- icônes
|
||||||
surface.SetDrawColor(255, 255, 255)
|
surface.SetDrawColor(255,255,255,255)
|
||||||
surface.SetMaterial(materials.heart)
|
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.DrawTexturedRect(x - RX(12.5), y + RY(50), RX(25), RY(25))
|
||||||
surface.SetMaterial(materials.shield)
|
surface.SetMaterial(materials.shield)
|
||||||
surface.DrawTexturedRect(x + RX(90) - RX(12.5), y + RY(50), RX(25), RY(27))
|
surface.DrawTexturedRect(x + RX(90) - RX(12.5), y + RY(50), RX(25), RY(27))
|
||||||
|
|||||||
@@ -0,0 +1,529 @@
|
|||||||
|
local rndx = include("lib/rndx.lua")
|
||||||
|
local logo = Material("nocode/announcement/logo.png")
|
||||||
|
|
||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Warn = BladW.Warn or {}
|
||||||
|
BladW.Warn.List = BladW.Warn.List or {}
|
||||||
|
|
||||||
|
-- palette
|
||||||
|
local COL_BG = Color(24, 24, 28)
|
||||||
|
local COL_PANEL = Color(38, 38, 44)
|
||||||
|
local COL_INPUT = Color(30, 30, 35)
|
||||||
|
local COL_LINE = Color(255, 255, 255, 22)
|
||||||
|
local COL_TEXT = Color(236, 236, 240)
|
||||||
|
local COL_MUTED = Color(150, 150, 160)
|
||||||
|
local COL_BLUE = Color(74, 108, 224)
|
||||||
|
local COL_RED = Color(226, 76, 76)
|
||||||
|
local COL_GREEN = Color(45, 170, 96)
|
||||||
|
local COL_ORANGE = Color(232, 168, 32)
|
||||||
|
local COL_SEL = Color(50, 50, 60)
|
||||||
|
local COL_BAR = Color(92, 92, 104) -- petit bout à gauche des joueurs (non sélectionné)
|
||||||
|
local COL_BADGE = Color(22, 22, 26) -- fond du badge "warns actifs" (gris foncé)
|
||||||
|
|
||||||
|
local function Lighten(c, a)
|
||||||
|
a = a or 22
|
||||||
|
return Color(math.min(c.r + a, 255), math.min(c.g + a, 255), math.min(c.b + a, 255), c.a)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- FontAwesome (vrais glyphes)
|
||||||
|
-- Le .ttf doit être chargé côté client : dépose un fa-solid-900.ttf
|
||||||
|
-- dans resource/fonts/. Change FA_FONT si ta police a un autre nom
|
||||||
|
-- de famille, et les codepoints si ta version de FA diffère.
|
||||||
|
-- ============================================================
|
||||||
|
local FA_FONT = "Font Awesome 6 Free Solid" -- nom de famille Windows (name ID 1), pas le typo "Font Awesome 6 Free"
|
||||||
|
local FA = {
|
||||||
|
search = 0xf002, -- magnifying-glass
|
||||||
|
shield = 0xf3ed, -- shield-halved (shield-alt en FA5)
|
||||||
|
card = 0xf2c2, -- id-card
|
||||||
|
copy = 0xf0c5, -- copy
|
||||||
|
trash = 0xf1f8, -- trash
|
||||||
|
}
|
||||||
|
|
||||||
|
local function FAIcon(cp, font, x, y, col, ax, ay)
|
||||||
|
draw.SimpleText(utf8.char(cp), font or "bw_fa", x, y, col, ax or TEXT_ALIGN_CENTER, ay or TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- fonts responsives
|
||||||
|
local FS = 1
|
||||||
|
local function BuildFonts()
|
||||||
|
FS = math.Clamp(ScrH() / 1080, 0.7, 1.5)
|
||||||
|
surface.CreateFont("bw_title", { font = "Poppins-Bold", size = math.Round(24 * FS), weight = 700, antialias = true })
|
||||||
|
surface.CreateFont("bw_big", { font = "Poppins-SemiBold", size = math.Round(19 * FS), weight = 600, antialias = true })
|
||||||
|
surface.CreateFont("bw_med", { font = "Poppins-Medium", size = math.Round(15 * FS), weight = 500, antialias = true })
|
||||||
|
surface.CreateFont("bw_bold", { font = "Poppins-Bold", size = math.Round(15 * FS), weight = 700, antialias = true })
|
||||||
|
surface.CreateFont("bw_small", { font = "Poppins-Regular", size = math.Round(13 * FS), weight = 400, antialias = true })
|
||||||
|
surface.CreateFont("bw_fa", { font = FA_FONT, size = math.Round(15 * FS), weight = 900, extended = true })
|
||||||
|
surface.CreateFont("bw_fa_lg", { font = FA_FONT, size = math.Round(19 * FS), weight = 900, extended = true })
|
||||||
|
end
|
||||||
|
BuildFonts()
|
||||||
|
hook.Add("OnScreenSizeChanged", "bw_warn_fonts", BuildFonts)
|
||||||
|
|
||||||
|
-- temps relatif ("2 mois", "3 j"...)
|
||||||
|
local function RelTime(t)
|
||||||
|
local d = os.time() - (t or 0)
|
||||||
|
if d < 60 then return "à l'instant"
|
||||||
|
elseif d < 3600 then return math.floor(d / 60) .. " min"
|
||||||
|
elseif d < 86400 then return math.floor(d / 3600) .. " h"
|
||||||
|
elseif d < 2592000 then return math.floor(d / 86400) .. " j"
|
||||||
|
elseif d < 31536000 then return math.floor(d / 2592000) .. " mois"
|
||||||
|
else return math.floor(d / 31536000) .. " an(s)" end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function Btn(parent, label, col, font)
|
||||||
|
local b = vgui.Create("DButton", parent)
|
||||||
|
b:SetText("")
|
||||||
|
b.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and Lighten(col) or col, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText(label, font or "bw_med", w * 0.5, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
return b
|
||||||
|
end
|
||||||
|
|
||||||
|
local function StyleEntry(e, ph, multiline)
|
||||||
|
e:SetFont("bw_med")
|
||||||
|
e:SetTextColor(COL_TEXT)
|
||||||
|
e:SetPlaceholderText(ph or "")
|
||||||
|
e:SetPaintBackground(false)
|
||||||
|
if multiline then e:SetMultiline(true) end
|
||||||
|
e.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, COL_INPUT, rndx.SHAPE_FIGMA)
|
||||||
|
self:DrawTextEntryText(COL_TEXT, COL_BLUE, COL_TEXT)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function StyleScroll(sp)
|
||||||
|
local bar = sp:GetVBar()
|
||||||
|
bar:SetWide(8)
|
||||||
|
bar.Paint = function() end
|
||||||
|
bar.btnUp.Paint = function() end
|
||||||
|
bar.btnDown.Paint = function() end
|
||||||
|
bar.btnGrip.Paint = function(self, w, h) rndx.Draw(4, 0, 0, w, h, COL_LINE, rndx.SHAPE_FIGMA) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function GetEntry(sid)
|
||||||
|
for _, e in ipairs(BladW.Warn.List) do
|
||||||
|
if e.sid == sid then return e end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Modal « Appliquer un warn »
|
||||||
|
-- ============================================================
|
||||||
|
local function OpenWarnModal(sid)
|
||||||
|
local reasons = (BladW.Warn.Conf or {}).reasons or {}
|
||||||
|
local chosen, chosenLabel = nil, "Choisissez une raison..."
|
||||||
|
|
||||||
|
local m = vgui.Create("DFrame")
|
||||||
|
m:SetSize(math.min(ScrW() * 0.42, 580), math.min(ScrH() * 0.5, 400))
|
||||||
|
m:Center()
|
||||||
|
m:SetTitle("")
|
||||||
|
m:ShowCloseButton(false)
|
||||||
|
m:MakePopup()
|
||||||
|
m:DockPadding(30, 66, 30, 26)
|
||||||
|
m.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(16, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||||
|
rndx.DrawOutlined(16, 0, 0, w, h, COL_LINE, 2, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText("Appliquer un warn", "bw_title", w * 0.5, 36, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
|
||||||
|
local mclose = vgui.Create("DButton", m)
|
||||||
|
mclose:SetSize(32, 32)
|
||||||
|
mclose:SetText("")
|
||||||
|
mclose.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and COL_RED or COL_BG, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText("X", "bw_med", w * 0.5, h * 0.5, COL_RED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
mclose.DoClick = function() m:Remove() end
|
||||||
|
m.PerformLayout = function(self, w, h) mclose:SetPos(w - 42, 14) end
|
||||||
|
|
||||||
|
-- dropdown des raisons
|
||||||
|
local rdd = vgui.Create("DButton", m)
|
||||||
|
rdd:Dock(TOP)
|
||||||
|
rdd:SetTall(46)
|
||||||
|
rdd:SetText("")
|
||||||
|
rdd.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, COL_INPUT, rndx.SHAPE_FIGMA)
|
||||||
|
rndx.DrawOutlined(8, 0, 0, w, h, COL_LINE, 2, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText(chosenLabel, "bw_med", 14, h * 0.5, chosen and COL_TEXT or COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
draw.NoTexture()
|
||||||
|
surface.SetDrawColor(COL_MUTED)
|
||||||
|
local ax = w - 26
|
||||||
|
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
|
||||||
|
rdd.DoClick = function(self)
|
||||||
|
local menu = DermaMenu()
|
||||||
|
menu:SetMinimumWidth(self:GetWide())
|
||||||
|
menu.Paint = function(s, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, Color(20, 20, 24, 252), rndx.SHAPE_FIGMA)
|
||||||
|
rndx.DrawOutlined(8, 0, 0, w, h, COL_LINE, 1, rndx.SHAPE_FIGMA)
|
||||||
|
end
|
||||||
|
for _, r in ipairs(reasons) do
|
||||||
|
local o = menu:AddOption(r, function() chosen, chosenLabel = r, r 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_BLUE, rndx.SHAPE_FIGMA) end
|
||||||
|
draw.SimpleText(r, "bw_med", 12, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local mx, my = self:LocalToScreen(0, self:GetTall() + 2)
|
||||||
|
menu:Open(mx, my)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- commentaire du staff
|
||||||
|
local clabel = vgui.Create("DLabel", m)
|
||||||
|
clabel:Dock(TOP)
|
||||||
|
clabel:DockMargin(0, 18, 0, 6)
|
||||||
|
clabel:SetTall(20)
|
||||||
|
clabel:SetFont("bw_med")
|
||||||
|
clabel:SetTextColor(COL_TEXT)
|
||||||
|
clabel:SetText("Commentaires du staff :")
|
||||||
|
|
||||||
|
local comment = vgui.Create("DTextEntry", m)
|
||||||
|
comment:Dock(TOP)
|
||||||
|
comment:SetTall(90)
|
||||||
|
StyleEntry(comment, "Joueur a surveiller...", true)
|
||||||
|
|
||||||
|
-- bouton Avertir
|
||||||
|
local warnBtn = Btn(m, "Avertir", COL_GREEN, "bw_big")
|
||||||
|
warnBtn:Dock(BOTTOM)
|
||||||
|
warnBtn:SetTall(46)
|
||||||
|
warnBtn:DockMargin(90, 16, 90, 0)
|
||||||
|
warnBtn.DoClick = function()
|
||||||
|
if not chosen then
|
||||||
|
surface.PlaySound("buttons/button10.wav")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
net.Start("bladw_warn_add")
|
||||||
|
net.WriteString(sid)
|
||||||
|
net.WriteString(chosen)
|
||||||
|
net.WriteString(comment:GetValue() or "")
|
||||||
|
net.SendToServer()
|
||||||
|
m:Remove()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Menu principal
|
||||||
|
-- ============================================================
|
||||||
|
function BladW.Warn.Open()
|
||||||
|
if IsValid(BladW.Warn.Frame) then BladW.Warn.Frame:Remove() end
|
||||||
|
|
||||||
|
local W = math.min(ScrW() * 0.72, 1180)
|
||||||
|
local H = math.min(ScrH() * 0.80, 750)
|
||||||
|
|
||||||
|
local frame = vgui.Create("DFrame")
|
||||||
|
BladW.Warn.Frame = frame
|
||||||
|
frame:SetSize(W, H)
|
||||||
|
frame:Center()
|
||||||
|
frame:SetTitle("")
|
||||||
|
frame:ShowCloseButton(false)
|
||||||
|
frame:MakePopup()
|
||||||
|
frame:DockPadding(16, 16, 16, 16)
|
||||||
|
frame.Paint = function(self, w, h)
|
||||||
|
local lw = w * 0.3 + 24 -- fin de la zone gauche (alignée sur la colonne joueurs)
|
||||||
|
|
||||||
|
-- côté droit : blur + voile (assez sombre mais on voit le fond)
|
||||||
|
rndx.Draw(18, 0, 0, w, h, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
|
||||||
|
rndx.Draw(18, 0, 0, w, h, Color(24, 24, 28, 200), rndx.SHAPE_FIGMA)
|
||||||
|
|
||||||
|
-- côté gauche : couleur pleine (coins arrondis à gauche, carré à droite)
|
||||||
|
rndx.Draw(18, 0, 0, lw, h, COL_BG, rndx.SHAPE_FIGMA + rndx.NO_TR + rndx.NO_BR)
|
||||||
|
|
||||||
|
-- bordure blanche (comme les annonces)
|
||||||
|
rndx.DrawOutlined(18, 0, 0, w, h, Color(255, 255, 255), 3, rndx.SHAPE_FIGMA)
|
||||||
|
end
|
||||||
|
|
||||||
|
local selectedSID
|
||||||
|
|
||||||
|
-- ===== colonne gauche =====
|
||||||
|
local left = vgui.Create("DPanel", frame)
|
||||||
|
left:Dock(LEFT)
|
||||||
|
left:SetWide(W * 0.3)
|
||||||
|
left:DockMargin(0, 0, 16, 0)
|
||||||
|
left:SetPaintBackground(false)
|
||||||
|
|
||||||
|
local lhead = vgui.Create("DPanel", left)
|
||||||
|
lhead:Dock(TOP)
|
||||||
|
lhead:SetTall(46)
|
||||||
|
lhead:SetPaintBackground(false)
|
||||||
|
lhead.Paint = function(self, w, h)
|
||||||
|
rndx.DrawMaterial(0, 0, (h - 42) * 0.5, 42, 42, color_white, logo)
|
||||||
|
draw.SimpleText("Joueurs", "bw_title", 54, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
|
||||||
|
local searchBox = vgui.Create("DPanel", left)
|
||||||
|
searchBox:Dock(TOP)
|
||||||
|
searchBox:DockMargin(0, 8, 0, 10)
|
||||||
|
searchBox:SetTall(40)
|
||||||
|
searchBox:SetPaintBackground(false)
|
||||||
|
searchBox.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, COL_INPUT, rndx.SHAPE_FIGMA)
|
||||||
|
FAIcon(FA.search, "bw_fa", 22, h * 0.5, COL_MUTED)
|
||||||
|
end
|
||||||
|
|
||||||
|
local search = vgui.Create("DTextEntry", searchBox)
|
||||||
|
search:Dock(FILL)
|
||||||
|
search:DockMargin(38, 0, 10, 0)
|
||||||
|
search:SetFont("bw_med")
|
||||||
|
search:SetTextColor(COL_TEXT)
|
||||||
|
search:SetPlaceholderText("Rechercher un joueur")
|
||||||
|
search:SetPaintBackground(false)
|
||||||
|
search.Paint = function(self, w, h)
|
||||||
|
self:DrawTextEntryText(COL_TEXT, COL_BLUE, COL_TEXT)
|
||||||
|
end
|
||||||
|
|
||||||
|
local plist = vgui.Create("DScrollPanel", left)
|
||||||
|
plist:Dock(FILL)
|
||||||
|
StyleScroll(plist)
|
||||||
|
|
||||||
|
-- ===== colonne droite =====
|
||||||
|
local right = vgui.Create("DPanel", frame)
|
||||||
|
right:Dock(FILL)
|
||||||
|
right:SetPaintBackground(false)
|
||||||
|
|
||||||
|
local rhead = vgui.Create("DPanel", right)
|
||||||
|
rhead:Dock(TOP)
|
||||||
|
rhead:SetTall(58)
|
||||||
|
rhead:SetPaintBackground(false)
|
||||||
|
rhead.Paint = function(self, w, h)
|
||||||
|
draw.SimpleText("Historique", "bw_title", w * 0.5, 16, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
FAIcon(FA.card, "bw_fa_lg", w * 0.5, 44, COL_TEXT)
|
||||||
|
end
|
||||||
|
|
||||||
|
local close = vgui.Create("DButton", rhead)
|
||||||
|
close:Dock(RIGHT)
|
||||||
|
close:SetWide(40)
|
||||||
|
close:SetText("")
|
||||||
|
close.Paint = function(self, w, h)
|
||||||
|
local y = (h - 34) * 0.5
|
||||||
|
rndx.Draw(8, w - 34, y, 34, 34, self:IsHovered() and COL_RED or COL_PANEL, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText("X", "bw_big", w - 17, h * 0.5, COL_RED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
close.DoClick = function() frame:Remove() end
|
||||||
|
|
||||||
|
local content = vgui.Create("DPanel", right)
|
||||||
|
content:Dock(FILL)
|
||||||
|
content:DockMargin(0, 8, 0, 0)
|
||||||
|
content:SetPaintBackground(false)
|
||||||
|
|
||||||
|
-- ===== refresh =====
|
||||||
|
local RefreshRight
|
||||||
|
|
||||||
|
local function RefreshPlayers()
|
||||||
|
plist:Clear()
|
||||||
|
local filter = string.lower(search:GetValue() or "")
|
||||||
|
|
||||||
|
for _, e in ipairs(BladW.Warn.List) do
|
||||||
|
if filter == "" or string.find(string.lower(e.name), filter, 1, true) then
|
||||||
|
local entry = e
|
||||||
|
local row = plist:Add("DButton")
|
||||||
|
row:Dock(TOP)
|
||||||
|
row:DockMargin(0, 0, 8, 8)
|
||||||
|
row:SetTall(52)
|
||||||
|
row:SetText("")
|
||||||
|
row.Paint = function(self, w, h)
|
||||||
|
local sel = selectedSID == entry.sid
|
||||||
|
rndx.Draw(8, 0, 0, w, h, sel and COL_SEL or COL_PANEL, rndx.SHAPE_FIGMA)
|
||||||
|
-- petit bout à gauche : bleu si sélectionné, gris clair sinon (coins gauche arrondis)
|
||||||
|
rndx.Draw(8, 0, 0, 5, h, sel and COL_BLUE or COL_BAR, rndx.SHAPE_FIGMA + rndx.NO_TR + rndx.NO_BR)
|
||||||
|
draw.SimpleText(entry.name, "bw_bold", 60, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
|
||||||
|
if entry.staff then
|
||||||
|
FAIcon(FA.shield, "bw_fa", w - 28, h * 0.5, COL_ORANGE)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
row.DoClick = function() selectedSID = entry.sid RefreshRight() end
|
||||||
|
|
||||||
|
local av = vgui.Create("AvatarImage", row)
|
||||||
|
av:SetSize(34, 34)
|
||||||
|
av:SetPos(14, 9)
|
||||||
|
av:SetSteamID(entry.sid, 64)
|
||||||
|
av:SetMouseInputEnabled(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function RefreshRight()
|
||||||
|
content:Clear()
|
||||||
|
|
||||||
|
local e = selectedSID and GetEntry(selectedSID)
|
||||||
|
if not e then
|
||||||
|
local ph = vgui.Create("DPanel", content)
|
||||||
|
ph:Dock(FILL)
|
||||||
|
ph:SetPaintBackground(false)
|
||||||
|
ph.Paint = function(self, w, h)
|
||||||
|
draw.SimpleText("Sélectionne un joueur à gauche", "bw_med", w * 0.5, h * 0.4, COL_MUTED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- carte joueur
|
||||||
|
local card = vgui.Create("DPanel", content)
|
||||||
|
card:Dock(TOP)
|
||||||
|
card:SetTall(90)
|
||||||
|
card:SetPaintBackground(false)
|
||||||
|
card.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(12, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText(e.name, "bw_big", 96, 30, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
draw.SimpleText("Steam ID :", "bw_small", 96, 56, COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
|
||||||
|
local n = #e.warns
|
||||||
|
local txt = n .. (n > 1 and " Warns actifs" or " Warn actif")
|
||||||
|
surface.SetFont("bw_bold")
|
||||||
|
local bw = surface.GetTextSize(txt) + 30
|
||||||
|
rndx.Draw(6, w * 0.5 - bw * 0.5, h * 0.5 - 16, bw, 32, COL_BADGE, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText(txt, "bw_bold", w * 0.5, h * 0.5, COL_RED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
local av = vgui.Create("AvatarImage", card)
|
||||||
|
av:SetSize(56, 56)
|
||||||
|
av:SetPos(22, 17)
|
||||||
|
av:SetSteamID(e.sid, 64)
|
||||||
|
av:SetMouseInputEnabled(false)
|
||||||
|
|
||||||
|
-- bouton copie du SteamID
|
||||||
|
local copyBtn = vgui.Create("DButton", card)
|
||||||
|
copyBtn:SetText("")
|
||||||
|
copyBtn:SetSize(16, 16)
|
||||||
|
copyBtn:SetTooltip("Copier le SteamID")
|
||||||
|
copyBtn.Paint = function(self, w, h)
|
||||||
|
FAIcon(FA.copy, "bw_fa", w * 0.5, h * 0.5, self:IsHovered() and COL_TEXT or COL_MUTED)
|
||||||
|
end
|
||||||
|
copyBtn.DoClick = function() SetClipboardText(e.sid) end
|
||||||
|
card.PerformLayout = function(self, w, h)
|
||||||
|
surface.SetFont("bw_small")
|
||||||
|
local tw = surface.GetTextSize("Steam ID :")
|
||||||
|
copyBtn:SetPos(96 + tw + 8, 49)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ligne « Ajouter un warn »
|
||||||
|
local addRow = vgui.Create("DPanel", content)
|
||||||
|
addRow:Dock(TOP)
|
||||||
|
addRow:SetTall(64)
|
||||||
|
addRow:DockMargin(0, 10, 0, 4)
|
||||||
|
addRow:SetPaintBackground(false)
|
||||||
|
addRow.Paint = function(self, w, h)
|
||||||
|
surface.SetDrawColor(COL_LINE)
|
||||||
|
surface.DrawRect(0, h * 0.5, w * 0.5 - 100, 1)
|
||||||
|
surface.DrawRect(w * 0.5 + 100, h * 0.5, w * 0.5 - 100, 1)
|
||||||
|
end
|
||||||
|
local addBtn = Btn(addRow, "Ajouter un warn", COL_BLUE, "bw_big")
|
||||||
|
addBtn:SetSize(190, 42)
|
||||||
|
addRow.PerformLayout = function(self, w, h) addBtn:SetPos(w * 0.5 - 95, h * 0.5 - 21) end
|
||||||
|
addBtn.DoClick = function() OpenWarnModal(e.sid) end
|
||||||
|
|
||||||
|
-- ligne « Vider le casier » (en bas)
|
||||||
|
local clearRow = vgui.Create("DPanel", content)
|
||||||
|
clearRow:Dock(BOTTOM)
|
||||||
|
clearRow:SetTall(54)
|
||||||
|
clearRow:SetPaintBackground(false)
|
||||||
|
local clearBtn = Btn(clearRow, "Vider le casier", COL_ORANGE, "bw_med")
|
||||||
|
clearBtn:SetSize(190, 40)
|
||||||
|
clearBtn.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and Lighten(COL_ORANGE) or COL_ORANGE, rndx.SHAPE_FIGMA)
|
||||||
|
FAIcon(FA.trash, "bw_fa", 32, h * 0.5, COL_TEXT)
|
||||||
|
draw.SimpleText("Vider le casier", "bw_med", w * 0.5 + 14, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
clearRow.PerformLayout = function(self, w, h) clearBtn:SetPos(w - 190, h - 46) end
|
||||||
|
clearBtn.DoClick = function()
|
||||||
|
if #e.warns == 0 then return end
|
||||||
|
net.Start("bladw_warn_clear")
|
||||||
|
net.WriteString(e.sid)
|
||||||
|
net.SendToServer()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- liste des warns
|
||||||
|
local warns = vgui.Create("DScrollPanel", content)
|
||||||
|
warns:Dock(FILL)
|
||||||
|
warns:DockMargin(0, 6, 0, 6)
|
||||||
|
StyleScroll(warns)
|
||||||
|
|
||||||
|
for i, w in ipairs(e.warns) do
|
||||||
|
local idx, warn = i, w
|
||||||
|
local wc = warns:Add("DPanel")
|
||||||
|
wc:Dock(TOP)
|
||||||
|
wc:DockMargin(0, 0, 8, 10)
|
||||||
|
wc:SetTall(116)
|
||||||
|
wc:SetPaintBackground(false)
|
||||||
|
wc.Paint = function(self, cw, ch)
|
||||||
|
rndx.Draw(10, 0, 0, cw, ch, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText("Warn émis par :", "bw_med", 16, 16, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||||||
|
draw.SimpleText(warn.issuer ~= "" and warn.issuer or "?", "bw_med", 168, 16, COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||||||
|
draw.SimpleText("Date :", "bw_med", 16, 40, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||||||
|
if warn.time and warn.time > 0 then
|
||||||
|
local d = ("(%s) %s"):format(RelTime(warn.time), os.date("%d/%m/%Y, %I:%M %p", warn.time))
|
||||||
|
draw.SimpleText(d, "bw_med", 168, 40, COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||||||
|
end
|
||||||
|
draw.SimpleText("Raison :", "bw_med", 16, 64, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||||||
|
rndx.Draw(6, 14, ch - 40, cw - 28, 30, COL_INPUT, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText(warn.reason or "", "bw_small", 26, ch - 25, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
|
||||||
|
local del = Btn(wc, "Supprimer", COL_RED, "bw_med")
|
||||||
|
del:SetSize(150, 36)
|
||||||
|
wc.PerformLayout = function(self, cw, ch) del:SetPos(cw - 164, 14) end
|
||||||
|
del.DoClick = function()
|
||||||
|
net.Start("bladw_warn_remove")
|
||||||
|
net.WriteString(e.sid)
|
||||||
|
net.WriteUInt(idx, 8)
|
||||||
|
net.SendToServer()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
frame.RefreshAll = function()
|
||||||
|
RefreshPlayers()
|
||||||
|
RefreshRight()
|
||||||
|
end
|
||||||
|
|
||||||
|
search.OnChange = function() RefreshPlayers() end
|
||||||
|
|
||||||
|
RefreshPlayers()
|
||||||
|
RefreshRight()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Réseau
|
||||||
|
-- ============================================================
|
||||||
|
net.Receive("bladw_warn_open", function()
|
||||||
|
BladW.Warn.Open()
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- annonce d'un warn dans le chat de base
|
||||||
|
net.Receive("bladw_warn_chat", function()
|
||||||
|
local target = net.ReadString()
|
||||||
|
local admin = net.ReadString()
|
||||||
|
local reason = net.ReadString()
|
||||||
|
chat.AddText(
|
||||||
|
COL_ORANGE, "[Warn] ",
|
||||||
|
COL_TEXT, target,
|
||||||
|
COL_MUTED, " a été averti par ",
|
||||||
|
COL_TEXT, admin,
|
||||||
|
COL_MUTED, " — ",
|
||||||
|
COL_RED, reason
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
net.Receive("bladw_warn_sync", function()
|
||||||
|
local list = {}
|
||||||
|
local n = net.ReadUInt(8)
|
||||||
|
for i = 1, n do
|
||||||
|
local e = { name = net.ReadString(), sid = net.ReadString(), staff = net.ReadBool(), warns = {} }
|
||||||
|
local wn = net.ReadUInt(8)
|
||||||
|
for j = 1, wn do
|
||||||
|
e.warns[j] = {
|
||||||
|
issuer = net.ReadString(),
|
||||||
|
time = net.ReadUInt(32),
|
||||||
|
reason = net.ReadString(),
|
||||||
|
comment = net.ReadString(),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
list[i] = e
|
||||||
|
end
|
||||||
|
BladW.Warn.List = list
|
||||||
|
|
||||||
|
if IsValid(BladW.Warn.Frame) and BladW.Warn.Frame.RefreshAll then
|
||||||
|
BladW.Warn.Frame.RefreshAll()
|
||||||
|
end
|
||||||
|
end)
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Warn = BladW.Warn or {}
|
||||||
|
|
||||||
|
-- envoie au client la liste des joueurs connectés + leurs warns
|
||||||
|
local function SyncTo(ply)
|
||||||
|
if not IsValid(ply) then return end
|
||||||
|
|
||||||
|
local players = player.GetAll()
|
||||||
|
|
||||||
|
net.Start("bladw_warn_sync")
|
||||||
|
net.WriteUInt(#players, 8)
|
||||||
|
for _, p in ipairs(players) do
|
||||||
|
local sid = p:SteamID64() or ""
|
||||||
|
local warns = BladW.Warn.Get(sid)
|
||||||
|
local isStaff = p:IsAdmin() or table.HasValue((BladW.Warn.Conf or {}).staff_groups or {}, p:GetUserGroup())
|
||||||
|
|
||||||
|
net.WriteString(p:Nick())
|
||||||
|
net.WriteString(sid)
|
||||||
|
net.WriteBool(isStaff)
|
||||||
|
net.WriteUInt(math.min(#warns, 255), 8)
|
||||||
|
for i = 1, math.min(#warns, 255) do
|
||||||
|
local w = warns[i]
|
||||||
|
net.WriteString(w.issuer or "")
|
||||||
|
net.WriteUInt(w.time or 0, 32)
|
||||||
|
net.WriteString(w.reason or "")
|
||||||
|
net.WriteString(w.comment or "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
net.Send(ply)
|
||||||
|
end
|
||||||
|
BladW.Warn.SyncTo = SyncTo
|
||||||
|
|
||||||
|
-- ouvre le menu pour un joueur autorisé
|
||||||
|
local function OpenFor(ply)
|
||||||
|
if not IsValid(ply) or not BladW.Warn.HasAccess(ply) then return end
|
||||||
|
net.Start("bladw_warn_open")
|
||||||
|
net.Send(ply)
|
||||||
|
SyncTo(ply)
|
||||||
|
end
|
||||||
|
|
||||||
|
concommand.Add("bladw_warns", function(ply) OpenFor(ply) end)
|
||||||
|
|
||||||
|
hook.Add("PlayerSay", "BladW:WarnChatCmd", function(ply, text)
|
||||||
|
local cmd = string.lower(string.Trim(text))
|
||||||
|
if cmd ~= "/warns" and cmd ~= "!warns" and cmd ~= "/warn" then return end
|
||||||
|
OpenFor(ply)
|
||||||
|
return "" -- on retire la commande du chat
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- un admin ajoute un warn
|
||||||
|
net.Receive("bladw_warn_add", function(len, ply)
|
||||||
|
if not IsValid(ply) or not BladW.Warn.HasAccess(ply) then return end
|
||||||
|
|
||||||
|
local sid = net.ReadString()
|
||||||
|
local reason = string.sub(net.ReadString(), 1, 100)
|
||||||
|
local comment = string.sub(net.ReadString(), 1, 300)
|
||||||
|
if sid == "" or reason == "" then return end
|
||||||
|
|
||||||
|
BladW.Warn.AddWarn(sid, {
|
||||||
|
issuer = ply:Nick(),
|
||||||
|
issuerid = ply:SteamID64(),
|
||||||
|
time = os.time(),
|
||||||
|
reason = reason,
|
||||||
|
comment = comment,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- annonce le warn dans le chat de base
|
||||||
|
local Conf = BladW.Warn.Conf or {}
|
||||||
|
if Conf.chat_announce ~= false then
|
||||||
|
local target = player.GetBySteamID64(sid)
|
||||||
|
local targetName = IsValid(target) and target:Nick() or sid
|
||||||
|
|
||||||
|
local recips
|
||||||
|
local scope = Conf.chat_scope or "all"
|
||||||
|
if scope == "target" then
|
||||||
|
recips = IsValid(target) and { target } or {}
|
||||||
|
elseif scope == "staff" then
|
||||||
|
recips = {}
|
||||||
|
for _, p in ipairs(player.GetAll()) do
|
||||||
|
if BladW.Warn.HasAccess(p) then recips[#recips + 1] = p end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- recips == nil -> tout le monde ; sinon on n'envoie que si la liste n'est pas vide
|
||||||
|
if not recips or #recips > 0 then
|
||||||
|
net.Start("bladw_warn_chat")
|
||||||
|
net.WriteString(targetName)
|
||||||
|
net.WriteString(ply:Nick())
|
||||||
|
net.WriteString(reason)
|
||||||
|
if recips then net.Send(recips) else net.Broadcast() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
SyncTo(ply)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- un admin supprime un warn précis
|
||||||
|
net.Receive("bladw_warn_remove", function(len, ply)
|
||||||
|
if not IsValid(ply) or not BladW.Warn.HasAccess(ply) then return end
|
||||||
|
|
||||||
|
local sid = net.ReadString()
|
||||||
|
local index = net.ReadUInt(8)
|
||||||
|
BladW.Warn.RemoveWarn(sid, index)
|
||||||
|
|
||||||
|
SyncTo(ply)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- un admin vide le casier d'un joueur
|
||||||
|
net.Receive("bladw_warn_clear", function(len, ply)
|
||||||
|
if not IsValid(ply) or not BladW.Warn.HasAccess(ply) then return end
|
||||||
|
|
||||||
|
local sid = net.ReadString()
|
||||||
|
BladW.Warn.ClearWarns(sid)
|
||||||
|
|
||||||
|
SyncTo(ply)
|
||||||
|
end)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
util.AddNetworkString("bladw_warn_open") -- ouvre le menu chez le client
|
||||||
|
util.AddNetworkString("bladw_warn_sync") -- serveur -> client : joueurs + leurs warns
|
||||||
|
util.AddNetworkString("bladw_warn_add") -- client -> serveur : ajouter un warn
|
||||||
|
util.AddNetworkString("bladw_warn_remove") -- client -> serveur : supprimer un warn
|
||||||
|
util.AddNetworkString("bladw_warn_clear") -- client -> serveur : vider le casier
|
||||||
|
util.AddNetworkString("bladw_warn_chat") -- serveur -> clients : annonce du warn dans le chat
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Warn = BladW.Warn or {}
|
||||||
|
|
||||||
|
-- le droit géré par l'admin mod (SAM / ULX / FAdmin)
|
||||||
|
local PRIV = "bladw_warn"
|
||||||
|
BladW.Warn.Privilege = PRIV
|
||||||
|
|
||||||
|
-- enregistre le droit dans l'admin mod présent
|
||||||
|
local function Register()
|
||||||
|
if sam and sam.permissions and sam.permissions.add then
|
||||||
|
pcall(sam.permissions.add, PRIV, "BladW Warn", "admin")
|
||||||
|
end
|
||||||
|
if ULib and ULib.ucl and ULib.ucl.registerAccess then
|
||||||
|
pcall(ULib.ucl.registerAccess, PRIV, "admin", "Gérer les warns BladW", "BladW")
|
||||||
|
end
|
||||||
|
if FAdmin and FAdmin.Access and FAdmin.Access.AddPrivilege then
|
||||||
|
pcall(FAdmin.Access.AddPrivilege, PRIV, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
hook.Add("SAM.LoadPermissions", "BladW:WarnPriv", Register)
|
||||||
|
hook.Add("InitPostEntity", "BladW:WarnPriv", Register)
|
||||||
|
Register()
|
||||||
|
|
||||||
|
-- ce joueur peut-il gérer les warns ?
|
||||||
|
function BladW.Warn.HasAccess(ply)
|
||||||
|
if not IsValid(ply) then return false end
|
||||||
|
|
||||||
|
if sam and sam.player and sam.player.has_permission then
|
||||||
|
return sam.player.has_permission(ply, PRIV)
|
||||||
|
end
|
||||||
|
if ULib and ULib.ucl and ULib.ucl.query then
|
||||||
|
return ULib.ucl.query(ply, PRIV)
|
||||||
|
end
|
||||||
|
if FAdmin and FAdmin.Access and FAdmin.Access.PlayerHasPrivilege then
|
||||||
|
return FAdmin.Access.PlayerHasPrivilege(ply, PRIV)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ply:IsAdmin()
|
||||||
|
end
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Warn = BladW.Warn or {}
|
||||||
|
|
||||||
|
-- stockage des warns : { [steamid64] = { { issuer, issuerid, time, reason, comment }, ... } }
|
||||||
|
local DIR = "bladw_warn"
|
||||||
|
local FILE = "bladw_warn/warns.json"
|
||||||
|
|
||||||
|
BladW.Warn.Data = BladW.Warn.Data or {}
|
||||||
|
|
||||||
|
function BladW.Warn.Save()
|
||||||
|
file.CreateDir(DIR)
|
||||||
|
file.Write(FILE, util.TableToJSON(BladW.Warn.Data))
|
||||||
|
end
|
||||||
|
|
||||||
|
function BladW.Warn.Load()
|
||||||
|
local raw = file.Read(FILE, "DATA")
|
||||||
|
BladW.Warn.Data = (raw and util.JSONToTable(raw)) or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function BladW.Warn.Get(sid)
|
||||||
|
return BladW.Warn.Data[sid] or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function BladW.Warn.AddWarn(sid, entry)
|
||||||
|
if not sid or sid == "" then return end
|
||||||
|
BladW.Warn.Data[sid] = BladW.Warn.Data[sid] or {}
|
||||||
|
table.insert(BladW.Warn.Data[sid], entry)
|
||||||
|
BladW.Warn.Save()
|
||||||
|
end
|
||||||
|
|
||||||
|
function BladW.Warn.RemoveWarn(sid, index)
|
||||||
|
local t = BladW.Warn.Data[sid]
|
||||||
|
if t and t[index] then
|
||||||
|
table.remove(t, index)
|
||||||
|
if #t == 0 then BladW.Warn.Data[sid] = nil end
|
||||||
|
BladW.Warn.Save()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function BladW.Warn.ClearWarns(sid)
|
||||||
|
BladW.Warn.Data[sid] = nil
|
||||||
|
BladW.Warn.Save()
|
||||||
|
end
|
||||||
|
|
||||||
|
BladW.Warn.Load()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Warn = BladW.Warn or {}
|
||||||
|
BladW.Warn.Conf = BladW.Warn.Conf or {}
|
||||||
|
|
||||||
|
-- config des warns
|
||||||
|
BladW.Warn.Conf = {
|
||||||
|
-- les raisons proposées dans le menu déroulant du modal
|
||||||
|
reasons = {
|
||||||
|
"RDM",
|
||||||
|
"VDM",
|
||||||
|
"No Fear RP",
|
||||||
|
"Fail RP",
|
||||||
|
"Troll",
|
||||||
|
"Metagaming",
|
||||||
|
"Powergaming",
|
||||||
|
"Propos déplacés / insultes",
|
||||||
|
"Non-respect du staff",
|
||||||
|
"Publicité",
|
||||||
|
"Bug abuse",
|
||||||
|
},
|
||||||
|
|
||||||
|
-- annonce le warn dans le chat de base
|
||||||
|
chat_announce = true,
|
||||||
|
-- à qui : "all" (tout le monde) | "target" (juste l'averti) | "staff" (les admins)
|
||||||
|
chat_scope = "target",
|
||||||
|
|
||||||
|
-- groupes considérés comme "staff" (affiche le bouclier). IsAdmin() est toujours staff.
|
||||||
|
staff_groups = { "superadmin", "admin", "moderator", "modo", "helper", "support" },
|
||||||
|
}
|
||||||
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.
Binary file not shown.
Reference in New Issue
Block a user