-- Blox League ball tracer: thin line showing where the ball will go, including bounces. -- Press + (numpad + or = key) to toggle. if getgenv().BallTracerCleanup then pcall(getgenv().BallTracerCleanup) end -- remove a previous run local RunService = game:GetService("RunService") local UIS = game:GetService("UserInputService") local cam = workspace.CurrentCamera local CFG = { Enabled = true, Color = Color3.fromRGB(130, 205, 255), Thickness = 2, SimStep = 1 / 40, -- seconds per drawn segment MaxTime = 3, -- how far ahead to predict MaxBounces = 3, Gravity = 34, -- measured from the real ball's flights (world gravity is 196, ball is floaty) FloorBounce = 0.65, WallBounce = 0.6, CornerRadius = 10, -- radius of the curved floor/wall/ceiling corners GoalHalfWidth = 40, -- rough goal opening, estimated from the goal model GoalHeight = 30, GoalDepth = 300, MinSpeed = 6, -- hide the line when the ball is basically still Window = 0.15, -- seconds of ball history used to estimate velocity (bigger = steadier, slower to react) GroundY = 5.6, -- ball centre below this = rolling on the floor (no gravity in the speed fit) HitThreshold = 8, -- studs of surprise movement that count as a hit/bounce and reset the estimate } local MAX_PTS = math.ceil(CFG.MaxTime / CFG.SimStep) + 2 -- Drawn with GUI frames (the Drawing library does not render on every setup) local gui = Instance.new("ScreenGui") gui.Name = "BallTracer" gui.IgnoreGuiInset = true gui.ResetOnSpawn = false gui.DisplayOrder = 999 gui.Parent = (gethui and gethui()) or game:GetService("CoreGui") local lines = {} for i = 1, MAX_PTS do local f = Instance.new("Frame") f.AnchorPoint = Vector2.new(0.5, 0.5) f.BorderSizePixel = 0 f.Visible = false f.Parent = gui lines[i] = f end local conns = {} getgenv().BallTracerCleanup = function() for _, c in ipairs(conns) do c:Disconnect() end gui:Destroy() getgenv().BallTracerCleanup = nil end local function hideFrom(n) for i = n, MAX_PTS do lines[i].Visible = false end end local function setLine(f, a, b) local d = b - a local len = d.Magnitude f.Position = UDim2.fromOffset((a.X + b.X) / 2, (a.Y + b.Y) / 2) f.Size = UDim2.fromOffset(len + 1, CFG.Thickness) -- +1 hides hairline gaps between segments f.Rotation = math.deg(math.atan2(d.Y, d.X)) f.BackgroundColor3 = CFG.Color f.Visible = true end local BOX = { min = Vector3.new(-205.8, 0, -257), max = Vector3.new(205.8, 103.7, 257) } -- Ball velocity: the ball is anchored (physics velocity reads 0), so estimate it from position history. -- Least-squares line through the last CFG.Window seconds after removing gravity's curve, which averages -- out frame-time noise. A sudden unexpected jump (hit / bounce / reset) clears the history so it reacts fast. local hist = {} local vel = Vector3.zero local base = nil -- smoothed ball position at "now" local function updateState(ball, now) local p = ball.Position local grounded = p.Y < CFG.GroundY local G = Vector3.new(0, grounded and 0 or -CFG.Gravity, 0) local fresh = false local last = hist[#hist] if last then local dt = now - last.t if dt <= 0 then return end local pred = last.p + vel * dt + G * (0.5 * dt * dt) if (p - pred).Magnitude > CFG.HitThreshold or (p - last.p).Magnitude > 60 then hist = {} fresh = true end end hist[#hist + 1] = { t = now, p = p } while #hist > 2 and now - hist[1].t > CFG.Window do table.remove(hist, 1) end base = p local n = #hist if n < 2 or now - hist[1].t < 0.015 then return end local sumT, sumP = 0, Vector3.zero local pcs = table.create(n) local taus = table.create(n) for i, h in ipairs(hist) do local tau = h.t - now local pc = h.p - G * (0.5 * tau * tau) -- remove gravity so the rest is a straight line taus[i], pcs[i] = tau, pc sumT += tau sumP += pc end local tm, pm = sumT / n, sumP / n local sxx, sxy = 0, Vector3.zero for i = 1, n do local dtau = taus[i] - tm sxx += dtau * dtau sxy += (pcs[i] - pm) * dtau end if sxx < 1e-9 then return end local newVel = sxy / sxx vel = (fresh or n <= 3) and newVel or vel:Lerp(newVel, 0.5) base = pm - newVel * tm -- fitted position at "now" (steadier than the raw one) end local function project(p) local v = cam:WorldToViewportPoint(p) return Vector2.new(v.X, v.Y), v.Z end local pts = table.create(MAX_PTS) local function step(now) local bodies = workspace:FindFirstChild("Bodies") local ball = bodies and bodies:FindFirstChild("Ball") if not CFG.Enabled or not ball then hideFrom(1) hist = {} return end cam = workspace.CurrentCamera updateState(ball, now) local radius = math.max(ball.Size.X, ball.Size.Y, ball.Size.Z) / 2 local p, v = base or ball.Position, vel if v.Magnitude < CFG.MinSpeed then hideFrom(1) return end local g = Vector3.new(0, -CFG.Gravity, 0) local R = CFG.CornerRadius local bounces, count, t = 0, 1, 0 pts[1] = p while t < CFG.MaxTime and count < MAX_PTS do local nv = v + g * CFG.SimStep local np = p + (v + nv) * 0.5 * CFG.SimStep -- The arena has no collidable/queryable parts, so collide with a rounded box: -- every edge and corner is a quarter-pipe, so the ball rides up and along walls. local bmin, bmax = BOX.min, BOX.max if math.abs(np.X) < CFG.GoalHalfWidth and np.Y < CFG.GoalHeight then bmin = Vector3.new(bmin.X, bmin.Y, -CFG.GoalDepth) bmax = Vector3.new(bmax.X, bmax.Y, CFG.GoalDepth) end local lo = bmin + Vector3.one * (radius + R) local hi = bmax - Vector3.one * (radius + R) local q = Vector3.new(math.clamp(np.X, lo.X, hi.X), math.clamp(np.Y, lo.Y, hi.Y), math.clamp(np.Z, lo.Z, hi.Z)) local d = np - q local dist = d.Magnitude if dist > R and dist > 1e-6 then local n = d / dist local vn = nv:Dot(n) if vn > 0 then np = q + n * R local e = CFG.WallBounce + (CFG.FloorBounce - CFG.WallBounce) * math.abs(n.Y) nv = nv - n * vn * (1 + e) if vn > 15 then bounces += 1 end -- a real impact, not just rolling along the curve end end count += 1 pts[count] = np p, v = np, nv t += CFG.SimStep if bounces > CFG.MaxBounces then break end if v.Magnitude < CFG.MinSpeed and bounces > 0 then break end end -- project each point once, then draw segments between them (clipping at the camera plane) local scr, depth = table.create(count), table.create(count) for i = 1, count do scr[i], depth[i] = project(pts[i]) end local used = 0 for i = 1, count - 1 do local a2, b2, ad, bd = scr[i], scr[i + 1], depth[i], depth[i + 1] if ad > 0 or bd > 0 then if ad <= 0 then a2 = project(pts[i]:Lerp(pts[i + 1], (0.1 - ad) / (bd - ad))) elseif bd <= 0 then b2 = project(pts[i]:Lerp(pts[i + 1], (0.1 - ad) / (bd - ad))) end used += 1 setLine(lines[used], a2, b2) end end hideFrom(used + 1) end table.insert(conns, RunService.RenderStepped:Connect(function() step(os.clock()) end)) table.insert(conns, UIS.InputBegan:Connect(function(input, gpe) if gpe then return end local k = input.KeyCode if k == Enum.KeyCode.KeypadPlus or k == Enum.KeyCode.Equals then CFG.Enabled = not CFG.Enabled print("[Tracer] " .. (CFG.Enabled and "ON" or "OFF")) end end)) getgenv().BallTracerCFG = CFG print("[Tracer] loaded, press + to toggle")