36 Commits

Author SHA1 Message Date
nocode e0a5424909 feat(bladw_warn): add warning system configuration
Create initial configuration file for the warning system with:
- Predefined warning reasons (RDM, VDM, RP violations, etc.)
- Chat announcement settings (enabled by default, scoped to target)
- Staff group definitions for permission levels
2026-07-20 23:29:53 +02:00
nocode 4e436a05c3 feat(storage): add warn data persistence layer
Implement server-side storage module for managing player warnings.

Provides functionality to:
- Load and save warnings from/to JSON file
- Retrieve warnings for a specific player
- Add new warnings with issuer information
- Remove individual warnings by index
- Clear all warnings for a player

Warnings are stored with issuer, issuerid, timestamp, reason, and comment.
2026-07-20 23:29:48 +02:00
nocode 63e35a534c feat(privileges): add privilege management for admin mods
Implement privilege registration and access checking for SAM, ULX, and FAdmin admin mods.

Provides:
- Registration of bladw_warn privilege with supported admin mods
- BladW.Warn.HasAccess() function to check player permissions
- Fallback to IsAdmin() if no admin mod is detected
2026-07-20 23:29:44 +02:00
nocode fad2323ab8 feat(networkstring): add network strings for warn system communication
Initialize network strings for bladw_warn system to enable communication between client and server for opening menu, syncing player warnings, adding/removing warnings, clearing records, and announcing warnings in chat.
2026-07-20 23:29:39 +02:00
nocode 6c95e7ed6f feat(bladw_warn): add server-side warning system functions
Implement core server-side functionality for the warning system including:
- SyncTo: synchronizes connected players and their warnings to a client
- OpenFor: opens the warning menu for authorized players
- Chat commands: /warns, !warns, /warn to open the menu
- Net receivers for adding, removing, and clearing warnings
- Chat announcement system for new warnings with configurable scope (all, target, staff)
- Access control checks on all admin operations
2026-07-20 23:29:35 +02:00
nocode 9ab34fe802 feat(client): add main warning menu interface with player list and history
Implement comprehensive client-side menu for the warning system including:
- Responsive player list with search functionality
- Warning history display with details
- Modal dialog for applying warnings with reason selection
- Custom styling with color palette and rounded corners
- Font Awesome icon integration
- Dynamic font scaling based on screen resolution
- Network communication for warning actions
2026-07-20 23:29:27 +02:00
nocode c0a3e57a89 build(fonts): add fontawesome solid 900 font file 2026-07-20 23:29:20 +02:00
nocode 41cc85a9e5 feat(bladWinRpMenu_loader): add support for pattern groups with alternatives syntax
Implement pattern group expansion allowing alternatives separated by '+' within parentheses.

Examples:
- "bladw_(ann*+warn)" expands to ["bladw_ann*", "bladw_warn"]
- Supports multiple groups with recursive Cartesian product expansion
- Refactor matching logic into separate Matcher() function for clarity
- Update root pattern in config from "bladw_announcement*" to "bladw_(ann*+warn)"
2026-07-20 23:29:10 +02:00
nocode 448baba2c1 feat(announcement): add announcement_theme configuration option
Add new configuration option to control the announcement border color theme.

Supports two theme options:
- "blue": blue border color
- "white": white border color (default)

This allows customization of the announcement appearance without code modification.
2026-07-20 18:23:53 +02:00
nocode e794c49c84 feat(announcement_menu): add theme-aware outer border color for loader style
introduce OuterCol() function to dynamically determine the outer border color based on the announcement theme configuration (white or blue). update the loader style border to use this function instead of hardcoded white color, allowing theme consistency across the announcement menu.
2026-07-20 18:23:49 +02:00
nocode f9773bd8ba feat(admin_menu): add theme-based outer border color configuration
Introduce OuterCol() function to dynamically set the outer border color based on the announcement_theme configuration. The border will be white when theme is set to "white", otherwise it defaults to the accent color. This allows for better visual customization of the admin menu appearance.
2026-07-20 18:23:44 +02:00
nocode 082ff3eedd chore(loader): enable bladw_announcement module loading
Change root configuration from "none" (disabled) to "bladw_announcement*" to enable automatic loading of announcement modules.
2026-07-20 18:23:30 +02:00
nocode dc2835b11b refactor(loader): refactor font creation and add root pattern validation
- Refactor font creation into a loop-based approach to reduce code duplication
- Add validation for root pattern with support for "none" value to disable the loader
- Store root pattern in local variable for consistent usage throughout the module
- Add early return when loader is disabled to prevent unnecessary processing
- Improve code organization with clearer section comments
- Change default root from "bladw_announcement" to "none" (disabled state)
2026-07-20 17:17:57 +02:00
nocode cf747dd454 docs(announcement): add documentation for target option in schedule
Add documentation explaining the optional 'target' parameter that allows restricting announcements to specific usergroups like 'vip', with all users receiving the announcement by default if not specified.
2026-07-20 15:41:38 +02:00
nocode 2b56b15886 feat(announcement): add target field to announcement presets
Add 'target' field to all announcement presets to specify which users receive the announcement. The target can be 'all' for everyone or a specific usergroup like 'vip' or 'admin'.

This allows announcements to be selectively sent to different user groups.
2026-07-20 15:41:33 +02:00
nocode 6f579fb2db feat(announcement): add usergroup targeting and recipient filtering to broadcasts
Add support for targeting announcements to specific usergroups or individual players.

- Add 'target' field to store a usergroup to filter recipients
- Implement recipient filtering logic that prioritizes explicit player lists over usergroup lists over preset target
- Update net.Broadcast() to net.Send() when specific recipients are selected
- Parse scope parameter from client to determine targeting type (all/group/players)
- Return early if no valid recipients are found after filtering
2026-07-20 15:41:28 +02:00
nocode 490d5a2194 feat(admin_menu): add recipient selection dropdown to announcement admin panel
Add a new dropdown menu for selecting announcement recipients with three options:
- "Tout le monde" (Everyone): broadcasts to all players
- "Groupe" (Group): allows selecting specific user groups
- "Joueurs" (Players): allows selecting individual players

Implement an animated scrollable list panel that displays checkboxes for group or player selection based on the chosen scope. The panel height animates smoothly between collapsed and expanded states.

Extract the frequently used outline color (255, 255, 255, 60) into a COL_LINE constant. Add a DrawArrow helper function to render dropdown indicators. Update the send button validation logic to ensure at least one recipient is selected before sending.

The panel remains open after sending so the admin can keep their text and reuse it if needed.
2026-07-20 15:41:03 +02:00
nocode 5f35c70286 feat(sv_networkstring): add network string for announcement result feedback
Add bladWAnnouncementResult network string to communicate announcement submission results back to the admin.
2026-07-20 14:23:17 +02:00
nocode 5fafde5e31 feat(privileges): add admin permission management for announcement system
Register BladW announcement privilege with supported admin mods (SAM, ULX/ULib, FAdmin) and provide function to check player access rights. Supports multiple admin systems with fallback to base admin status.
2026-07-20 14:23:12 +02:00
nocode e357313204 feat(announcement): add access control and announcement queueing
- Add BladW.Announcement.HasAccess() permission check to menu commands and net receive
- Implement announcement queueing: prevent stacking announcements while one is already displaying
- Track active announcement duration with activeUntil timer
- Add feedback to admin: send result status and remaining queue time after broadcast attempt
- Improve chat command handling: hide command from chat for all users, only open menu if authorized
- Refactor configuration and duration retrieval for clarity
2026-07-20 14:23:08 +02:00
nocode 05864e1ed4 feat(admin_menu): add server feedback handling for announcement submissions
Implement bidirectional communication for announcement submissions:
- Display server feedback messages (e.g., announcement already in progress) in the admin menu
- Keep the menu open after sending to allow users to retry if submission fails
- Add net.Receive handler to process server responses
- Show countdown timer when announcement is already in progress
- Close menu only on successful publication, not on rejection

This improves UX by giving admins visibility into why their submission failed and allowing them to correct and resubmit without losing their input.
2026-07-20 14:22:50 +02:00
nocode e40893a290 feat(sound): add announcement audio file 2026-07-20 13:49:54 +02:00
nocode b0ab2e3e31 chore(materials): add logo image for nocode announcement 2026-07-20 13:49:50 +02:00
nocode 49a6a085af feat(announcement): add schedule configuration for automated announcements
Create a new schedule configuration file that allows defining automated announcements with flexible scheduling options.

Supports two scheduling methods:
- Interval-based: every X seconds/minutes/hours/days
- Cron-based: standard 5-field cron syntax for precise scheduling

Announcements can use presets (discord, restart, event) or custom title/text combinations.
Includes comprehensive documentation and commented examples.
2026-07-20 13:49:45 +02:00
nocode 00eb696e7f feat(announcement): add preset announcements for reuse
Add a new sh_presets.lua file containing reusable announcement presets including restart, shutdown, maintenance, event, and discord announcements. Each preset defines title, text, style, and duration properties that can be used by the menu and scheduler.
2026-07-20 13:49:39 +02:00
nocode 2c5a73cf49 feat(announcement): add shared configuration for announcement system
Create initial configuration file for the announcement system with settings for style, display duration, maximum message length, and sound effects.
2026-07-20 13:49:34 +02:00
nocode 8695419876 feat(scheduler): add announcement scheduler with interval and cron support
Implement a scheduler system for BladW announcements that supports both:
- Interval-based scheduling (e.g., "30m", "90s", "2h", "1d")
- Cron-based scheduling with standard 5-field format (minute hour day month weekday)

The scheduler parses configuration from BladW.Announcement.Schedules and manages timers automatically, cleaning up old timers on reload. Cron expressions are validated and compiled into matcher functions for efficient execution checking every 20 seconds.
2026-07-20 13:49:27 +02:00
nocode fd5a9703bc feat(announcement): add network string definitions for announcement system
Add network string registration for the announcement module:
- bladWOpenAnnouncementMenu: opens announcement menu on client
- bladWSendAnnouncement: receives announcement from admin
- bladWShowAnnouncement: broadcasts announcement to all players
2026-07-20 13:48:37 +02:00
nocode 7248b8c434 feat(announcement): add server-side announcement functions and handlers
Implement server-side announcement system with the following features:
- LimitChars() utility function to safely truncate strings with UTF-8 support
- bladw_amenu concommand to open announcement menu for players
- /bannonce and !bannonce chat commands as aliases for the menu
- Resolve() function to merge announcement presets with custom entries
- Broadcast() function to send announcements to all clients with proper length limits
- Net message receiver for admin announcement submissions
- Resource file declaration for sound asset download
2026-07-20 13:48:26 +02:00
nocode 904cf3566c feat(announcement): add client-side announcement menu system
Implement a responsive announcement display system with support for multiple styles (loader with progress bar and border timer). Features include:
- Dynamic font scaling based on screen resolution
- Smooth intro/outro animations with easing
- Text wrapping for multi-line messages
- Customizable styling and duration
- Border timer visualization for remaining time
- Network message handling for server-sent announcements
2026-07-20 13:48:15 +02:00
nocode b220f41c4a feat(admin_menu): add network receiver for opening announcement menu
Implement net.Receive handler to open the announcement admin menu when triggered by the server. This allows server-side code to remotely open the admin menu on client instances.
2026-07-20 13:48:10 +02:00
nocode b183517e9f feat(admin_menu): add announcement admin menu interface
Implement a custom GUI menu for administrators to send announcements with the following features:
- Support for announcement presets that auto-fill title and message fields
- Real-time character count validation with visual feedback
- Warning indicator when message exceeds max length limit
- Custom styled text input fields with placeholder text
- Rounded corners and custom color scheme matching announcement styling
- Close button and ESC key handling
- Network message handling to send announcements to server
2026-07-20 13:48:05 +02:00
nocode ba9e71e6ae refactor(rndx): reformat and restructure shader library code
Reorganize and reformat the rndx shader library for improved code structure and maintainability. The changes maintain all functionality while improving code organization and readability through consistent formatting and structural improvements.
2026-07-20 13:47:50 +02:00
nocode f6ec6b1236 refactor(loader): replace manual loader with generic recursive autoloader system
Replace the hardcoded module loader with a flexible, recursive autoloader (NoCode) that:

- Supports glob patterns (wildcards) for module discovery
- Recursively scans folders without depth limits
- Properly handles shared (sh_), server (sv_), and client (cl_) file prefixes
- Implements safe loading with error handling and statistics
- Searches both LUA and GAME paths simultaneously
- Sends library files to clients without auto-including them
- Adds comprehensive logging with color-coded output
- Excludes configured files/folders (template.lua, _dev, disabled)

The new system is more maintainable and allows adding new modules without modifying the loader.
2026-07-20 13:47:39 +02:00
xyoss 75428ef21d ajout de commentaire sur le code 2026-06-27 08:18:07 +02:00
xyoss 875954ac63 ajout de la lib rndx 2026-06-24 10:42:14 +02:00
25 changed files with 2992 additions and 141 deletions
+372 -49
View File
@@ -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),
warn = Color(255, 200, 50),
dim = Color(160, 160, 160),
title = Color(200, 140, 255),
}
local function Log(col, msg)
MsgC(col, msg .. "\n")
end
-- ============================================================
-- Utilitaires
-- ============================================================
local function StartsWith(str, prefix)
return str:sub(1, #prefix) == prefix
end
local function IsExcluded(name)
for _, ex in ipairs(NoCode.config.exclude) do
if name == ex then return true end
end
return false
end
local function SortFiles(files)
table.sort(files, function(a, b)
local function rank(n)
if StartsWith(n, "sh_") then return 1 end
if StartsWith(n, "sv_") then return 2 end
if StartsWith(n, "cl_") then return 3 end
return 4
end
return rank(a) < rank(b)
end)
end
--- Fusionne deux tables en évitant les doublons
local function Merge(t1, t2)
local seen = {}
local result = {}
for _, v in ipairs(t1 or {}) do
if not seen[v] then seen[v] = true; table.insert(result, v) end
end
for _, v in ipairs(t2 or {}) do
if not seen[v] then seen[v] = true; table.insert(result, v) end
end
return result
end
-- ============================================================
-- Compteurs
-- ============================================================
local stats = { loaded = 0, sent = 0, errors = 0 }
-- ============================================================
-- Chargement d'un fichier unique
-- ============================================================
local function LoadFile(path)
local filename = path:match("([^/]+)$")
local realm, shouldLoad, shouldSend
if StartsWith(filename, "sh_") then
realm = "SHARED"
shouldSend = SERVER
shouldLoad = true
elseif StartsWith(filename, "sv_") then
realm = "SERVER"
shouldLoad = SERVER
elseif StartsWith(filename, "cl_") then
realm = "CLIENT"
shouldSend = SERVER
shouldLoad = not SERVER
else
return
end
if shouldSend then
AddCSLuaFile(path)
stats.sent = stats.sent + 1
if NoCode.config.verbose and not shouldLoad then
Log(C.info, (" [→] [%s] %s"):format(realm, path))
end
end
if not shouldLoad then return end
if NoCode.config.safe_load then
local ok, err = pcall(include, path)
if ok then
stats.loaded = stats.loaded + 1
if NoCode.config.verbose then
Log(C.ok, (" [✓] [%s] %s"):format(realm, path))
end
else
stats.errors = stats.errors + 1
Log(C.err, (" [✗] [%s] %s"):format(realm, path))
Log(C.err, (" ↳ %s"):format(tostring(err)))
end
else
include(path)
stats.loaded = stats.loaded + 1
if NoCode.config.verbose then
Log(C.ok, (" [✓] [%s] %s"):format(realm, path))
end
end
end
-- ============================================================
-- Scan récursif — cherche TOUJOURS dans LUA et GAME en même temps
-- ============================================================
local function LoadFolder(dir, depth)
depth = depth or 0
local basename = dir:match("([^/]+)$") or dir
if IsExcluded(basename) then
Log(C.warn, (" [~] Dossier ignoré : %s"):format(dir))
return
end
-- On cherche dans les deux paths et on fusionne les résultats
local files1, dirs1 = file.Find(dir .. "/*", "LUA")
local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")
local files = Merge(files1, files2)
local dirs = Merge(dirs1, dirs2)
if #files == 0 and #dirs == 0 then
if NoCode.config.verbose and depth > 0 then
Log(C.warn, (" [!] Dossier vide ou introuvable : %s"):format(dir))
end
return
end
if NoCode.config.verbose and depth > 0 then
Log(C.dim, string.rep(" ", depth) .. "📂 " .. basename .. "/")
end
SortFiles(files)
for _, fname in ipairs(files) do
if fname:EndsWith(".lua") and not IsExcluded(fname) then
LoadFile(dir .. "/" .. fname)
end
end
-- Récursion sur tous les sous-dossiers sans limite de profondeur
for _, subdir in ipairs(dirs) do
if not IsExcluded(subdir) then
LoadFolder(dir .. "/" .. subdir, depth + 1)
end
end
end
-- ============================================================
-- Envoi récursif d'une bibliothèque au client (sans l'inclure)
-- Les fichiers de lib/ ne suivent pas le préfixe sh_/sv_/cl_ ;
-- on les expédie simplement pour que include("lib/...") fonctionne.
-- ============================================================
local function SendFolder(dir)
if not SERVER then return end
local files1, dirs1 = file.Find(dir .. "/*", "LUA")
local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")
local files = Merge(files1, files2)
local dirs = Merge(dirs1, dirs2)
for _, fname in ipairs(files) do
if fname:EndsWith(".lua") and not IsExcluded(fname) then
local path = dir .. "/" .. fname
AddCSLuaFile(path)
stats.sent = stats.sent + 1
if NoCode.config.verbose then
Log(C.info, (" [→] [LIB] %s"):format(path))
end
end
end
for _, subdir in ipairs(dirs) do
if not IsExcluded(subdir) then
SendFolder(dir .. "/" .. subdir)
end
end
end
-- ============================================================
-- Découverte des dossiers-modules
-- (file.Find ne supporte le wildcard que sur le dernier segment,
-- donc on liste la racine lua/ puis on filtre nous-mêmes.)
-- • pattern sans "*" -> match exact (un seul dossier)
-- • pattern avec "*" -> glob converti en motif Lua (préfixe/suffixe/milieu)
-- ============================================================
--- Échappe les caractères magiques d'un motif Lua.
local function EscapePattern(s)
return (s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"))
end
--- Transforme un glob simple ("bladw_*") en motif Lua ancré ("^bladw_.*$").
local function GlobToPattern(glob)
return "^" .. EscapePattern(glob):gsub("%%%*", ".*") .. "$"
end
--- É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 else
include("bladw_hud/client/cl_hud.lua") for _, root in ipairs(roots) do
include("bladw_cMenu/client/cl_cMenu.lua") if NoCode.config.verbose then
include("bladw_deathscreen/client/cl_interface_ds.lua") Log(C.title, "" .. root)
end
LoadFolder(root)
end
end
surface.CreateFont("bladw_text", { Log(C.dim, "")
font = "Poppins-SemiBold", Log(C.title, " ╔══════════════════════════════════════╗")
size = 25, if SERVER then
weight = 600, Log(C.ok, (" ║ ✓ %d chargé(s) / %d envoyé(s) au client"):format(stats.loaded, stats.sent))
antialias = true else
}) Log(C.ok, (" ║ ✓ %d fichier(s) chargé(s) [CLIENT]"):format(stats.loaded))
end
surface.CreateFont("bladw_text_Medium", { if stats.errors > 0 then
font = "Poppins-SemiBold", Log(C.err, (" ║ ✗ %d erreur(s)"):format(stats.errors))
size = 20, end
weight = 800, Log(C.title, " ╚══════════════════════════════════════╝")
antialias = true Log(C.dim, "")
})
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
@@ -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
}
+44 -76
View File
@@ -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
@@ -232,8 +204,4 @@ hook.Add("PlayerButtonUp", "bladw_CMenu_close", function(ply, key)
if key == KEY_C then if key == KEY_C then
CloseCMenu() CloseCMenu()
end end
end)
hook.Add("OnContextMenuOpen", "bladw_blockC", function()
return true
end) 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
+2 -3
View File
@@ -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,10 +29,11 @@ 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")
if job == "Citizen" or job == "Citizen" then if job == "Citizen" or job == "Citizen" then
hasResponder = true hasResponder = true
break break
end end
+11 -13
View File
@@ -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))
+529
View File
@@ -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)
+116
View File
@@ -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
+40
View File
@@ -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
+47
View File
@@ -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()
+29
View File
@@ -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" },
}
+710
View File
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.