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