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)