--[[ Ball Controller - Modern Draggable GUI Place this single LocalScript in StarterPlayerScripts or StarterGui ]] local Players = game:GetService("Players") local RunService = game:GetService("RunService") local UIS = game:GetService("UserInputService") local CoreGui = game:GetService("CoreGui") local LocalPlayer = Players.LocalPlayer local Mouse = LocalPlayer:GetMouse() -- ========================================== -- STATE & CONFIGURATION -- ========================================== local State = { Balls = {}, OriginalCFrames = {}, SelectedBall = nil, Mode = "Idle", ActiveToggles = {}, Connections = {}, SearchText = "", } local Config = { TpSpeed = 1, UpdateInterval = 0, OrbitSpeed = 2, OrbitRadius = 8, FollowDistance = 3, StackOffset = 1, Smoothness = 0.15, Hotkeys = { BringSelected = Enum.KeyCode.B, BringAll = Enum.KeyCode.N, Stack = Enum.KeyCode.M, Orbit = Enum.KeyCode.O, Refresh = Enum.KeyCode.R, Close = Enum.KeyCode.KeypadMinus } } -- ========================================== -- UTILITY FUNCTIONS -- ========================================== local function ExtractNumber(name) local num = string.match(name, "%d+") return num and tonumber(num) or 0 end local function GetBallsFolder() return workspace:FindFirstChild("Balls") end local function GetValidBalls() local folder = GetBallsFolder() if not folder then return {} end local balls = {} for _, child in ipairs(folder:GetChildren()) do if child:IsA("BasePart") then table.insert(balls, child) if not State.OriginalCFrames[child] then State.OriginalCFrames[child] = child.CFrame end end end table.sort(balls, function(a, b) return ExtractNumber(a.Name) < ExtractNumber(b.Name) end) return balls end local function MovePart(part, targetCFrame, instant) if not part or not part.Parent then return end pcall(function() if instant or Config.Smoothness <= 0 then part.CFrame = targetCFrame else part.CFrame = part.CFrame:Lerp(targetCFrame, Config.Smoothness) end part.Velocity = Vector3.new(0,0,0) part.RotVelocity = Vector3.new(0,0,0) end) end -- ========================================== -- GUI CREATION FRAMEWORK -- ========================================== local function Create(className, properties, children) local inst = Instance.new(className) for k, v in pairs(properties or {}) do inst[k] = v end for _, child in ipairs(children or {}) do child.Parent = inst end return inst end local function MakeCorner(radius) return Create("UICorner", {CornerRadius = UDim.new(0, radius)}) end local function MakeStroke(color, thickness) return Create("UIStroke", {Color = color, Thickness = thickness or 1, ApplyStrokeMode = Enum.ApplyStrokeMode.Border}) end -- ScreenGui Setup local oldGui = CoreGui:FindFirstChild("BallControllerGUI") or LocalPlayer.PlayerGui:FindFirstChild("BallControllerGUI") if oldGui then oldGui:Destroy() end local ScreenGui = Create("ScreenGui", { Name = "BallControllerGUI", ResetOnSpawn = false, IgnoreGuiInset = true, ZIndexBehavior = Enum.ZIndexBehavior.Sibling }) pcall(function() ScreenGui.Parent = CoreGui end) if ScreenGui.Parent == nil then ScreenGui.Parent = LocalPlayer:WaitForChild("PlayerGui") end local Theme = { BG = Color3.fromRGB(20, 20, 25), Panel = Color3.fromRGB(30, 30, 35), Accent = Color3.fromRGB(80, 120, 255), Text = Color3.fromRGB(240, 240, 240), Danger = Color3.fromRGB(255, 60, 60), Border = Color3.fromRGB(50, 50, 60) } -- Selection Highlight Instance local SelectionHighlight = Create("Highlight", { FillColor = Theme.Accent, OutlineColor = Color3.new(1, 1, 1), FillTransparency = 0.5, OutlineTransparency = 0, Parent = ScreenGui -- Parented to GUI so it deletes if GUI is closed }) local MainFrame = Create("Frame", { Size = UDim2.new(0, 600, 0, 450), Position = UDim2.new(0.5, -300, 0.5, -225), BackgroundColor3 = Theme.BG, Active = true, }, { MakeCorner(8), MakeStroke(Theme.Border) }) MainFrame.Parent = ScreenGui -- Draggable Logic local dragging, dragInput, dragStart, startPos MainFrame.InputBegan:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then dragging, dragStart, startPos = true, input.Position, MainFrame.Position input.Changed:Connect(function() if input.UserInputState == Enum.UserInputState.End then dragging = false end end) end end) MainFrame.InputChanged:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then dragInput = input end end) RunService.Heartbeat:Connect(function() if dragging and dragInput then local delta = dragInput.Position - dragStart MainFrame.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y) end end) -- Top Bar local TopBar = Create("Frame", {Size = UDim2.new(1, 0, 0, 30), BackgroundColor3 = Theme.Panel, BorderSizePixel = 0, Parent = MainFrame}, {MakeCorner(8)}) Create("Frame", {Size = UDim2.new(1,0,0,8), Position = UDim2.new(0,0,1,-8), BackgroundColor3 = Theme.Panel, BorderSizePixel = 0, Parent = TopBar}) -- Fill bottom corners local Title = Create("TextLabel", {Size = UDim2.new(1, -100, 1, 0), Position = UDim2.new(0, 10, 0, 0), BackgroundTransparency = 1, Text = "Ball Controller", TextColor3 = Theme.Text, Font = Enum.Font.GothamBold, TextSize = 14, TextXAlignment = Enum.TextXAlignment.Left, Parent = TopBar}) local CloseBtn = Create("TextButton", {Size = UDim2.new(0, 30, 0, 30), Position = UDim2.new(1, -30, 0, 0), BackgroundTransparency = 1, Text = "X", TextColor3 = Theme.Danger, Font = Enum.Font.GothamBold, TextSize = 14, Parent = TopBar}) local MinBtn = Create("TextButton", {Size = UDim2.new(0, 30, 0, 30), Position = UDim2.new(1, -60, 0, 0), BackgroundTransparency = 1, Text = "-", TextColor3 = Theme.Text, Font = Enum.Font.GothamBold, TextSize = 14, Parent = TopBar}) CloseBtn.MouseButton1Click:Connect(function() ScreenGui:Destroy() end) MinBtn.MouseButton1Click:Connect(function() local min = MainFrame.Size.Y.Offset == 30 MainFrame.Size = UDim2.new(0, 600, 0, min and 450 or 30) for _, child in ipairs(MainFrame:GetChildren()) do if child ~= TopBar and child:IsA("GuiObject") then child.Visible = min end end end) -- Layout Containers local LeftPanel = Create("Frame", {Size = UDim2.new(0, 200, 1, -40), Position = UDim2.new(0, 10, 0, 40), BackgroundColor3 = Theme.Panel, Parent = MainFrame}, {MakeCorner(6), MakeStroke(Theme.Border)}) local RightPanel = Create("ScrollingFrame", {Size = UDim2.new(1, -230, 1, -40), Position = UDim2.new(0, 220, 0, 40), BackgroundTransparency = 1, CanvasSize = UDim2.new(0,0,0,650), ScrollBarThickness = 4, Parent = MainFrame}) Create("UIListLayout", {Padding = UDim.new(0, 10), Parent = RightPanel}) -- Left Panel: List & Search local SearchBar = Create("TextBox", {Size = UDim2.new(1, -20, 0, 25), Position = UDim2.new(0, 10, 0, 10), BackgroundColor3 = Theme.BG, TextColor3 = Theme.Text, PlaceholderText = "Search...", Font = Enum.Font.Gotham, TextSize = 12, Parent = LeftPanel}, {MakeCorner(4), MakeStroke(Theme.Border)}) local BallList = Create("ScrollingFrame", {Size = UDim2.new(1, -20, 1, -80), Position = UDim2.new(0, 10, 0, 45), BackgroundTransparency = 1, CanvasSize = UDim2.new(0,0,0,0), ScrollBarThickness = 4, Parent = LeftPanel}) local ListLayout = Create("UIListLayout", {Padding = UDim.new(0, 5), Parent = BallList}) local RefreshBtn = Create("TextButton", {Size = UDim2.new(1, -20, 0, 25), Position = UDim2.new(0, 10, 1, -30), BackgroundColor3 = Theme.Accent, TextColor3 = Theme.Text, Text = "Refresh List", Font = Enum.Font.GothamBold, TextSize = 12, Parent = LeftPanel}, {MakeCorner(4)}) -- Factory for UI Sections local function CreateSection(title, parent) local Frame = Create("Frame", {Size = UDim2.new(1, -10, 0, 0), BackgroundColor3 = Theme.Panel, AutomaticSize = Enum.AutomaticSize.Y, Parent = parent}, {MakeCorner(6), MakeStroke(Theme.Border)}) Create("TextLabel", {Size = UDim2.new(1, -10, 0, 20), Position = UDim2.new(0, 5, 0, 5), BackgroundTransparency = 1, Text = title, TextColor3 = Theme.Accent, Font = Enum.Font.GothamBold, TextSize = 12, TextXAlignment = Enum.TextXAlignment.Left, Parent = Frame}) local Content = Create("Frame", {Size = UDim2.new(1, -10, 0, 0), Position = UDim2.new(0, 5, 0, 25), BackgroundTransparency = 1, AutomaticSize = Enum.AutomaticSize.Y, Parent = Frame}) Create("UIGridLayout", {CellSize = UDim2.new(0.48, 0, 0, 25), CellPadding = UDim2.new(0.04, 0, 0, 5), Parent = Content}) Create("UIPadding", {PaddingBottom = UDim.new(0, 5), Parent = Frame}) return Content end local function CreateButton(text, parent, callback) local btn = Create("TextButton", {BackgroundColor3 = Theme.BG, TextColor3 = Theme.Text, Text = text, Font = Enum.Font.Gotham, TextSize = 11, Parent = parent}, {MakeCorner(4), MakeStroke(Theme.Border)}) btn.MouseButton1Click:Connect(callback) return btn end local function CreateToggle(text, configKey, parent) local btn = Create("TextButton", {BackgroundColor3 = Theme.BG, TextColor3 = Theme.Text, Text = text, Font = Enum.Font.Gotham, TextSize = 11, Parent = parent}, {MakeCorner(4), MakeStroke(Theme.Border)}) btn.MouseButton1Click:Connect(function() State.ActiveToggles[configKey] = not State.ActiveToggles[configKey] btn.BackgroundColor3 = State.ActiveToggles[configKey] and Theme.Accent or Theme.BG State.Mode = State.ActiveToggles[configKey] and configKey or "Idle" UpdateStatus() end) return btn end -- Sections local StatusLabel = Create("TextLabel", {Size = UDim2.new(1, -10, 0, 20), BackgroundTransparency = 1, Text = "Balls: 0 | Selected: None | Mode: Idle", TextColor3 = Theme.Text, Font = Enum.Font.GothamBold, TextSize = 12, Parent = RightPanel}) local function UpdateStatus() StatusLabel.Text = string.format("Balls: %d | Selected: %s | Mode: %s", #State.Balls, State.SelectedBall and State.SelectedBall.Name or "None", State.Mode) end local SelectedSec = CreateSection("Selected Ball Controls", RightPanel) CreateButton("TP to Player", SelectedSec, function() if State.SelectedBall and LocalPlayer.Character then MovePart(State.SelectedBall, LocalPlayer.Character.PrimaryPart.CFrame, true) end end) CreateButton("TP to Mouse", SelectedSec, function() if State.SelectedBall then MovePart(State.SelectedBall, CFrame.new(Mouse.Hit.Position), true) end end) CreateButton("Return Original", SelectedSec, function() if State.SelectedBall and State.OriginalCFrames[State.SelectedBall] then MovePart(State.SelectedBall, State.OriginalCFrames[State.SelectedBall], true) end end) CreateButton("Bring Front", SelectedSec, function() if State.SelectedBall and LocalPlayer.Character then MovePart(State.SelectedBall, LocalPlayer.Character.PrimaryPart.CFrame * CFrame.new(0,0,-5), true) end end) local MassSec = CreateSection("Mass Controls (Formations)", RightPanel) local Formations = { {"TP All Player", function(i, n, hrp) return hrp.CFrame end}, {"Stack All", function(i, n, hrp) return hrp.CFrame * CFrame.new(0, 5, 0) end}, {"Circle", function(i, n, hrp) return hrp.CFrame * CFrame.Angles(0, math.rad((360/n)*i), 0) * CFrame.new(0, 0, -Config.OrbitRadius) end}, {"Line", function(i, n, hrp) return hrp.CFrame * CFrame.new((i - n/2) * Config.StackOffset, 0, -5) end}, {"Grid", function(i, n, hrp) local cols = math.ceil(math.sqrt(n)); local row = math.floor((i-1)/cols); local col = (i-1)%cols; return hrp.CFrame * CFrame.new((col - cols/2)*3, 0, (row - cols/2)*3 - 5) end}, {"Tower", function(i, n, hrp) return hrp.CFrame * CFrame.new(0, i * Config.StackOffset, -5) end}, {"Wall", function(i, n, hrp) local cols = math.ceil(math.sqrt(n)); local row = math.floor((i-1)/cols); local col = (i-1)%cols; return hrp.CFrame * CFrame.new((col - cols/2)*3, row * 3, -8) end}, {"Reset All", function(i, n, hrp, ball) return State.OriginalCFrames[ball] or ball.CFrame end} } -- Merge Identical button CreateButton("Merge Identical", MassSec, function() State.Mode = "Merging Identical Balls" local grouped = {} -- Group balls by name for _, ball in ipairs(State.Balls) do if ball and ball.Parent then if not grouped[ball.Name] then grouped[ball.Name] = {} end table.insert(grouped[ball.Name], ball) end end -- Teleport identically named balls to the first one in the group for name, group in pairs(grouped) do if #group > 1 then local targetCF = group[1].CFrame for i = 2, #group do MovePart(group[i], targetCF, true) end end end UpdateStatus() end) for _, form in ipairs(Formations) do CreateButton(form[1], MassSec, function() State.Mode = form[1] local hrp = LocalPlayer.Character and LocalPlayer.Character:FindFirstChild("HumanoidRootPart") if not hrp and form[1] ~= "Reset All" then return end for i, ball in ipairs(State.Balls) do MovePart(ball, form[2](i, #State.Balls, hrp, ball), true) end UpdateStatus() end) end local ModesSec = CreateSection("Continuous / Auto Modes", RightPanel) local ContinuousModes = {"Auto Bring", "Auto Stack", "Orbit Mode", "Train Mode", "Snake Mode", "Auto Follow Mouse"} for _, mode in ipairs(ContinuousModes) do CreateToggle(mode, mode, ModesSec) end -- ========================================== -- LOGIC & UPDATES -- ========================================== local function PopulateList() for _, child in ipairs(BallList:GetChildren()) do if child:IsA("TextButton") then child:Destroy() end end State.Balls = GetValidBalls() -- If selected ball was deleted, remove adornee if State.SelectedBall and not State.SelectedBall.Parent then State.SelectedBall = nil SelectionHighlight.Adornee = nil end for _, ball in ipairs(State.Balls) do local btn = Create("TextButton", {Size = UDim2.new(1, 0, 0, 25), BackgroundColor3 = Theme.BG, TextColor3 = Theme.Text, Text = ball.Name, Font = Enum.Font.Gotham, TextSize = 12, Parent = BallList}, {MakeCorner(4), MakeStroke(Theme.Border)}) btn.MouseButton1Click:Connect(function() State.SelectedBall = ball SelectionHighlight.Adornee = ball -- Highlights the ball in-game for _, b in ipairs(BallList:GetChildren()) do if b:IsA("TextButton") then b.BackgroundColor3 = Theme.BG end end btn.BackgroundColor3 = Theme.Accent UpdateStatus() end) end BallList.CanvasSize = UDim2.new(0, 0, 0, #State.Balls * 30) UpdateStatus() end SearchBar:GetPropertyChangedSignal("Text"):Connect(function() local filter = string.lower(SearchBar.Text) for _, btn in ipairs(BallList:GetChildren()) do if btn:IsA("TextButton") then btn.Visible = filter == "" or string.find(string.lower(btn.Text), filter) ~= nil end end end) RefreshBtn.MouseButton1Click:Connect(PopulateList) -- Auto Update Connections task.spawn(function() while task.wait(1) do local folder = GetBallsFolder() if folder and not State.Connections.FolderAdded then State.Connections.FolderAdded = folder.ChildAdded:Connect(PopulateList) State.Connections.FolderRemoved = folder.ChildRemoved:Connect(PopulateList) PopulateList() end end end) -- Main Render/Heartbeat Loop for Modes RunService.Heartbeat:Connect(function() local hrp = LocalPlayer.Character and LocalPlayer.Character:FindFirstChild("HumanoidRootPart") if not hrp then return end local timeT = tick() local n = #State.Balls if State.ActiveToggles["Orbit Mode"] then for i, ball in ipairs(State.Balls) do local angle = (timeT * Config.OrbitSpeed) + (math.rad(360/n) * i) local target = hrp.CFrame * CFrame.new(math.cos(angle) * Config.OrbitRadius, 0, math.sin(angle) * Config.OrbitRadius) MovePart(ball, target, false) end elseif State.ActiveToggles["Train Mode"] or State.ActiveToggles["Snake Mode"] then local prevTarget = hrp.CFrame * CFrame.new(0,0, -Config.FollowDistance) for i, ball in ipairs(State.Balls) do local offset = State.ActiveToggles["Snake Mode"] and CFrame.new(math.sin(timeT * 3 + i) * 2, 0, 0) or CFrame.new(0,0,0) MovePart(ball, prevTarget * offset, false) prevTarget = ball.CFrame * CFrame.new(0,0, -Config.FollowDistance) end elseif State.ActiveToggles["Auto Bring"] then for i, ball in ipairs(State.Balls) do MovePart(ball, hrp.CFrame * CFrame.new(0,0,-3), false) end elseif State.ActiveToggles["Auto Stack"] then for i, ball in ipairs(State.Balls) do MovePart(ball, hrp.CFrame * CFrame.new(0, 5 + (i*0.1), 0), false) end elseif State.ActiveToggles["Auto Follow Mouse"] then for i, ball in ipairs(State.Balls) do MovePart(ball, CFrame.new(Mouse.Hit.Position) * CFrame.new(0, i*0.5, 0), false) end end end) -- Hotkeys UIS.InputBegan:Connect(function(input, gameProcessed) if gameProcessed then return end if input.KeyCode == Config.Hotkeys.Close then ScreenGui.Enabled = not ScreenGui.Enabled end if input.KeyCode == Config.Hotkeys.BringSelected and State.SelectedBall and LocalPlayer.Character then MovePart(State.SelectedBall, LocalPlayer.Character.PrimaryPart.CFrame, true) end if input.KeyCode == Config.Hotkeys.Refresh then PopulateList() end end) -- Initial Load PopulateList()