From 904cf3566c9955593beab766fc433a889551134b Mon Sep 17 00:00:00 2001 From: nocode Date: Mon, 20 Jul 2026 13:48:15 +0200 Subject: [PATCH] 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 --- .../client/announcement_menu/cl_menu.lua | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 lua/bladw_announcement/client/announcement_menu/cl_menu.lua diff --git a/lua/bladw_announcement/client/announcement_menu/cl_menu.lua b/lua/bladw_announcement/client/announcement_menu/cl_menu.lua new file mode 100644 index 0000000..faf52be --- /dev/null +++ b/lua/bladw_announcement/client/announcement_menu/cl_menu.lua @@ -0,0 +1,236 @@ +local rndx = include("lib/rndx.lua") +local logo = Material("nocode/announcement/logo.png") + +-- client/ charge avant shared/, donc je m'assure que la table est là +BladW = BladW or {} +BladW.Announcement = BladW.Announcement or {} + +local COL_ACCENT = Color(76, 119, 200) +local COL_WHITE = Color(255, 255, 255) +local COL_BG = Color(0, 0, 0, 200) +local COL_TRACK = Color(54, 54, 54, 200) + +-- fonts responsives (refaites quand la résolution change) +local function AnnScale() + return math.min(ScrW() / 1920, ScrH() / 1080) +end + +-- durées + easing de l'anim d'apparition/disparition +local INTRO_TIME = 0.45 +local OUTRO_TIME = 0.35 +local function EaseOutCubic(t) return 1 - (1 - t) ^ 3 end + +local function BuildFonts() + local s = AnnScale() + surface.CreateFont("bladw_ann_title", { + font = "Poppins-SemiBold", size = math.max(1, math.Round(25 * s)), weight = 600, antialias = true, + }) + surface.CreateFont("bladw_ann_text", { + font = "Poppins-SemiBold", size = math.max(1, math.Round(20 * s)), weight = 800, antialias = true, + }) +end +BuildFonts() +hook.Add("OnScreenSizeChanged", "BladeWin:AnnouncementFonts", BuildFonts) + +-- coupe le texte en lignes qui rentrent dans maxW (px) pour la police donnée +local function WrapText(text, font, maxW) + surface.SetFont(font) + local lines, line = {}, "" + + for _, word in ipairs(string.Explode(" ", text or "")) do + local test = (line == "") and word or (line .. " " .. word) + if surface.GetTextSize(test) > maxW and line ~= "" then + lines[#lines + 1] = line + line = word + else + line = test + end + end + if line ~= "" then lines[#lines + 1] = line end + + return lines +end + +-- le contour du cadre sert de jauge : reste en colRemain, ce qui est passé en colDone (1 = plein, 0 = fini) +local function DrawBorderTimer(x, y, w, h, r, remaining, colRemain, colDone, thickness) + thickness = math.max(1, thickness or 3) + + local pts = {} + local function line(x1, y1, x2, y2) + local steps = math.max(1, math.floor(math.Distance(x1, y1, x2, y2) / 4)) + for i = 0, steps do + pts[#pts + 1] = { Lerp(i / steps, x1, x2), Lerp(i / steps, y1, y2) } + end + end + local function arc(cx, cy, a0, a1) + for i = 0, 6 do + local a = math.rad(Lerp(i / 6, a0, a1)) + pts[#pts + 1] = { cx + math.cos(a) * r, cy + math.sin(a) * r } + end + end + + -- sens horaire à partir du bord haut + line(x + r, y, x + w - r, y) + arc (x + w - r, y + r, 270, 360) + line(x + w, y + r, x + w, y + h - r) + arc (x + w - r, y + h - r, 0, 90) + line(x + w - r, y + h, x + r, y + h) + arc (x + r, y + h - r, 90, 180) + line(x, y + h - r, x, y + r) + arc (x + r, y + r, 180, 270) + + local total = 0 + for i = 1, #pts - 1 do + total = total + math.Distance(pts[i][1], pts[i][2], pts[i + 1][1], pts[i + 1][2]) + end + if total == 0 then return end + + local consumedLen = (1 - remaining) * total + draw.NoTexture() + + local acc = 0 + for i = 1, #pts - 1 do + local x1, y1 = pts[i][1], pts[i][2] + local x2, y2 = pts[i + 1][1], pts[i + 1][2] + local segLen = math.Distance(x1, y1, x2, y2) + + surface.SetDrawColor(acc < consumedLen and colDone or colRemain) + surface.DrawTexturedRectRotated((x1 + x2) * 0.5, (y1 + y2) * 0.5, + segLen + thickness, thickness, -math.deg(math.atan2(y2 - y1, x2 - x1))) + + acc = acc + segLen + end +end + +-- état de l'annonce en cours (survit au reload) +BladW.Announcement.Active = BladW.Announcement.Active or false +BladW.Announcement.Title = BladW.Announcement.Title or "Annonce" +BladW.Announcement.Text = BladW.Announcement.Text or "" +BladW.Announcement.Style = BladW.Announcement.Style or "loader" +BladW.Announcement.Duration = BladW.Announcement.Duration or 10 +BladW.Announcement.StartTime = BladW.Announcement.StartTime or 0 + +-- lance une annonce. style/duration vides = valeurs de la config +function BladW.Announcement.Show(title, text, style, duration) + local Conf = BladW.Announcement.Conf or {} + + BladW.Announcement.Title = (title and title ~= "") and title or "Annonce" + BladW.Announcement.Text = text or "" + BladW.Announcement.Style = (style and style ~= "") and style or (Conf.announcement_style or "loader") + BladW.Announcement.Duration = (duration and duration > 0) and duration or (Conf.announcement_duration or 10) + BladW.Announcement.StartTime = CurTime() + BladW.Announcement.Active = true + + if Conf.announcement_sound and Conf.announcement_sound ~= "" then + surface.PlaySound(Conf.announcement_sound) + end +end + +-- le serveur m'envoie une annonce à afficher +net.Receive("bladWShowAnnouncement", function() + local title = net.ReadString() + local text = net.ReadString() + local style = net.ReadString() + local duration = net.ReadUInt(16) + BladW.Announcement.Show(title, text, style, duration) +end) + +-- affichage (responsive + s'adapte au message) +hook.Add("HUDPaint", "BladeWin:Announcement", function() + if not BladW.Announcement.Active then return end + + local ply = LocalPlayer() + if not IsValid(ply) then return end + + local duration = BladW.Announcement.Duration or 10 + local elapsed = CurTime() - BladW.Announcement.StartTime + + -- temps écoulé -> on cache + if elapsed >= duration then + BladW.Announcement.Active = false + return + end + + local style = BladW.Announcement.Style or "loader" + local title = BladW.Announcement.Title + local text = BladW.Announcement.Text + local remaining = math.Clamp(1 - (elapsed / duration), 0, 1) + + -- échelle responsive + local s = AnnScale() + local padX = 26 * s + local padY = 18 * s + local gap = 8 * s + local logoS = 62 * s + local barW = 200 * s + local barH = 18 * s + local radius = 14 * s + + -- wrap du message (largeur interne responsive, la vraie limite c'est le nb de caractères) + local wrapW = ScrW() * 0.8 + local lines = WrapText(text, "bladw_ann_text", wrapW) + + surface.SetFont("bladw_ann_title") + local titleW, titleH = surface.GetTextSize(title) + + surface.SetFont("bladw_ann_text") + local _, lineH = surface.GetTextSize("Ay") + local msgW = 0 + for _, l in ipairs(lines) do + msgW = math.max(msgW, (surface.GetTextSize(l))) + end + + -- taille de la box selon le contenu + local contentW = math.max(titleW, msgW, style == "loader" and barW or 0) + local boxW = math.Clamp(contentW + padX * 2, 260 * s, ScrW() * 0.92) + local msgH = #lines * lineH + local boxH = padY + logoS + gap + titleH + gap + msgH + + (style == "loader" and (gap + barH) or 0) + padY + + -- anim : fondu + glisse du haut (et l'inverse quand ça part) + local appear = math.Clamp(elapsed / INTRO_TIME, 0, 1) + local disappear = math.Clamp((duration - elapsed) / OUTRO_TIME, 0, 1) + local anim = math.min(EaseOutCubic(appear), EaseOutCubic(disappear)) -- 0 -> 1 -> 0 + local slide = (1 - anim) * 26 * s + + local boxX = (ScrW() - boxW) * 0.5 + local boxY = ScrH() * 0.035 - slide + local cx = boxX + boxW * 0.5 + + surface.SetAlphaMultiplier(anim) + + -- fond + rndx.Draw(radius, boxX, boxY, boxW, boxH, nil, rndx.SHAPE_FIGMA + rndx.BLUR) + rndx.Draw(radius, boxX, boxY, boxW, boxH, COL_BG, rndx.SHAPE_FIGMA) + + -- le loader garde son contour blanc (le simple lui a sa jauge sur le bord) + if style == "loader" then + rndx.DrawOutlined(radius, boxX, boxY, boxW, boxH, COL_WHITE, 3 * s, rndx.SHAPE_FIGMA) + end + + -- contenu empilé et centré + local y = boxY + padY + rndx.DrawMaterial(0, cx - logoS * 0.5, y, logoS, logoS, COL_WHITE, logo) + y = y + logoS + gap + + draw.SimpleText(title, "bladw_ann_title", cx, y, COL_WHITE, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) + y = y + titleH + gap + + for _, l in ipairs(lines) do + draw.SimpleText(l, "bladw_ann_text", cx, y, COL_WHITE, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) + y = y + lineH + end + + -- jauge de temps + if style == "loader" then + y = y + gap + local bx = cx - barW * 0.5 + rndx.Draw(barH * 0.5, bx, y, barW, barH, COL_TRACK, rndx.SHAPE_FIGMA) + rndx.Draw(barH * 0.5, bx, y, barW * remaining, barH, COL_ACCENT, rndx.SHAPE_FIGMA) + else + -- le bord bleu qui se vide en blanc + DrawBorderTimer(boxX, boxY, boxW, boxH, radius, remaining, COL_ACCENT, COL_WHITE, 3 * s) + end + + surface.SetAlphaMultiplier(1) +end)