local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local CoreGui = game:GetService("CoreGui") local LocalPlayer = Players.LocalPlayer local oldGui = CoreGui:FindFirstChild("SlapBallGui") if oldGui then pcall(function() oldGui:Destroy() end) end local Tetherball = ReplicatedStorage:WaitForChild("Tetherball") local Config = require(Tetherball:WaitForChild("Config")) local BallMath = require(Tetherball:WaitForChild("BallMath")) local Remotes = Tetherball:WaitForChild("Remotes") local ClientInput = Remotes:WaitForChild("ClientInput") local MatchEvent = Remotes:WaitForChild("MatchEvent") local Courts = workspace:WaitForChild("TetherballCourts") local TAU = 6.283185307179586 local PI = 3.141592653589793 local MAX_REWIND = Config.MaxRewind or 0.35 local REWIND_SAFE = MAX_REWIND * 0.78 local PERFECT_ANGLE = Config.PerfectAngle or 0.10471975511965978 local LOCKOUT = Config.MissClickLockout or 0.75 local court, side, sideAngle local serveArmed = false local serveRetry = 0 local lastStamp = 0 local lockUntil = 0 local pending = nil local scheduled = nil local gen = 0 local clockOffset = 0 local latency = 0.06 local samples = {} local freeKey, freeList local matchHitOk = false local badStreak = 0 local failStreak = 0 local marginIndex = 1 local margins = { { value = 0.06, ok = 0, fail = 0 }, { value = -0.03, ok = 0, fail = 0 }, { value = 0.15, ok = 0, fail = 0 }, { value = 0, ok = 0, fail = 0 }, } local connections = {} local stateConn local function track(conn) table.insert(connections, conn) return conn end local function angDiff(a, b) return math.atan2(math.sin(a - b), math.cos(a - b)) end local function serverNow() return workspace:GetServerTimeNow() + clockOffset end local function readState() if not court or not court.Parent then return nil end return BallMath.decode(court:GetAttribute(BallMath.ATTR)) end local function stateKey(state) if not state then return "nil" end return string.format("%s|%.5f|%.5f|%.5f", tostring(state.m), state.t or 0, state.o or 0, state.th or 0) end local function noteLatency(state) if not state or type(state.t) ~= "number" then return end local delta = workspace:GetServerTimeNow() - state.t if delta ~= delta or math.abs(delta) > 5 then return end table.insert(samples, delta) if #samples > 24 then table.remove(samples, 1) end local lowest = math.huge for _, value in samples do if value < lowest then lowest = value end end if lowest < 0 then clockOffset = clockOffset - lowest + 0.002 table.clear(samples) return end latency = math.clamp(lowest, 0.01, 0.4) end local function resetMatch() pending = nil scheduled = nil lastStamp = 0 lockUntil = 0 freeKey = nil freeList = nil gen = gen + 1 end local function bindCourt(model, index) court = model side = index sideAngle = index == 1 and 0 or PI resetMatch() if stateConn then stateConn:Disconnect() stateConn = nil end if court then stateConn = court:GetAttributeChangedSignal(BallMath.ATTR):Connect(function() noteLatency(readState()) scheduled = nil end) track(stateConn) end end local function flipSide() if not side then return end bindCourt(court, side == 1 and 2 or 1) end local function resolveCourt() local character = LocalPlayer.Character local root = character and character:FindFirstChild("HumanoidRootPart") if not root then return end local bestModel, bestIndex, bestDist for _, model in Courts:GetChildren() do if model:IsA("Model") then for index = 1, 2 do local pad = model:FindFirstChild("PlayerPad" .. index) if pad then local dist = (pad.Position - root.Position).Magnitude if dist <= 18 and (not bestDist or dist < bestDist) then bestModel, bestIndex, bestDist = model, index, dist end end end end end if bestModel and (bestModel ~= court or bestIndex ~= side) then bindCourt(bestModel, bestIndex) end end local function rallyCandidates(state, from, out) local o = state.o if not o or o == 0 then return end local theta = BallMath.eval(state, from) local dir = o >= 0 and 1 or -1 local travel = (angDiff(sideAngle, theta) * dir) % TAU local period = 1 / math.abs(o) local first = from + (travel / TAU) * period for index = 0, 3 do table.insert(out, first + period * index) end end local function freeCandidates(state, from, out) local key = stateKey(state) if key ~= freeKey or not freeList then local found = {} local step = 1 / 240 local limit = from + 6 local prevT = from local prev = angDiff(BallMath.eval(state, prevT), sideAngle) local t = from while t < limit and #found < 4 do t = t + step local cur = angDiff(BallMath.eval(state, t), sideAngle) if (prev >= 0) ~= (cur >= 0) and math.abs(prev) + math.abs(cur) < 0.9 then local lo, hi = prevT, t for _ = 1, 30 do local mid = (lo + hi) * 0.5 if (angDiff(BallMath.eval(state, mid), sideAngle) >= 0) == (prev >= 0) then lo = mid else hi = mid end end table.insert(found, (lo + hi) * 0.5) end prev, prevT = cur, t end freeKey = key freeList = found end for _, value in freeList do table.insert(out, value) end end local function currentMargin() if failStreak >= 2 then local bestIndex, bestScore for index, entry in margins do if index ~= marginIndex then local score = (entry.ok + 1) / (entry.ok + entry.fail + 2) if not bestScore or score > bestScore then bestIndex, bestScore = index, score end end end if bestIndex then marginIndex = bestIndex end failStreak = 0 end return margins[marginIndex] end local function sendClick(stamp, marginUsed) lastStamp = stamp pending = { stamp = stamp, margin = marginUsed, clock = os.clock(), tries = 1, } ClientInput:FireServer("click", stamp) end local function scoreOutcome(perfect) local entry = pending and pending.margin or margins[marginIndex] if perfect then entry.ok = entry.ok + 1 failStreak = 0 else entry.fail = entry.fail + 1 failStreak = failStreak + 1 end end track(MatchEvent.OnClientEvent:Connect(function(name, data) data = data or {} if name == "MatchStart" or name == "Queued" then if data.court and data.side then bindCourt(data.court, data.side) end serveArmed = false matchHitOk = false badStreak = 0 elseif name == "ServeReady" then serveArmed = data.server == LocalPlayer serveRetry = 0 resetMatch() elseif name == "Turn" then serveArmed = false elseif name == "HitResult" then if side and data.side == side then matchHitOk = true badStreak = 0 local rtt = pending and (os.clock() - pending.clock) or nil if rtt and rtt > 0 and rtt < 1.5 then latency = math.clamp(math.min(latency, rtt * 0.5), 0.01, 0.4) end scoreOutcome(data.grade == "Perfect") pending = nil end elseif name == "BadClick" then scoreOutcome(false) pending = nil scheduled = nil lockUntil = serverNow() + LOCKOUT + 0.05 if not matchHitOk then badStreak = badStreak + 1 if badStreak >= 2 then badStreak = 0 flipSide() end end end end)) local repo = "https://raw.githubusercontent.com/deividcomsono/Obsidian/main/" local Library = loadstring(game:HttpGet(repo .. "Library.lua"))() local SaveManager = loadstring(game:HttpGet(repo .. "addons/SaveManager.lua"))() local ThemeManager = loadstring(game:HttpGet(repo .. "addons/ThemeManager.lua"))() Library.ForceCheckbox = false Library.ShowToggleFrameInKeybinds = true local Window = Library:CreateWindow({ Title = "Slap Ball", Footer = "Slap Ball | Made by DonnieAzoff", AutoShow = true, NotifySide = "Right", ShowCustomCursor = false, }) local MainTab = Window:AddTab("Main", "swords") local SlapGroup = MainTab:AddLeftGroupbox("Combat", "zap") SlapGroup:AddToggle("PerfectSlap", { Text = "PerfectSlap", Default = false, }) local CreditsGroup = MainTab:AddRightGroupbox("Credits", "badge-check") CreditsGroup:AddLabel("Slap Ball") CreditsGroup:AddLabel("Script made by DonnieAzoff") local SettingsTab = Window:AddTab("Settings", "sliders-horizontal") local candidates = {} local function enabled() local toggle = Library.Toggles.PerfectSlap return toggle and toggle.Value end local function commit(cross, marginUsed, key) if not enabled() then return end local state = readState() if not state or stateKey(state) ~= key then return end local now = serverNow() local stamp = cross if now - stamp > REWIND_SAFE then stamp = now - REWIND_SAFE end if math.abs(angDiff(BallMath.eval(state, stamp), sideAngle)) > PERFECT_ANGLE * 0.4 then return end sendClick(stamp, marginUsed) end local function step() if not enabled() then return end if not court or not court.Parent or not sideAngle then resolveCourt() if not court or not sideAngle then return end end local state = readState() if not state then return end local now = serverNow() if state.m == "rest" or state.m == "hold" then pending = nil scheduled = nil if serveArmed and now >= serveRetry and now >= lockUntil then serveRetry = now + 0.55 ClientInput:FireServer("click", now) end return end if pending then local waited = os.clock() - pending.clock if waited > latency * 2 + 0.12 then if pending.tries < 3 and (now - pending.stamp) + latency < REWIND_SAFE then pending.tries = pending.tries + 1 pending.clock = os.clock() ClientInput:FireServer("click", pending.stamp) else pending = nil end end end if now < lockUntil then return end local _, _, speed = BallMath.eval(state, now) if not speed or speed <= 0 then return end table.clear(candidates) local from = now - REWIND_SAFE * 0.9 if state.m == "rally" then rallyCandidates(state, from, candidates) else freeCandidates(state, from, candidates) end if #candidates == 0 then return end table.sort(candidates) local target for _, cross in candidates do if cross > lastStamp + 0.12 then if cross >= now or (now - cross) + latency <= REWIND_SAFE then target = cross break end end end if not target then return end if scheduled and math.abs(scheduled - target) < 0.02 then return end local margin = currentMargin() local lead = math.clamp(latency - margin.value, 0, REWIND_SAFE * 0.85) local fireAt = target - lead local key = stateKey(state) local delay = fireAt - now if delay <= 0 then scheduled = target commit(target, margin, key) return end if delay <= 0.35 then scheduled = target local token = gen task.delay(delay, function() if token == gen then commit(target, margin, key) end end) end end local function safeStep() local ok, err = pcall(step) if not ok then warn(err) end end track(RunService.RenderStepped:Connect(safeStep)) track(RunService.Heartbeat:Connect(safeStep)) SaveManager:SetLibrary(Library) SaveManager:IgnoreThemeSettings() SaveManager:SetFolder("Slap Ball") SaveManager:SetIgnoreIndexes({ "MenuKeybind" }) ThemeManager:SetLibrary(Library) ThemeManager:SetFolder("Slap Ball") local ConfigGroupbox = SettingsTab:AddLeftGroupbox("Configuration", "folder-cog") ConfigGroupbox:AddInput("SaveManager_ConfigName", { Text = "Config name", Placeholder = "My Config", }) ConfigGroupbox:AddButton("Create config", function() local name = Library.Options.SaveManager_ConfigName.Value if name:gsub(" ", "") == "" then Library:Notify("Invalid config name (empty)", 2) return end local success, err = SaveManager:Save(name) if not success then Library:Notify("Failed to create config: " .. err) return end Library:Notify(string.format("Created config %q", name)) Library.Options.SaveManager_ConfigList:SetValues(SaveManager:RefreshConfigList()) Library.Options.SaveManager_ConfigList:SetValue(nil) end) ConfigGroupbox:AddDivider() ConfigGroupbox:AddDropdown("SaveManager_ConfigList", { Text = "Config list", Values = SaveManager:RefreshConfigList(), AllowNull = true, }) ConfigGroupbox:AddButton("Load config", function() local name = Library.Options.SaveManager_ConfigList.Value local success, err = SaveManager:Load(name) if not success then Library:Notify("Failed to load config: " .. err) return end Library:Notify(string.format("Loaded config %q", name)) end) ConfigGroupbox:AddButton("Overwrite config", function() local name = Library.Options.SaveManager_ConfigList.Value local success, err = SaveManager:Save(name) if not success then Library:Notify("Failed to overwrite config: " .. err) return end Library:Notify(string.format("Overwrote config %q", name)) end) ConfigGroupbox:AddButton("Delete config", function() local name = Library.Options.SaveManager_ConfigList.Value local success, err = SaveManager:Delete(name) if not success then Library:Notify("Failed to delete config: " .. err) return end Library:Notify(string.format("Deleted config %q", name)) Library.Options.SaveManager_ConfigList:SetValues(SaveManager:RefreshConfigList()) Library.Options.SaveManager_ConfigList:SetValue(nil) end) ConfigGroupbox:AddButton("Refresh list", function() Library.Options.SaveManager_ConfigList:SetValues(SaveManager:RefreshConfigList()) Library.Options.SaveManager_ConfigList:SetValue(nil) end) local AutoloadLabel = ConfigGroupbox:AddLabel("Current autoload config: " .. SaveManager:GetAutoloadConfig(), true) ConfigGroupbox:AddButton("Set as autoload", function() local name = Library.Options.SaveManager_ConfigList.Value local success, err = SaveManager:SaveAutoloadConfig(name) if not success then Library:Notify("Failed to set autoload config: " .. err) return end Library:Notify(string.format("Set %q to auto load", name)) AutoloadLabel:SetText("Current autoload config: " .. name) end) ConfigGroupbox:AddButton("Reset autoload", function() local success, err = SaveManager:DeleteAutoLoadConfig() if not success then Library:Notify("Failed to reset autoload config: " .. err) return end Library:Notify("Set autoload to none") AutoloadLabel:SetText("Current autoload config: none") end) local MenuGroup = SettingsTab:AddRightGroupbox("Menu", "wrench") MenuGroup:AddLabel("Menu bind"):AddKeyPicker("MenuKeybind", { Default = "RightControl", NoUI = true, Text = "Menu keybind", }) MenuGroup:AddButton("Unload", function() Library:Unload() end) SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "MenuKeybind" }) Library.ToggleKeybind = Library.Options.MenuKeybind ThemeManager:ApplyToTab(SettingsTab) SaveManager:LoadAutoloadConfig() Library:OnUnload(function() gen = gen + 1 for _, conn in connections do pcall(function() conn:Disconnect() end) end table.clear(connections) table.clear(samples) stateConn = nil court = nil side = nil sideAngle = nil serveArmed = false pending = nil scheduled = nil freeKey = nil freeList = nil print("Slap Ball by DonnieAzoff unloaded.") end)