--[[ wave ui v1.2.2 - pc + mobile (auto detected) local Wave = loadstring(game:HttpGet("https://unagitatedly.site/Wave.lua"))() local Window = Wave:CreateWindow({ Title = "Wave" }) local Tab = Window:Tab({ Name = "Main", Icon = "home" }) local Sec = Tab:Section({ Name = "Section" }) ]] local Wave = { Version = "1.2.2", Flags = {}, Options = {}, Folder = "Wave", Logo = "https://unagitatedly.site/Wave.png", IconBase = "https://unagitatedly.site/icons/", Lowercase = true, } -- services local Players = game:GetService("Players") local TweenService = game:GetService("TweenService") local UserInputService = game:GetService("UserInputService") local RunService = game:GetService("RunService") local TextService = game:GetService("TextService") local HttpService = game:GetService("HttpService") local LocalPlayer = Players.LocalPlayer -- device: "PC" | "Mobile" local function detectDevice() if UserInputService.TouchEnabled then if not (UserInputService.KeyboardEnabled or UserInputService.MouseEnabled) then return "Mobile" end -- touch laptops: go by the last input used if UserInputService:GetLastInputType() == Enum.UserInputType.Touch then return "Mobile" end end return "PC" end local Device, IsMobile local ROW, TEXT, NOTIF_W local function setDevice(device) if device == nil or device == "Auto" then device = detectDevice() end Device = device IsMobile = device == "Mobile" ROW = IsMobile and 38 or 34 -- taller rows on touch TEXT = IsMobile and 14 or 13 NOTIF_W = IsMobile and 270 or 290 Wave.Device, Wave.IsMobile = Device, IsMobile end setDevice() Wave.DetectDevice = detectDevice function Wave:SetDevice(device) setDevice(device) end -- theme local Theme = { Background = Color3.fromRGB(9, 9, 10), Sidebar = Color3.fromRGB(13, 13, 15), Surface = Color3.fromRGB(15, 15, 17), Element = Color3.fromRGB(21, 21, 24), ElementHover = Color3.fromRGB(28, 28, 32), Inner = Color3.fromRGB(30, 30, 34), Stroke = Color3.fromRGB(34, 34, 38), Text = Color3.fromRGB(240, 240, 242), SubText = Color3.fromRGB(150, 150, 158), Muted = Color3.fromRGB(78, 78, 86), Accent = Color3.fromRGB(255, 255, 255), AccentText = Color3.fromRGB(9, 9, 10), Font = Enum.Font.Gotham, FontMedium = Enum.Font.GothamMedium, FontBold = Enum.Font.GothamBold, } Wave.Theme = Theme -- utils local function create(class, props) local obj = Instance.new(class) local parent = props and props.Parent for k, v in pairs(props or {}) do if k ~= "Parent" then obj[k] = v end end if parent then obj.Parent = parent end return obj end local function corner(parent, radius) return create("UICorner", {CornerRadius = typeof(radius) == "UDim" and radius or UDim.new(0, radius or 8), Parent = parent}) end local function stroke(parent, transparency) return create("UIStroke", { Color = Theme.Stroke, Transparency = transparency or 0, ApplyStrokeMode = Enum.ApplyStrokeMode.Border, Parent = parent, }) end local function list(parent, pad, horizontal) return create("UIListLayout", { Padding = UDim.new(0, pad or 6), FillDirection = horizontal and Enum.FillDirection.Horizontal or Enum.FillDirection.Vertical, SortOrder = Enum.SortOrder.LayoutOrder, Parent = parent, }) end local function tween(obj, props, time, style) local t = TweenService:Create(obj, TweenInfo.new(time or 0.2, style or Enum.EasingStyle.Quint, Enum.EasingDirection.Out), props) t:Play() return t end local function txt(s) if Wave.Lowercase and type(s) == "string" then return s:lower() end return s end local function label(props) props.Text = txt(props.Text) props.BackgroundTransparency = 1 props.Font = props.Font or Theme.FontMedium props.TextColor3 = props.TextColor3 or Theme.Text props.TextSize = props.TextSize or TEXT props.TextXAlignment = props.TextXAlignment or Enum.TextXAlignment.Left return create("TextLabel", props) end local function isPress(input) return input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch end local function isMove(input) return input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch end -- no AutomaticSize (breaks under UIScale), heights come from the layout -- AbsoluteContentSize is scaled so divide by the UIScale local function fitHeight(frame, layout, extra, scaler) local function update() local s = scaler and scaler.Scale or 1 if s <= 0 then return end frame.Size = UDim2.new(frame.Size.X.Scale, frame.Size.X.Offset, 0, math.ceil(layout.AbsoluteContentSize.Y / s) + (extra or 0)) end layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(update) update() end local function fitCanvas(scroll, layout, extra, scaler) local function update() local s = scaler and scaler.Scale or 1 if s <= 0 then return end scroll.CanvasSize = UDim2.fromOffset(0, math.ceil(layout.AbsoluteContentSize.Y / s) + (extra or 0)) end layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(update) update() end -- svg path -> ui strokes (M/L/H/V/C/Z + circles). scale based so it stays sharp local Icons = { minus = {vb = 24, "M5 12h14"}, plus = {vb = 24, "M5 12h14", "M12 5v14"}, x = {vb = 24, "M18 6 6 18", "m6 6 12 12"}, check = {vb = 24, "M20 6 9 17l-5-5"}, ["chevron-down"] = {vb = 24, "m6 9 6 6 6-6"}, ["chevron-right"]= {vb = 24, "m9 18 6-6-6-6"}, ["grip-vertical"]= {vb = 24, {9, 5, 1.3}, {9, 12, 1.3}, {9, 19, 1.3}, {15, 5, 1.3}, {15, 12, 1.3}, {15, 19, 1.3}}, info = {vb = 24, {12, 12, 10, ring = true}, "M12 16v-4", {12, 8, 1.1}}, ["circle-check"] = {vb = 24, {12, 12, 10, ring = true}, "m9 12 2 2 4-4"}, ["circle-x"] = {vb = 24, {12, 12, 10, ring = true}, "m15 9-6 6", "m9 9 6 6"}, ["triangle-alert"] = {vb = 24, "M12 3 L22 20 L2 20 Z", "M12 9v4", {12, 17, 1.1}}, home = {vb = 24, "M3 10 12 3l9 7", "M5 9v11h14V9", "M10 20v-6h4v6"}, sword = {vb = 24, "M14.5 17.5 3 6V3h3l11.5 11.5", "M13 19l6-6", "M16 16l4 4", "M19 21l2-2"}, eye = {vb = 24, "M2 12C5 5 19 5 22 12C19 19 5 19 2 12Z", {12, 12, 3, ring = true}}, user = {vb = 24, {12, 8, 4, ring = true}, "M4 21C4 15 20 15 20 21"}, zap = {vb = 24, "M13 2 3 14h9l-1 8 10-12h-9l1-8Z"}, layers = {vb = 24, "m12 2 10 5-10 5L2 7Z", "m2 17 10 5 10-5", "m2 12 10 5 10-5"}, sliders = {vb = 24, "M21 4H14", "M10 4H3", "M21 12H12", "M8 12H3", "M21 20H16", "M12 20H3", "M14 2v4", "M8 10v4", "M16 18v4"}, search = {vb = 24, {11, 11, 8, ring = true}, "m21 21-4.3-4.3"}, star = {vb = 24, "M12 2 15 8.6 22 9.3 17 14 18.2 21 12 17.8 5.8 21 7 14 2 9.3 9 8.6Z"}, folder = {vb = 24, "M3 6V19H21V8H12L10 5H3Z"}, settings = {vb = 24, {12, 12, 3, ring = true}, {12, 12, 8, ring = true}, "M12 2v2", "M12 20v2", "M2 12h2", "M20 12h2", "m4.9 4.9 1.4 1.4", "m17.7 17.7 1.4 1.4", "m4.9 19.1 1.4-1.4", "m17.7 6.3 1.4-1.4"}, -- logo fallback if images aren't supported (svgrepo #452816) wave = {vb = 400, weight = 0.85, "M54.1283 283.001C52.8319 269.74 61.6674 256.236 66.8345 244.212C101.013 164.722 175.226 108.737 272.532 113.255C369.839 117.773 347.821 179.071 332.874 185.404C316.115 192.505 311.264 167.423 297.932 172.739C288.462 176.515 297.063 191.448 275.696 194.111C267.743 195.105 262.071 180.79 253.46 179.071C246.965 177.779 243.466 190.944 235.196 190.944C228.342 190.944 226.77 180.144 220.104 177.491C214.951 175.433 210.584 191.737 202.636 193.323C192.462 195.348 185.132 161.883 163.038 191.242", "M248.772 191.721C201.419 236.2 219.137 281.053 309.587 287.981"}, } Wave.Icons = Icons local pathCache = {} local function parsePath(d, steps) local key = d .. "|" .. steps if pathCache[key] then return pathCache[key] end local lines, line = {}, nil local x, y, sx, sy = 0, 0, 0, 0 local function push(px, py) if not line then line = {} lines[#lines + 1] = line end line[#line + 1] = Vector2.new(px, py) end for cmd, args in d:gmatch("([MmLlHhVvCcZz])([^MmLlHhVvCcZz]*)") do local n = {} for num in args:gmatch("[%-%+]?%d*%.?%d+") do n[#n + 1] = tonumber(num) end local rel, c, i = cmd:lower() == cmd, cmd:upper(), 1 if c == "M" then local first = true while i + 1 <= #n do local nx, ny = n[i], n[i + 1] if rel then nx, ny = x + nx, y + ny end x, y = nx, ny if first then line = nil sx, sy = x, y first = false end push(x, y) i += 2 end elseif c == "L" then while i + 1 <= #n do local nx, ny = n[i], n[i + 1] if rel then nx, ny = x + nx, y + ny end x, y = nx, ny push(x, y) i += 2 end elseif c == "H" then while i <= #n do x = rel and x + n[i] or n[i] push(x, y) i += 1 end elseif c == "V" then while i <= #n do y = rel and y + n[i] or n[i] push(x, y) i += 1 end elseif c == "C" then while i + 5 <= #n do local x1, y1, x2, y2, x3, y3 = n[i], n[i + 1], n[i + 2], n[i + 3], n[i + 4], n[i + 5] if rel then x1, y1, x2, y2, x3, y3 = x + x1, y + y1, x + x2, y + y2, x + x3, y + y3 end for s = 1, steps do local t = s / steps local u = 1 - t push( u * u * u * x + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3, u * u * u * y + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3 ) end x, y = x3, y3 i += 6 end elseif c == "Z" then push(sx, sy) x, y = sx, sy end end pathCache[key] = lines return lines end -- stroke fallback. returns holder, setColor local function drawStrokes(name, size, color, parent) local holder = create("Frame", { Name = "Icon", BackgroundTransparency = 1, Size = UDim2.fromOffset(size, size), Parent = parent, }) -- uploaded images work too if type(name) == "string" and name:find("rbxasset") then local img = create("ImageLabel", { BackgroundTransparency = 1, Size = UDim2.fromScale(1, 1), Image = name, ImageColor3 = color, ScaleType = Enum.ScaleType.Fit, Parent = holder, }) return holder, function(c) img.ImageColor3 = c end end local spec = type(name) == "table" and name or Icons[name] if not spec then return holder, function() end end local vb = spec.vb or 24 local weight = (spec.weight or 1) * (vb * 0.085) -- stroke width (vb units) local steps = vb >= 100 and 8 or 10 local parts = {} local function seg(a, b) local d = b - a local f = create("Frame", { BorderSizePixel = 0, BackgroundColor3 = color, AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale((a.X + b.X) / 2 / vb, (a.Y + b.Y) / 2 / vb), Size = UDim2.fromScale((d.Magnitude + weight) / vb, weight / vb), Rotation = math.deg(math.atan2(d.Y, d.X)), Parent = holder, }) corner(f, UDim.new(1, 0)) parts[#parts + 1] = f end for _, item in ipairs(spec) do if type(item) == "string" then for _, pl in ipairs(parsePath(item, steps)) do for k = 1, #pl - 1 do seg(pl[k], pl[k + 1]) end end else local cx, cy, r = item[1], item[2], item[3] if item.ring then local n = r > 6 and 20 or 14 local prev for k = 0, n do local a = k / n * math.pi * 2 local p = Vector2.new(cx + math.cos(a) * r, cy + math.sin(a) * r) if prev then seg(prev, p) end prev = p end else local dot = create("Frame", { BorderSizePixel = 0, BackgroundColor3 = color, AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(cx / vb, cy / vb), Size = UDim2.fromScale(r * 2.2 / vb, r * 2.2 / vb), Parent = holder, }) corner(dot, UDim.new(1, 0)) parts[#parts + 1] = dot end end end return holder, function(c) for _, p in ipairs(parts) do p.BackgroundColor3 = c end end end -- remote images (logo, icons): fetched once, cached to disk, getcustomasset local assetCache = {} local getAsset = getcustomasset or getsynasset local function canFetch() return getAsset ~= nil and writefile ~= nil and isfile ~= nil end local function fetchAsset(url, callback) local entry = assetCache[url] if type(entry) == "string" then return callback(entry) end if entry == false then return callback(nil) end if type(entry) == "table" then entry[#entry + 1] = callback return end local waiting = {callback} assetCache[url] = waiting task.spawn(function() local asset if canFetch() then pcall(function() local dir = Wave.Folder .. "/assets" if makefolder and isfolder then if not isfolder(Wave.Folder) then makefolder(Wave.Folder) end if not isfolder(dir) then makefolder(dir) end end local path = dir .. "/" .. Wave.Version .. "_" .. url:gsub("[^%w]", "_"):sub(-48) if not isfile(path) then writefile(path, game:HttpGet(url)) end asset = getAsset(path) end) end assetCache[url] = asset or false for _, cb in ipairs(waiting) do task.spawn(cb, asset) end end) end local function imageIcon(url, fallbackName, size, color, parent) local img = create("ImageLabel", { Name = "Icon", BackgroundTransparency = 1, ImageColor3 = color, ScaleType = Enum.ScaleType.Fit, Size = UDim2.fromOffset(size, size), Parent = parent, }) local setFallback fetchAsset(url, function(asset) if asset then img.Image = asset elseif img.Parent then local h, set = drawStrokes(fallbackName, size, img.ImageColor3, img) h.Size = UDim2.fromScale(1, 1) setFallback = set end end) -- fade color changes (tab select) instead of snapping return img, function(c) tween(img, {ImageColor3 = c}, 0.15) if setFallback then setFallback(c) end end end -- returns holder, setColor local function drawIcon(name, size, color, parent) if type(name) == "string" and Icons[name] and name ~= "wave" and canFetch() then return imageIcon(Wave.IconBase .. name .. ".png", name, size, color, parent) end return drawStrokes(name, size, color, parent) end Wave.DrawIcon = drawIcon local function logo(size, color, parent, src) src = src or Wave.Logo if type(src) == "string" and src:find("^rbxasset") then return (drawStrokes(src, size, color, parent)) end if type(src) == "string" and src:find("^https?://") and canFetch() then return (imageIcon(src, "wave", size, color, parent)) end return (drawStrokes("wave", size, color, parent)) end -- gui local function getGuiParent() local ok, hui = pcall(function() return gethui and gethui() end) if ok and hui then return hui end local okCore, core = pcall(function() return game:GetService("CoreGui") end) if okCore and core then local test = Instance.new("Folder") if pcall(function() test.Parent = core end) then test:Destroy() return core end end return LocalPlayer:WaitForChild("PlayerGui") end local Screen = create("ScreenGui", { Name = HttpService:GenerateGUID(false), ResetOnSpawn = false, IgnoreGuiInset = true, ZIndexBehavior = Enum.ZIndexBehavior.Sibling, DisplayOrder = 999, }) pcall(function() if syn and syn.protect_gui then syn.protect_gui(Screen) end end) Screen.Parent = getGuiParent() Wave.ScreenGui = Screen if canFetch() then fetchAsset(Wave.Logo, function() end) for name in pairs(Icons) do if name ~= "wave" then fetchAsset(Wave.IconBase .. name .. ".png", function() end) end end end local function viewSize() local v = Screen.AbsoluteSize if v.X < 1 then v = workspace.CurrentCamera and workspace.CurrentCamera.ViewportSize or Vector2.new(1280, 720) end return v end -- per device ui scale local scalers = {} local function computeScale() local vp = viewSize() if IsMobile then return math.clamp(math.min(vp.X / 600, vp.Y / 360), 0.55, 1) end return math.clamp(math.min(vp.X / 1150, vp.Y / 720), 0.7, 1) end local function addScaler(obj) local s = create("UIScale", {Scale = computeScale(), Parent = obj}) scalers[#scalers + 1] = s s.AncestryChanged:Connect(function(_, parent) if parent then return end local i = table.find(scalers, s) if i then table.remove(scalers, i) end end) return s end local function refreshScale() local s = computeScale() for _, sc in ipairs(scalers) do if not sc:GetAttribute("Animating") then sc.Scale = s end end end if workspace.CurrentCamera then workspace.CurrentCamera:GetPropertyChangedSignal("ViewportSize"):Connect(refreshScale) end -- drag (mouse + touch), eased. short press = tap -- scale positions tracked as floats, easing the int Offset jitters local function getPos(target) local ap, as, ap0 = target.AbsolutePosition, target.AbsoluteSize, Screen.AbsolutePosition return ap.X - ap0.X + as.X * target.AnchorPoint.X, ap.Y - ap0.Y + as.Y * target.AnchorPoint.Y end local function setPos(target, x, y) local v = viewSize() target.Position = UDim2.fromScale(x / v.X, y / v.Y) end local function makeDraggable(handle, target, onTap) local dragging, moved = false, false local dragInput, startInput local startX, startY, curX, curY, goalX, goalY = 0, 0, 0, 0, 0, 0 local stepConn local function clamp(x, y) local view = viewSize() local size = target.AbsoluteSize local ax, ay = target.AnchorPoint.X, target.AnchorPoint.Y return math.clamp(x, size.X * ax, view.X - size.X * (1 - ax)), math.clamp(y, size.Y * ay, view.Y - size.Y * (1 - ay)) end local function step(dt) local a = 1 - math.exp(-dt * 34) curX += (goalX - curX) * a curY += (goalY - curY) * a if not dragging and math.abs(goalX - curX) < 0.05 and math.abs(goalY - curY) < 0.05 then curX, curY = goalX, goalY stepConn:Disconnect() stepConn = nil end setPos(target, curX, curY) end handle.InputBegan:Connect(function(input) if not isPress(input) then return end dragging, moved = true, false startInput = input.Position startX, startY = getPos(target) if not stepConn then curX, curY = startX, startY end goalX, goalY = curX, curY dragInput = input.UserInputType == Enum.UserInputType.Touch and input or nil local changed changed = input.Changed:Connect(function() if input.UserInputState == Enum.UserInputState.End then dragging = false changed:Disconnect() if not moved and onTap then onTap() end end end) end) UserInputService.InputChanged:Connect(function(input) if not dragging or not isMove(input) then return end if dragInput and input ~= dragInput then return end if not dragInput and input.UserInputType ~= Enum.UserInputType.MouseMovement then return end local delta = input.Position - startInput if not moved and delta.Magnitude < 6 then return end moved = true goalX, goalY = clamp(startX + delta.X, startY + delta.Y) if not stepConn then stepConn = RunService.RenderStepped:Connect(step) end end) end -- notifications local NotifyHolder = create("Frame", { Name = "Notifications", BackgroundTransparency = 1, Size = UDim2.fromScale(1, 1), ZIndex = 100, Parent = Screen, }) local activeNotifs = {} local NOTIF_ICONS = { info = "info", success = "circle-check", warning = "triangle-alert", error = "circle-x", } local function layoutNotifs() local y = IsMobile and 60 or 16 for _, n in ipairs(activeNotifs) do n.Goal = IsMobile and UDim2.new(0.5, 0, 0, y) or UDim2.new(1, -16, 1, -y) if not n.Closing then tween(n.Frame, {Position = n.Goal}, 0.35) end y += n.Height * n.Scale.Scale + 8 end end -- fade a small tree (notifications only, windows just scale) local function fadeTree(root, faded, time) for _, o in ipairs(root:GetDescendants()) do if o:IsA("TextLabel") then tween(o, {TextTransparency = faded and 1 or 0}, time) elseif o:IsA("ImageLabel") then tween(o, {ImageTransparency = faded and 1 or 0}, time) elseif o:IsA("Frame") then local base = o:GetAttribute("Bg") if base == nil then base = o.BackgroundTransparency o:SetAttribute("Bg", base) end if base < 1 then tween(o, {BackgroundTransparency = faded and 1 or base}, time) end elseif o:IsA("UIStroke") then local base = o:GetAttribute("Base") if base == nil then base = o.Transparency o:SetAttribute("Base", base) end tween(o, {Transparency = faded and 1 or base}, time) end end tween(root, {BackgroundTransparency = faded and 1 or 0}, time) end function Wave:Notify(opts) opts = type(opts) == "string" and {Content = opts} or (opts or {}) local title = opts.Title or "Wave" local content = opts.Content or "" local duration = opts.Duration or 4 local icon = NOTIF_ICONS[opts.Type or "info"] or opts.Icon or "info" local textW = NOTIF_W - 56 local bodyH = TextService:GetTextSize(content:gsub("<[^>]+>", ""), TEXT - 1, Theme.Font, Vector2.new(textW, math.huge)).Y local height = math.max(52, 31 + bodyH + 13) local card = create("TextButton", { Name = "Notification", AutoButtonColor = false, Text = "", BackgroundColor3 = Theme.Sidebar, AnchorPoint = IsMobile and Vector2.new(0.5, 0) or Vector2.new(1, 1), Size = UDim2.fromOffset(NOTIF_W, height), Parent = NotifyHolder, }) corner(card, 10) stroke(card, 0.2) local scaler = addScaler(card) local ic = drawIcon(icon, 16, Theme.Text, card) ic.Position = UDim2.fromOffset(14, 13) label({ Text = title, RichText = true, Font = Theme.FontBold, Position = UDim2.fromOffset(40, 11), Size = UDim2.new(1, -52, 0, 18), TextTruncate = Enum.TextTruncate.AtEnd, Parent = card, }) label({ Text = content, RichText = true, Font = Theme.Font, TextSize = TEXT - 1, TextColor3 = Theme.SubText, TextWrapped = true, TextYAlignment = Enum.TextYAlignment.Top, Position = UDim2.fromOffset(40, 30), Size = UDim2.fromOffset(textW, bodyH + 4), Parent = card, }) local bar = create("Frame", { BackgroundColor3 = Theme.Accent, BackgroundTransparency = 0.6, BorderSizePixel = 0, AnchorPoint = Vector2.new(0, 1), Position = UDim2.new(0, 10, 1, -1), Size = UDim2.new(1, -20, 0, 2), Parent = card, }) corner(bar, UDim.new(1, 0)) local entry = {Frame = card, Height = height, Scale = scaler} table.insert(activeNotifs, 1, entry) layoutNotifs() -- intro fadeTree(card, true, 0) card.Position = entry.Goal + (IsMobile and UDim2.fromOffset(0, -20) or UDim2.fromOffset(30, 0)) tween(card, {Position = entry.Goal}, 0.4) fadeTree(card, false, 0.25) tween(bar, {Size = UDim2.new(0, 0, 0, 2)}, duration, Enum.EasingStyle.Linear) local closed = false local function close() if closed then return end closed = true entry.Closing = true fadeTree(card, true, 0.2) tween(card, {Position = entry.Goal + (IsMobile and UDim2.fromOffset(0, -14) or UDim2.fromOffset(30, 0))}, 0.25) task.delay(0.25, function() local idx = table.find(activeNotifs, entry) if idx then table.remove(activeNotifs, idx) end card:Destroy() layoutNotifs() end) end card.MouseButton1Click:Connect(close) -- tap to close task.delay(duration, close) return {Close = close} end -- config local function serialize(opt) local v = opt.Value if typeof(v) == "Color3" then return {v.R, v.G, v.B} end if typeof(v) == "EnumItem" then return v.Name end if opt.Type == "Dropdown" and opt.Multi then return opt:Get() end return v end local function deserialize(opt, v) if opt.Type == "ColorPicker" and type(v) == "table" then return Color3.new(v[1], v[2], v[3]) end if opt.Type == "Keybind" and type(v) == "string" then return Enum.KeyCode[v] end return v end function Wave:SaveConfig(name) if not writefile then return false end pcall(function() if not isfolder(self.Folder) then makefolder(self.Folder) end end) local data = {} for flag, opt in pairs(self.Options) do data[flag] = serialize(opt) end writefile(self.Folder .. "/" .. name .. ".json", HttpService:JSONEncode(data)) return true end function Wave:LoadConfig(name) if not (readfile and isfile) then return false end local path = self.Folder .. "/" .. name .. ".json" if not isfile(path) then return false end local ok, data = pcall(function() return HttpService:JSONDecode(readfile(path)) end) if not ok then return false end for flag, v in pairs(data) do local opt = self.Options[flag] if opt then pcall(opt.Set, opt, deserialize(opt, v)) end end return true end function Wave:ListConfigs() local out = {} if not (listfiles and isfolder) or not isfolder(self.Folder) then return out end for _, f in ipairs(listfiles(self.Folder)) do local n = f:match("([^/\\]+)%.json$") if n then out[#out + 1] = n end end return out end local function register(opt) if opt.Flag then Wave.Options[opt.Flag] = opt Wave.Flags[opt.Flag] = opt.Value end end local function setFlag(opt) if opt.Flag then Wave.Flags[opt.Flag] = opt.Value end end -- keybinds local keybinds = {} UserInputService.InputBegan:Connect(function(input, processed) if processed or input.UserInputType ~= Enum.UserInputType.Keyboard then return end for _, kb in ipairs(keybinds) do if kb.Value and input.KeyCode == kb.Value and not kb.Listening then task.spawn(kb.Callback, kb.Value) end end end) -- one shared move listener for sliders / color pickers local dragTargets = {} UserInputService.InputChanged:Connect(function(input) if not isMove(input) then return end for target in pairs(dragTargets) do if (target.Input and input == target.Input) or (not target.Input and input.UserInputType == Enum.UserInputType.MouseMovement) then target.Move(input) end end end) local function bindDrag(frame, onMove) frame.InputBegan:Connect(function(input) if not isPress(input) then return end local target = {Move = onMove, Input = input.UserInputType == Enum.UserInputType.Touch and input or nil} dragTargets[target] = true onMove(input) local c c = input.Changed:Connect(function() if input.UserInputState == Enum.UserInputState.End then dragTargets[target] = nil c:Disconnect() end end) end) end -- elements local Section = {} Section.__index = Section function Section:_order() self._n += 1 return self._n end local function row(section, height) local f = create("Frame", { BackgroundColor3 = section.RowColor or Theme.Element, Size = UDim2.new(1, 0, 0, height or ROW), ClipsDescendants = true, LayoutOrder = section:_order(), Parent = section.Container, }) corner(f, 7) return f end local function hover(frame, button) if IsMobile then return end local base = frame.BackgroundColor3 local over = base:Lerp(Color3.new(1, 1, 1), 0.04) button.MouseEnter:Connect(function() tween(frame, {BackgroundColor3 = over}, 0.12) end) button.MouseLeave:Connect(function() tween(frame, {BackgroundColor3 = base}, 0.12) end) end -- expandable area inside a row (toggle settings). returns a Section-like object local function expander(section, r, arrowRight) local sub = create("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(8, ROW), Size = UDim2.new(1, -16, 0, 0), Parent = r, }) local layout = list(sub, 5) local child = setmetatable({_n = 0, Scaler = section.Scaler, Container = sub, RowColor = Theme.Surface}, getmetatable(section)) local open, animating = false, false local function contentH() local s = section.Scaler.Scale return s > 0 and math.ceil(layout.AbsoluteContentSize.Y / s) or 0 end local function resize(animate) local ch = contentH() sub.Size = UDim2.new(1, -16, 0, ch) local goal = UDim2.new(1, 0, 0, ROW + (open and ch + 8 or 0)) if animate then tween(r, {Size = goal}, 0.25) else r.Size = goal end end layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() if not animating then resize(false) end end) local arrow = drawIcon("chevron-down", 14, Theme.SubText, r) arrow.AnchorPoint = Vector2.new(1, 0.5) arrow.Position = UDim2.new(1, -arrowRight, 0, ROW / 2) local btn = create("TextButton", { BackgroundTransparency = 1, Text = "", AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -(arrowRight - 8), 0, 0), Size = UDim2.fromOffset(30, ROW), ZIndex = 3, Parent = r, }) btn.MouseButton1Click:Connect(function() open = not open animating = true tween(arrow, {Rotation = open and 180 or 0}, 0.25) resize(true) task.delay(0.26, function() animating = false resize(false) end) end) return child end local function hit(parent, height) return create("TextButton", { BackgroundTransparency = 1, Text = "", Size = height and UDim2.new(1, 0, 0, height) or UDim2.fromScale(1, 1), Parent = parent, }) end -- wrapped text that keeps its height right under UIScale local function wrappedLabel(section, props) props.TextWrapped = true props.TextYAlignment = Enum.TextYAlignment.Top props.Size = UDim2.new(1, 0, 0, 16) local l = label(props) local function update() local s = section.Scaler.Scale if s > 0 then l.Size = UDim2.new(1, 0, 0, math.ceil(l.TextBounds.Y / s)) end end l:GetPropertyChangedSignal("TextBounds"):Connect(update) update() return l end function Section:Label(text) local l = wrappedLabel(self, { Text = text or "", Font = Theme.Font, TextColor3 = Theme.SubText, LayoutOrder = self:_order(), Parent = self.Container, }) return {Set = function(_, t) l.Text = txt(t) end, Instance = l} end function Section:Paragraph(opts) local f = create("Frame", { BackgroundColor3 = Theme.Element, Size = UDim2.new(1, 0, 0, 40), LayoutOrder = self:_order(), Parent = self.Container, }) corner(f, 7) create("UIPadding", {PaddingTop = UDim.new(0, 9), PaddingBottom = UDim.new(0, 9), PaddingLeft = UDim.new(0, 11), PaddingRight = UDim.new(0, 11), Parent = f}) local layout = list(f, 3) local t = label({Text = opts.Title or "", Font = Theme.FontBold, Size = UDim2.new(1, 0, 0, 16), Parent = f}) local c = wrappedLabel(self, {Text = opts.Content or "", Font = Theme.Font, TextColor3 = Theme.SubText, LayoutOrder = 1, Parent = f}) fitHeight(f, layout, 18, self.Scaler) return { Set = function(_, title, content) if title then t.Text = txt(title) end if content then c.Text = txt(content) end end, } end function Section:Button(opts) local r = row(self) label({Text = opts.Name or "Button", Position = UDim2.fromOffset(11, 0), Size = UDim2.new(1, -40, 1, 0), Parent = r}) local ic = drawIcon("chevron-right", 14, Theme.SubText, r) ic.AnchorPoint = Vector2.new(1, 0.5) ic.Position = UDim2.new(1, -10, 0.5, 0) local btn = hit(r) hover(r, btn) btn.MouseButton1Click:Connect(function() r.BackgroundColor3 = Theme.Inner tween(r, {BackgroundColor3 = Theme.Element}, 0.3) if opts.Callback then task.spawn(opts.Callback) end end) return {Instance = r} end function Section:Toggle(opts) local opt = {Type = "Toggle", Flag = opts.Flag, Value = opts.Default == true, Callback = opts.Callback or function() end} local r = row(self) label({Text = opts.Name or "Toggle", Position = UDim2.fromOffset(11, 0), Size = UDim2.new(1, -100, 0, ROW), Parent = r}) local tw, th = IsMobile and 38 or 32, IsMobile and 22 or 18 local track = create("Frame", { BackgroundColor3 = Theme.Inner, AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0, ROW / 2), Size = UDim2.fromOffset(tw, th), Parent = r, }) corner(track, UDim.new(1, 0)) local knob = create("Frame", { BackgroundColor3 = Theme.Muted, AnchorPoint = Vector2.new(0, 0.5), Position = UDim2.new(0, 3, 0.5, 0), Size = UDim2.fromOffset(th - 6, th - 6), Parent = track, }) corner(knob, UDim.new(1, 0)) local function render(instant) local on = opt.Value local t = instant and 0 or 0.2 tween(track, {BackgroundColor3 = on and Theme.Accent or Theme.Inner}, t) tween(knob, { Position = on and UDim2.new(1, -(th - 3), 0.5, 0) or UDim2.new(0, 3, 0.5, 0), BackgroundColor3 = on and Theme.AccentText or Theme.Muted, }, t) end function opt:Set(v) self.Value = v == true setFlag(self) render() task.spawn(self.Callback, self.Value) end function opt:Get() return self.Value end local btn = hit(r, ROW) hover(r, btn) btn.MouseButton1Click:Connect(function() opt:Set(not opt.Value) end) -- arrow next to the switch, opens this toggle's own settings local section = self function opt:Settings() if self._settings then return self._settings end self._settings = expander(section, r, tw + 20) return self._settings end render(true) register(opt) if opt.Value then task.spawn(opt.Callback, true) end if opts.Settings then return opt, opt:Settings() end return opt end function Section:Slider(opts) local min, max = opts.Min or 0, opts.Max or 100 local decimals = opts.Decimals or 0 local suffix = opts.Suffix or "" local opt = {Type = "Slider", Flag = opts.Flag, Value = math.clamp(opts.Default or min, min, max), Callback = opts.Callback or function() end} local r = row(self, ROW + 12) label({Text = opts.Name or "Slider", Position = UDim2.fromOffset(11, 5), Size = UDim2.new(1, -90, 0, 18), Parent = r}) local valueLabel = label({ Text = "", Font = Theme.Font, TextColor3 = Theme.SubText, TextXAlignment = Enum.TextXAlignment.Right, AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -11, 0, 5), Size = UDim2.fromOffset(80, 18), Parent = r, }) local bar = create("Frame", { BackgroundColor3 = Theme.Inner, AnchorPoint = Vector2.new(0, 1), Position = UDim2.new(0, 11, 1, -10), Size = UDim2.new(1, -22, 0, IsMobile and 5 or 4), Parent = r, }) corner(bar, UDim.new(1, 0)) local fill = create("Frame", {BackgroundColor3 = Theme.Accent, Size = UDim2.fromScale(0, 1), Parent = bar}) corner(fill, UDim.new(1, 0)) local knob = create("Frame", { BackgroundColor3 = Theme.Accent, AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0, 0.5), Size = UDim2.fromOffset(IsMobile and 14 or 10, IsMobile and 14 or 10), Parent = bar, }) corner(knob, UDim.new(1, 0)) local function fmt(v) local s = decimals > 0 and string.format("%." .. decimals .. "f", v) or tostring(math.floor(v + 0.5)) return s .. suffix end local function render(instant) local a = (opt.Value - min) / math.max(max - min, 1e-9) valueLabel.Text = txt(fmt(opt.Value)) local t = instant and 0 or 0.08 tween(fill, {Size = UDim2.fromScale(a, 1)}, t) tween(knob, {Position = UDim2.fromScale(a, 0.5)}, t) end function opt:Set(v) local p = 10 ^ decimals v = math.clamp(math.floor(v * p + 0.5) / p, min, max) if v == self.Value then render() return end self.Value = v setFlag(self) render() task.spawn(self.Callback, v) end function opt:Get() return self.Value end local btn = hit(r) hover(r, btn) bindDrag(btn, function(input) local a = math.clamp((input.Position.X - bar.AbsolutePosition.X) / bar.AbsoluteSize.X, 0, 1) opt:Set(min + (max - min) * a) end) render(true) register(opt) return opt end function Section:Textbox(opts) local opt = {Type = "Textbox", Flag = opts.Flag, Value = opts.Default or "", Callback = opts.Callback or function() end} local r = row(self) local name = txt(opts.Name or "Textbox") local nameW = TextService:GetTextSize(name, TEXT, Theme.FontMedium, Vector2.new(1000, 50)).X label({Text = name, Position = UDim2.fromOffset(11, 0), Size = UDim2.new(0, nameW + 4, 1, 0), Parent = r}) local holder = create("Frame", { BackgroundColor3 = Theme.Inner, AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -6, 0.5, 0), Size = UDim2.new(1, -(nameW + 11 + 18), 1, -10), Parent = r, }) corner(holder, 5) local box = create("TextBox", { BackgroundTransparency = 1, Text = tostring(opt.Value), PlaceholderText = txt(opts.Placeholder or ""), PlaceholderColor3 = Theme.Muted, TextColor3 = Theme.Text, Font = Theme.Font, TextSize = TEXT, ClearTextOnFocus = false, TextTruncate = Enum.TextTruncate.AtEnd, Position = UDim2.fromOffset(8, 0), Size = UDim2.new(1, -16, 1, 0), Parent = holder, }) box.Focused:Connect(function() tween(holder, {BackgroundColor3 = Theme.ElementHover}, 0.15) end) box.FocusLost:Connect(function() tween(holder, {BackgroundColor3 = Theme.Inner}, 0.15) local text = box.Text if opts.Numeric then local n = tonumber(text) if not n then box.Text = tostring(opt.Value) return end text = n end opt.Value = text setFlag(opt) task.spawn(opt.Callback, text) end) function opt:Set(v) self.Value = v box.Text = tostring(v) setFlag(self) task.spawn(self.Callback, v) end function opt:Get() return self.Value end register(opt) return opt end function Section:Dropdown(opts) local multi = opts.Multi == true local options = opts.Options or {} local opt = {Type = "Dropdown", Multi = multi, Flag = opts.Flag, Callback = opts.Callback or function() end, Open = false} if multi then opt.Value = {} for _, v in ipairs(opts.Default or {}) do opt.Value[v] = true end else opt.Value = opts.Default end local OPT_H = IsMobile and 30 or 26 local r = row(self) label({Text = opts.Name or "Dropdown", Position = UDim2.fromOffset(11, 0), Size = UDim2.new(0.42, -11, 0, ROW), Parent = r}) local current = label({ Text = "", Font = Theme.Font, TextColor3 = Theme.SubText, TextXAlignment = Enum.TextXAlignment.Right, TextTruncate = Enum.TextTruncate.AtEnd, AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -30, 0, 0), Size = UDim2.new(0.58, -30, 0, ROW), Parent = r, }) local chevron = drawIcon("chevron-down", 14, Theme.SubText, r) chevron.AnchorPoint = Vector2.new(1, 0.5) chevron.Position = UDim2.new(1, -10, 0, ROW / 2) local listFrame = create("ScrollingFrame", { BackgroundTransparency = 1, BorderSizePixel = 0, Position = UDim2.fromOffset(5, ROW), Size = UDim2.new(1, -10, 0, 0), CanvasSize = UDim2.new(), ScrollBarThickness = 2, ScrollBarImageColor3 = Theme.Muted, ScrollingDirection = Enum.ScrollingDirection.Y, Visible = false, Parent = r, }) local listLayout = list(listFrame, 3) fitCanvas(listFrame, listLayout, 0, self.Scaler) local items = {} local function display() if multi then local picked = {} for _, o in ipairs(options) do if opt.Value[o] then picked[#picked + 1] = tostring(o) end end current.Text = txt(#picked > 0 and table.concat(picked, ", ") or "None") else current.Text = txt(opt.Value ~= nil and tostring(opt.Value) or "None") end for o, it in pairs(items) do local on = (multi and opt.Value[o]) or (not multi and opt.Value == o) tween(it.Frame, {BackgroundColor3 = on and Theme.Accent or Theme.Inner}, 0.15) tween(it.Label, {TextColor3 = on and Theme.AccentText or Theme.Text}, 0.15) end end local function listHeight() return math.min(#options, 6) * (OPT_H + 3) + 5 end local function setOpen(open) opt.Open = open local h = open and listHeight() or 0 if open then listFrame.Visible = true end tween(r, {Size = UDim2.new(1, 0, 0, ROW + h)}, 0.25) tween(listFrame, {Size = UDim2.new(1, -10, 0, h)}, 0.25) tween(chevron, {Rotation = open and 180 or 0}, 0.25) if not open then task.delay(0.25, function() if not opt.Open then listFrame.Visible = false end end) end end local function build() for _, it in pairs(items) do it.Frame:Destroy() end items = {} for i, o in ipairs(options) do local f = create("TextButton", { AutoButtonColor = false, Text = "", BackgroundColor3 = Theme.Inner, Size = UDim2.new(1, -4, 0, OPT_H), LayoutOrder = i, Parent = listFrame, }) corner(f, 5) local l = label({Text = tostring(o), Font = Theme.Font, Position = UDim2.fromOffset(9, 0), Size = UDim2.new(1, -18, 1, 0), Parent = f}) items[o] = {Frame = f, Label = l} f.MouseButton1Click:Connect(function() if multi then opt.Value[o] = not opt.Value[o] or nil setFlag(opt) display() task.spawn(opt.Callback, opt:Get()) else opt.Value = o setFlag(opt) display() setOpen(false) task.spawn(opt.Callback, o) end end) end display() if opt.Open then setOpen(true) end end function opt:Set(v) if multi then self.Value = {} for _, x in ipairs(type(v) == "table" and v or {v}) do self.Value[x] = true end setFlag(self) display() task.spawn(self.Callback, self:Get()) else self.Value = v setFlag(self) display() task.spawn(self.Callback, v) end end function opt:Get() if not multi then return self.Value end local out = {} for _, x in ipairs(options) do if self.Value[x] then out[#out + 1] = x end end return out end function opt:Refresh(newOptions, keep) options = newOptions or {} if not keep then self.Value = multi and {} or nil end build() end local header = hit(r, ROW) hover(r, header) header.MouseButton1Click:Connect(function() setOpen(not opt.Open) end) build() register(opt) return opt end function Section:Keybind(opts) local opt = { Type = "Keybind", Flag = opts.Flag, Value = opts.Default, Callback = opts.Callback or function() end, Changed = opts.ChangedCallback or function() end, Listening = false, } local r = row(self) label({Text = opts.Name or "Keybind", Position = UDim2.fromOffset(11, 0), Size = UDim2.new(1, -100, 1, 0), Parent = r}) local pill = create("TextButton", { AutoButtonColor = false, BackgroundColor3 = Theme.Inner, AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -6, 0.5, 0), Size = UDim2.fromOffset(44, ROW - 10), Font = Theme.FontMedium, TextSize = TEXT - 1, TextColor3 = Theme.Text, Text = "", Parent = r, }) corner(pill, 5) local function render() pill.Text = txt(opt.Listening and "..." or (opt.Value and opt.Value.Name or "None")) local w = TextService:GetTextSize(pill.Text, pill.TextSize, pill.Font, Vector2.new(200, 50)).X + 18 tween(pill, {Size = UDim2.fromOffset(math.max(40, w), ROW - 10), BackgroundColor3 = opt.Listening and Theme.ElementHover or Theme.Inner}, 0.15) end function opt:Set(v) self.Value = v setFlag(self) render() task.spawn(self.Changed, v) end function opt:Get() return self.Value end pill.MouseButton1Click:Connect(function() if opt.Listening then return end opt.Listening = true render() local c c = UserInputService.InputBegan:Connect(function(input) if input.UserInputType ~= Enum.UserInputType.Keyboard then return end c:Disconnect() task.defer(function() opt.Listening = false end) if input.KeyCode == Enum.KeyCode.Escape or input.KeyCode == Enum.KeyCode.Backspace then opt:Set(nil) else opt:Set(input.KeyCode) end end) end) render() keybinds[#keybinds + 1] = opt register(opt) return opt end function Section:ColorPicker(opts) local opt = {Type = "ColorPicker", Flag = opts.Flag, Value = opts.Default or Color3.new(1, 1, 1), Callback = opts.Callback or function() end, Open = false} local h, s, v = opt.Value:ToHSV() local r = row(self) label({Text = opts.Name or "Color", Position = UDim2.fromOffset(11, 0), Size = UDim2.new(1, -60, 0, ROW), Parent = r}) local swatch = create("Frame", { BackgroundColor3 = opt.Value, AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0, ROW / 2), Size = UDim2.fromOffset(IsMobile and 26 or 22, IsMobile and 16 or 14), Parent = r, }) corner(swatch, 4) local PICK_H = IsMobile and 116 or 100 local area = create("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(10, ROW), Size = UDim2.new(1, -20, 0, PICK_H - 10), Visible = false, Parent = r, }) local sv = create("TextButton", {AutoButtonColor = false, Text = "", BackgroundColor3 = Color3.fromHSV(h, 1, 1), Size = UDim2.new(1, -30, 1, 0), Parent = area}) corner(sv, 5) local white = create("Frame", {BackgroundColor3 = Color3.new(1, 1, 1), Size = UDim2.fromScale(1, 1), Parent = sv}) corner(white, 5) create("UIGradient", {Transparency = NumberSequence.new(0, 1), Parent = white}) local black = create("Frame", {BackgroundColor3 = Color3.new(0, 0, 0), Size = UDim2.fromScale(1, 1), Parent = sv}) corner(black, 5) create("UIGradient", {Rotation = 90, Transparency = NumberSequence.new(1, 0), Parent = black}) local svKnob = create("Frame", {BackgroundColor3 = Color3.new(1, 1, 1), AnchorPoint = Vector2.new(0.5, 0.5), Size = UDim2.fromOffset(10, 10), ZIndex = 3, Parent = sv}) corner(svKnob, UDim.new(1, 0)) local hue = create("TextButton", { AutoButtonColor = false, Text = "", BackgroundColor3 = Color3.new(1, 1, 1), AnchorPoint = Vector2.new(1, 0), Position = UDim2.fromScale(1, 0), Size = UDim2.new(0, 20, 1, 0), Parent = area, }) corner(hue, 5) local keys = {} for i = 0, 6 do keys[#keys + 1] = ColorSequenceKeypoint.new(i / 6, Color3.fromHSV(i / 6, 1, 1)) end create("UIGradient", {Rotation = 90, Color = ColorSequence.new(keys), Parent = hue}) local hueKnob = create("Frame", {BackgroundColor3 = Color3.new(1, 1, 1), AnchorPoint = Vector2.new(0.5, 0.5), Size = UDim2.new(1, 4, 0, 3), Parent = hue}) corner(hueKnob, UDim.new(1, 0)) local function render() sv.BackgroundColor3 = Color3.fromHSV(h, 1, 1) svKnob.Position = UDim2.fromScale(s, 1 - v) hueKnob.Position = UDim2.fromScale(0.5, h) swatch.BackgroundColor3 = opt.Value end local function commit() opt.Value = Color3.fromHSV(h, s, v) setFlag(opt) render() task.spawn(opt.Callback, opt.Value) end bindDrag(sv, function(input) s = math.clamp((input.Position.X - sv.AbsolutePosition.X) / sv.AbsoluteSize.X, 0, 1) v = 1 - math.clamp((input.Position.Y - sv.AbsolutePosition.Y) / sv.AbsoluteSize.Y, 0, 1) commit() end) bindDrag(hue, function(input) h = math.clamp((input.Position.Y - hue.AbsolutePosition.Y) / hue.AbsoluteSize.Y, 0, 1) commit() end) function opt:Set(c) h, s, v = c:ToHSV() commit() end function opt:Get() return self.Value end local header = hit(r, ROW) hover(r, header) header.MouseButton1Click:Connect(function() opt.Open = not opt.Open if opt.Open then area.Visible = true end tween(r, {Size = UDim2.new(1, 0, 0, ROW + (opt.Open and PICK_H or 0))}, 0.25) if not opt.Open then task.delay(0.25, function() if not opt.Open then area.Visible = false end end) end end) render() register(opt) return opt end -- window local Window = {} Window.__index = Window local Tab = {} Tab.__index = Tab function Wave:CreateWindow(opts) opts = opts or {} setDevice(opts.Device) if opts.Lowercase ~= nil then Wave.Lowercase = opts.Lowercase end local self = setmetatable({}, Window) self.Tabs = {} self.Minimized = false self.Keybind = opts.Keybind or Enum.KeyCode.RightShift local logoSrc = opts.Logo or Wave.Logo local title = opts.Title or "Wave" local W = IsMobile and 520 or 620 local H = IsMobile and 310 or 400 local SIDEBAR = IsMobile and 130 or 160 local TOP = 48 if opts.Size then W, H = opts.Size.X.Offset, opts.Size.Y.Offset end local view = viewSize() -- root local root = create("Frame", { Name = "Window", BackgroundColor3 = Theme.Background, AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromOffset(view.X / 2, view.Y / 2), Size = UDim2.fromOffset(W, H), Visible = false, Parent = Screen, }) corner(root, 12) stroke(root, 0.3) self.Root = root self.Scale = addScaler(root) -- sidebar local sidebar = create("Frame", {BackgroundColor3 = Theme.Sidebar, Size = UDim2.new(0, SIDEBAR, 1, 0), Parent = root}) corner(sidebar, 12) create("Frame", { -- hide right corners BackgroundColor3 = Theme.Sidebar, BorderSizePixel = 0, AnchorPoint = Vector2.new(1, 0), Position = UDim2.fromScale(1, 0), Size = UDim2.new(0, 12, 1, 0), Parent = sidebar, }) -- brand (draggable) local brand = create("Frame", {BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, TOP + 4), Parent = sidebar}) local lg = logo(22, Theme.Text, brand, logoSrc) lg.AnchorPoint = Vector2.new(0, 0.5) lg.Position = UDim2.new(0, 14, 0.5, 0) label({Text = title, Font = Theme.FontBold, TextSize = 14, Position = UDim2.fromOffset(44, 0), Size = UDim2.new(1, -52, 1, 0), TextTruncate = Enum.TextTruncate.AtEnd, Parent = brand}) local tabList = create("ScrollingFrame", { BackgroundTransparency = 1, BorderSizePixel = 0, Position = UDim2.fromOffset(8, TOP + 8), Size = UDim2.new(1, -16, 1, -(TOP + 16)), CanvasSize = UDim2.new(), ScrollBarThickness = 0, ScrollingDirection = Enum.ScrollingDirection.Y, Parent = sidebar, }) local tabLayout = list(tabList, 3) fitCanvas(tabList, tabLayout, 0, self.Scale) self.TabList = tabList -- topbar (draggable) local topbar = create("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(SIDEBAR, 0), Size = UDim2.new(1, -SIDEBAR, 0, TOP), Parent = root, }) self.PageTitle = label({Text = "", Font = Theme.FontBold, TextSize = 14, Position = UDim2.fromOffset(16, 0), Size = UDim2.new(1, -100, 1, 0), Parent = topbar}) local function control(icon, order, onClick) local size = IsMobile and 30 or 26 local b = create("TextButton", { AutoButtonColor = false, Text = "", BackgroundColor3 = Theme.Element, BackgroundTransparency = 1, AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10 - (order - 1) * (size + 4), 0.5, 0), Size = UDim2.fromOffset(size, size), Parent = topbar, }) corner(b, 6) local ic, setColor = drawIcon(icon, 14, Theme.SubText, b) ic.AnchorPoint = Vector2.new(0.5, 0.5) ic.Position = UDim2.fromScale(0.5, 0.5) if not IsMobile then b.MouseEnter:Connect(function() tween(b, {BackgroundTransparency = 0}, 0.12) setColor(Theme.Text) end) b.MouseLeave:Connect(function() tween(b, {BackgroundTransparency = 1}, 0.12) setColor(Theme.SubText) end) end b.MouseButton1Click:Connect(onClick) end control("x", 1, function() self:Destroy() end) control("minus", 2, function() self:Minimize() end) -- pages local pages = create("Frame", { BackgroundTransparency = 1, ClipsDescendants = true, Position = UDim2.fromOffset(SIDEBAR, TOP), Size = UDim2.new(1, -SIDEBAR, 1, -TOP), Parent = root, }) self.Pages = pages -- drag handles makeDraggable(brand, root) makeDraggable(topbar, root) -- minimized pill: grip | logo Open local pillH = IsMobile and 38 or 34 local pillText = "Open " .. title local textW = TextService:GetTextSize(pillText, TEXT, Theme.FontMedium, Vector2.new(400, 40)).X local pill = create("Frame", { Name = "Pill", BackgroundColor3 = Theme.Background, AnchorPoint = Vector2.new(0.5, 0), Position = UDim2.fromOffset(view.X / 2, -60), Size = UDim2.fromOffset(62 + textW + 16, pillH), Visible = false, Parent = Screen, }) corner(pill, UDim.new(1, 0)) stroke(pill, 0.3) addScaler(pill) local grip = drawIcon("grip-vertical", 14, Theme.Muted, pill) grip.AnchorPoint = Vector2.new(0, 0.5) grip.Position = UDim2.new(0, 12, 0.5, 0) local pl = logo(18, Theme.Text, pill, logoSrc) pl.AnchorPoint = Vector2.new(0, 0.5) pl.Position = UDim2.new(0, 36, 0.5, 0) label({Text = pillText, Position = UDim2.fromOffset(62, 0), Size = UDim2.new(0, textW + 4, 1, 0), Parent = pill}) local pillBtn = hit(pill) pillBtn.ZIndex = 5 makeDraggable(pillBtn, pill, function() self:Restore() end) self.Pill = pill -- mobile toggle button (rounded square logo) if IsMobile or opts.ShowMobileButton then local mb = create("Frame", { Name = "MobileButton", BackgroundColor3 = Theme.Background, Position = UDim2.fromOffset(16, math.floor(view.Y * 0.3)), Size = UDim2.fromOffset(46, 46), Parent = Screen, }) corner(mb, 12) stroke(mb, 0.3) addScaler(mb) local ml = logo(28, Theme.Text, mb, logoSrc) ml.AnchorPoint = Vector2.new(0.5, 0.5) ml.Position = UDim2.fromScale(0.5, 0.5) local mbBtn = hit(mb) makeDraggable(mbBtn, mb, function() if self.Minimized then self:Restore() else self:Minimize() end end) self.MobileButton = mb end -- pc toggle key self._keyConn = UserInputService.InputBegan:Connect(function(input, processed) if processed or input.KeyCode ~= self.Keybind then return end if self.Minimized then self:Restore() else self:Minimize() end end) -- open anim (deferred so tabs made right after are in place first) task.defer(function() self:_popIn() end) return self end function Window:_popIn() local base = computeScale() self.Scale:SetAttribute("Animating", true) self.Scale.Scale = base * 0.96 self.Root.Visible = true tween(self.Scale, {Scale = base}, 0.3) task.delay(0.3, function() self.Scale:SetAttribute("Animating", false) end) end function Window:Minimize() if self.Minimized or self._anim then return end self.Minimized, self._anim = true, true local base = computeScale() self.Scale:SetAttribute("Animating", true) tween(self.Scale, {Scale = base * 0.96}, 0.18) task.delay(0.18, function() self.Root.Visible = false self.Scale.Scale = base self.Scale:SetAttribute("Animating", false) self._anim = false end) -- pill in local view = viewSize() local x, y = self.PillX or view.X / 2, self.PillY or 12 self.Pill.Visible = true self.Pill.Position = UDim2.fromOffset(x, -60) tween(self.Pill, {Position = UDim2.fromOffset(x, y)}, 0.35) end function Window:Restore() if not self.Minimized or self._anim then return end self.Minimized, self._anim = false, true -- keep pill pos local px, py = getPos(self.Pill) self.PillX, self.PillY = px, math.max(py, 6) tween(self.Pill, {Position = UDim2.fromOffset(self.PillX, -60)}, 0.25) task.delay(0.25, function() if not self.Minimized then self.Pill.Visible = false end end) self:_popIn() task.delay(0.3, function() self._anim = false end) end function Window:Destroy() if self._keyConn then self._keyConn:Disconnect() end tween(self.Scale, {Scale = self.Scale.Scale * 0.96}, 0.15) task.delay(0.15, function() self.Root:Destroy() self.Pill:Destroy() if self.MobileButton then self.MobileButton:Destroy() end end) end function Window:Notify(o) return Wave:Notify(o) end function Window:Tab(opts) opts = type(opts) == "string" and {Name = opts} or (opts or {}) local tab = setmetatable({Window = self, Name = opts.Name or "Tab"}, Tab) -- tab button local btn = create("TextButton", { AutoButtonColor = false, Text = "", BackgroundColor3 = Theme.Accent, BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, IsMobile and 34 or 32), LayoutOrder = #self.Tabs + 1, Parent = self.TabList, }) corner(btn, 7) local textX = 10 local setIconColor = function() end if opts.Icon then local ic ic, setIconColor = drawIcon(opts.Icon, 15, Theme.SubText, btn) ic.AnchorPoint = Vector2.new(0, 0.5) ic.Position = UDim2.new(0, 9, 0.5, 0) textX = 31 end local text = label({Text = tab.Name, TextColor3 = Theme.SubText, Position = UDim2.fromOffset(textX, 0), Size = UDim2.new(1, -textX - 6, 1, 0), TextTruncate = Enum.TextTruncate.AtEnd, Parent = btn}) -- page: one column, sections stacked local page = create("ScrollingFrame", { BackgroundTransparency = 1, BorderSizePixel = 0, Size = UDim2.fromScale(1, 1), CanvasSize = UDim2.new(), ScrollBarThickness = 2, ScrollBarImageColor3 = Theme.Muted, ScrollingDirection = Enum.ScrollingDirection.Y, Visible = false, Parent = self.Pages, }) local PAD = 12 local column = create("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(PAD, 2), Size = UDim2.new(1, -PAD * 2, 0, 0), Parent = page, }) local columnLayout = list(column, 8) fitCanvas(page, columnLayout, PAD + 2, self.Scale) tab.Column = column tab.Button, tab.Label, tab.Page, tab.SetIconColor = btn, text, page, setIconColor btn.MouseButton1Click:Connect(function() self:SelectTab(tab) end) if not IsMobile then btn.MouseEnter:Connect(function() if self.Current ~= tab then tween(text, {TextColor3 = Theme.Text}, 0.12) end end) btn.MouseLeave:Connect(function() if self.Current ~= tab then tween(text, {TextColor3 = Theme.SubText}, 0.12) end end) end table.insert(self.Tabs, tab) if #self.Tabs == 1 then self:SelectTab(tab) end return tab end function Window:SelectTab(tab) if self.Current == tab then return end local prev = self.Current self.Current = tab if prev then prev.Page.Visible = false tween(prev.Button, {BackgroundTransparency = 1}, 0.15) tween(prev.Label, {TextColor3 = Theme.SubText}, 0.15) prev.SetIconColor(Theme.SubText) end tween(tab.Button, {BackgroundTransparency = 0}, 0.15) tween(tab.Label, {TextColor3 = Theme.AccentText}, 0.15) tab.SetIconColor(Theme.AccentText) self.PageTitle.Text = txt(tab.Name) -- page in tab.Page.CanvasPosition = Vector2.zero tab.Page.Visible = true tab.Page.Position = UDim2.fromOffset(0, 8) tween(tab.Page, {Position = UDim2.fromOffset(0, 0)}, 0.25) end function Tab:Section(opts) opts = type(opts) == "string" and {Name = opts} or (opts or {}) local scaler = self.Window.Scale local sec = setmetatable({_n = 0, Scaler = scaler, Open = opts.Opened ~= false}, Section) local HEAD = ROW + 2 local frame = create("Frame", { BackgroundColor3 = Theme.Surface, Size = UDim2.new(1, 0, 0, HEAD), ClipsDescendants = true, LayoutOrder = #self.Column:GetChildren(), Parent = self.Column, }) corner(frame, 10) label({ Text = opts.Name or "Section", Font = Theme.FontBold, Position = UDim2.fromOffset(12, 0), Size = UDim2.new(1, -44, 0, HEAD), Parent = frame, }) local arrow = drawIcon("chevron-down", 14, Theme.SubText, frame) arrow.AnchorPoint = Vector2.new(1, 0.5) arrow.Position = UDim2.new(1, -12, 0, HEAD / 2) arrow.Rotation = sec.Open and 180 or 0 local body = create("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(6, HEAD), Size = UDim2.new(1, -12, 0, 0), Parent = frame, }) local layout = list(body, 5) local animating = false local function resize(animate) local s = scaler.Scale if s <= 0 then return end local ch = math.ceil(layout.AbsoluteContentSize.Y / s) body.Size = UDim2.new(1, -12, 0, ch) local goal = UDim2.new(1, 0, 0, HEAD + (sec.Open and ch + 6 or 0)) if animate then tween(frame, {Size = goal}, 0.25) else frame.Size = goal end end layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() if not animating then resize(false) end end) local header = hit(frame, HEAD) header.MouseButton1Click:Connect(function() sec.Open = not sec.Open animating = true tween(arrow, {Rotation = sec.Open and 180 or 0}, 0.25) resize(true) task.delay(0.26, function() animating = false resize(false) end) end) sec.Container = body sec.Frame = frame return sec end return Wave