8 Ball Duels Script (Delta Mobile) – Autoplay with Trick Shots 48 27 No Key

8 Ball Duels Script (Delta Mobile) – Autoplay with Trick Shots

8 Ball Duels · Roblox
Play
No key Mobile friendly Working · Nov 4
Report

Description

Pool Duels Autoplay is a 8 Ball Duels script you can run in Delta on Android, iOS or PC. It includes autoplay, trick shots, shot lines and more. It waits for your turn and for the balls to settle before each shot. Created by finderisgone. No key.

Key Features

  • Autoplay: plans and takes your shots
  • Trick and bank shots
  • Shot path lines

Pool Duels Autoplay, finderisgone’s 8 Ball Duels autoplay script, doesn’t guess. Before it shoots, it loads the game’s own pool physics and rules modules, runs candidate shots through a simulation, and only fires the one that comes out best. It waits for your turn and for every ball to stop rolling first.

How a turn plays out

  1. Planning. It builds direct pots, bank shots (object ball off a cushion) and kick shots for your legal targets. Then it re-tests the top 30 direct shots at 25, 50, 75 and 100 percent power, with some top and back spin too.
  2. Scoring. The status line counts through the candidates (“Testing every candidate”) and then reports the winner as “Best shot”, with its predicted pots and power percentage.
  3. Shooting. The cue swings round, charges and releases like a real input. With ball in hand it places the cue ball too.

If no candidate passes, the panel reads “No safe shot found. Waiting…” and it keeps looking.

Panel and controls

The real panel is small and draggable, with two buttons: Autoplay: OFF/ON and Lines: ON/OFF. F8 toggles autoplay on a keyboard, and on mobile you tap the button. With autoplay off and lines on, it previews the path of the aim you’re lining up yourself.

Setup notes

No key and no web requests. The full source sits in the code box. It needs an executor that can require the game’s client modules. If that fails you’ll see “Pool module missing” or “Unsupported PoolMatchClient interface”. Running it twice unloads the first copy, and getgenv().PoolDuelsToolsUnload() removes it completely. The tested list puts Delta first. Arceus X and Codex owners can try it too, as can Fluxus users and anyone on a desktop injector.

Questions

Does it shoot during my opponent’s turn? No. It holds at “Waiting for your turn” until the game hands you the cue.

Are there other 8 Ball Duels scripts? Two: Vitality’s Hub and Barrage, both loader-based. This one is fully readable.

Table tip: try a few frames with Lines on and Autoplay off before letting it play. Watching where your own aim actually sends the object ball off a cushion teaches bank angles quickly, and it makes the autoplay’s choices easier to follow.

Tested With
DeltaCodexArceus XFluxusPC executors
Script CodePool Duels Autoplay (finderisgone)
local Players=game:GetService('Players')
local Run=game:GetService('RunService')
local UIS=game:GetService('UserInputService')
local RS=game:GetService('ReplicatedStorage')
local player=assert(Players.LocalPlayer,'Run on the game client')
local ENV=type(getgenv)=='function' and getgenv() or _G
if type(ENV.PoolDuelsToolsUnload)=='function' then pcall(ENV.PoolDuelsToolsUnload)end
local function child(parent,name)return assert(parent:WaitForChild(name,20),'Pool module missing: '..name)end
local pool=child(child(child(RS,'Libraries'),'GameSpecific'),'Pool')
local function module(name)return require(child(pool,name))end
local Physics,Geometry,Rules,C=module('PoolPhysics'),module('PoolGeometry'),module('PoolRules'),module('PoolConstants')
local Client,Input=module('PoolMatchClient'),module('PoolInputController')
local Renderer,View=module('PoolTableRenderer2D'),module('PoolViewController')
local Planner=(function()
local Planner={}
function Planner.new(Physics,Geometry,Rules,C)
    local self={}
    local R=C.BallRadius
    local DT=1/120
    local function copy(value,seen)
        if type(value)~='table' then return value end
        seen=seen or {};if seen[value] then return seen[value] end
        local result={};seen[value]=result
        for k,v in value do result[k]=copy(v,seen) end
        return result
    end
    function self:copyRules(rules)return copy(rules)end
    function self:validPlacement(sim,rules,pos)
        if (Rules.ClampPlacement(rules,pos)-pos).Magnitude>.001 then return false end
        if not Geometry.IsInsidePlayArea(pos,R,Physics.GetTable(sim)) then return false end
        for _,id in sim.Order do
            local b=sim.Balls[id]
            if id~=Physics.CueNumber(sim) and not b.Pocketed and not b.Destroyed and
                (pos-b.Position).Magnitude<R+(b.Radius or R)+.02 then return false end
        end
        return true
    end
    local function clear(sim,a,b,skip1,skip2)
        local delta=b-a;local len=delta:Dot(delta)
        if len<1e-8 then return false end
        for _,id in sim.Order do
            local ball=sim.Balls[id]
            if id~=skip1 and id~=skip2 and not ball.Pocketed and not ball.Destroyed then
                local t=math.clamp((ball.Position-a):Dot(delta)/len,0,1)
                -- Reduced safety margin from .015 to .002 to allow tight straight-on shots
                if (ball.Position-(a+delta*t)).Magnitude<R+(ball.Radius or R)+.002 then return false end
            end
        end
        return true
    end
    function self:candidates(sim,rules,yieldWork)
        local cueId=Physics.CueNumber(sim)
        local cue=sim.Balls[cueId]
        if not cue then return {} end
        local shape=Physics.GetTable(sim)
        local pockets=Geometry.GetPockets(shape)
        local placements={cue.Position}
        if rules.BallInHand then
            for _,x in {-.62,-.25,.2,.6} do
                for _,y in {-.55,0,.55} do
                    local pos=Rules.ClampPlacement(rules,Vector2.new(shape.HalfWidth*x,shape.HalfHeight*y))
                    if self:validPlacement(sim,rules,pos) then placements[#placements+1]=pos end
                end
            end
        end
        local proposals={}
        local function propose(origin,dir,target,pocket,distance,rank,override)
            if dir.Magnitude<1e-6 then return end
            local speed=math.sqrt(2*C.Physics.RollingDeceleration*math.max(distance+5,1))
            local force=override or math.clamp((speed-C.Physics.MinShotSpeed)/(C.Physics.MaxShotSpeed-C.Physics.MinShotSpeed),0,1)^(1/C.Physics.PowerCurve)
            proposals[#proposals+1]={direction=dir.Unit,power=math.clamp(force,.08,1),spin=Vector2.zero,
                target=target,call=pocket,placement=rules.BallInHand and origin or nil,rank=rank}
        end
        for _,origin in placements do
            if not rules.BallInHand or self:validPlacement(sim,rules,origin) then
                for _,id in sim.Order do
                    local b=sim.Balls[id]
                    if not b.Pocketed and not b.Destroyed then
                        local isLegalTarget = Rules.IsLegalFirstContact(rules,sim,id)
                        
                        if isLegalTarget then
                            -- PLAN A: Legal Targets (Direct, Banks, and Kicks)
                            for _,pocket in pockets do
                                if yieldWork then yieldWork()end
                                local point=pocket.BowlCentre or pocket.MouthCentre
                                local out=point-b.Position
                                if out.Magnitude>.01 then
                                    local ghost=b.Position-out.Unit*(R+(b.Radius or R))
                                    local delta=ghost-origin
                                    local cut=delta.Magnitude>.01 and delta.Unit:Dot(out.Unit) or 0
                                    
                                    -- 1. Direct cut shots (Relaxed IsInsidePlayArea check to R*0.1 for frozen balls)
                                    if cut>.12 and Geometry.IsInsidePlayArea(ghost,R*0.1,shape) and clear(sim,origin,ghost,cueId,id)
                                        and clear(sim,b.Position,point,cueId,id) then
                                        propose(origin,delta,id,pocket.Id,delta.Magnitude+out.Magnitude/(cut*cut),200*cut)
                                    end
                                    
                                    local wx,wy=shape.HalfWidth-(b.Radius or R),shape.HalfHeight-(b.Radius or R)
                                    
                                    -- 2. Targeted Kick Shots (Cue ball off wall to hit target into pocket)
                                    for _,image in {Vector2.new(2*wx-ghost.X,ghost.Y),Vector2.new(-2*wx-ghost.X,ghost.Y),
                                        Vector2.new(ghost.X,2*wy-ghost.Y),Vector2.new(ghost.X,-2*wy-ghost.Y)}do
                                        local kick=image-origin
                                        if kick.Magnitude>.01 then
                                            propose(origin,kick,id,pocket.Id,kick.Magnitude+out.Magnitude, 90)
                                        end
                                    end

                                    -- 3. Targeted Bank Shots (Object ball off wall into pocket)
                                    for _,image in {Vector2.new(2*wx-point.X,point.Y),Vector2.new(-2*wx-point.X,point.Y),
                                        Vector2.new(point.X,2*wy-point.Y),Vector2.new(point.X,-2*wy-point.Y)}do
                                        local bank=image-b.Position
                                        if bank.Magnitude>.01 then
                                            local contact=b.Position-bank.Unit*(R+(b.Radius or R))
                                            local approach=contact-origin
                                            local angle=approach.Magnitude>.01 and approach.Unit:Dot(bank.Unit)or 0
                                            if angle>.2 and Geometry.IsInsidePlayArea(contact,R*0.1,shape) and clear(sim,origin,contact,cueId,id)then
                                                propose(origin,approach,id,pocket.Id,approach.Magnitude+bank.Magnitude/(angle*angle),80*angle)
                                            end
                                        end
                                    end
                                end
                            end
                            
                            local direct=b.Position-origin
                            if clear(sim,origin,b.Position,cueId,id) then
                                -- Safe hits to avoid scratching if nothing goes in
                                local tangent=Vector2.new(-direct.Y,direct.X).Unit
                                propose(origin,direct,id,nil,direct.Magnitude,-10,0.15)
                                propose(origin,direct,id,nil,direct.Magnitude,-20,0.5)
                                propose(origin,direct+tangent*(R*0.8),id,nil,direct.Magnitude,-30,0.2)
                                propose(origin,direct-tangent*(R*0.8),id,nil,direct.Magnitude,-30,0.2)
                            else
                                -- PLAN B: Blocked smash (Just to try and break out the ball)
                                propose(origin,direct,id,nil,direct.Magnitude,-150,0.8)
                                propose(origin,direct,id,nil,direct.Magnitude,-150,1.0)
                            end
                        else
                            -- PLAN C: Enemy balls. ONLY if completely desperate, rank lowest.
                            local direct=b.Position-origin
                            if clear(sim,origin,b.Position,cueId,id) then
                                propose(origin,direct,id,nil,direct.Magnitude,-200,0.6)
                                propose(origin,direct,id,nil,direct.Magnitude,-200,1.0)
                            end
                        end
                    end
                end
            end
            
            -- PLAN D: Generic blind wall kicks to avoid fouls if heavily snookered
            for i=1, 36 do
                local angle = (i/36) * math.pi * 2
                local dir = Vector2.new(math.cos(angle), math.sin(angle))
                proposals[#proposals+1]={direction=dir, power=0.5, spin=Vector2.zero, placement=rules.BallInHand and origin or nil, rank=-100}
                proposals[#proposals+1]={direction=dir, power=0.85, spin=Vector2.zero, placement=rules.BallInHand and origin or nil, rank=-110}
            end
        end
        
        -- Sort by best potential rank first
        table.sort(proposals,function(a,b)return a.rank>b.rank end)
        local candidates={}
        local seen={}
        local function add(p,power,angle,spin)
            angle=angle or 0
            local co,si=math.cos(angle),math.sin(angle)
            local dir=Vector2.new(p.direction.X*co-p.direction.Y*si,p.direction.X*si+p.direction.Y*co)
            local v={direction=dir,power=math.clamp(power,C.Input.MinimumPower,1),spin=spin or p.spin,
                target=p.target,call=p.call,placement=p.placement}
            local pos=v.placement or cue.Position
            local key=string.format('%.4f:%.4f:%.3f:%.2f:%.2f:%.2f:%.2f',dir.X,dir.Y,v.power,v.spin.X,v.spin.Y,pos.X,pos.Y)
            if not seen[key] then seen[key]=true;candidates[#candidates+1]=v end
        end
        if rules.Phase=='Break' then
            local nearest,dist
            for _,id in sim.Order do
                local b=sim.Balls[id]
                if id~=cueId and not b.Pocketed then
                    local d=(b.Position-cue.Position).Magnitude
                    if not dist or d<dist then nearest,dist=b,d end
                end
            end
            if nearest then
                local p={direction=(nearest.Position-cue.Position).Unit,spin=Vector2.zero,
                    target=nearest.Number,placement=rules.BallInHand and cue.Position or nil}
                for _,angle in {0,.01,-.01,.03,-.03} do add(p,1,angle)end
            end
        end
        
        -- Test up to 350 top proposals
        for i=1, math.min(350,#proposals) do
            local p=proposals[i]
            if p.call then
                add(p,p.power)
                add(p,math.clamp(p.power*1.2, 0.1, 1))
                add(p,math.clamp(p.power*0.8, 0.1, 1))
                
                -- Power Bracketing: For the top 30 direct shots, test at multiple fixed powers to avoid pocket rattle
                if i <= 30 then
                    for _,power in ipairs({0.25, 0.5, 0.75, 1.0}) do add(p,power) end
                    for _,spin in ipairs({Vector2.new(0,.4),Vector2.new(0,-.4)}) do add(p,p.power,0,spin) end
                end
            else
                add(p,p.power)
            end
        end
        return candidates
    end
    function self:simulate(base,rules,shot,yieldWork,trace)
        local sim=Physics.Clone(base)
        local match=copy(rules)
        local cueId=Physics.CueNumber(sim)
        if shot.placement then
            if not rules.BallInHand or not self:validPlacement(sim,rules,shot.placement)then return nil end
            sim.Balls[cueId].Position=shot.placement
            sim.Balls[cueId].Pocketed=false;sim.Balls[cueId].Sinking=false
        end
        local context=Rules.BeginShot(match,sim)
        context.CalledPocket=shot.call
        local paths={}
        local function record()
            for _,id in sim.Order do
                local b=sim.Balls[id]
                if not b.Pocketed then
                    local line=paths[id]or {};paths[id]=line
                    local pos=b.Position
                    if #line==0 or (line[#line]-pos).Magnitude>.04 then
                        if #line>=2 then
                            local a=line[#line]-line[#line-1];local d=pos-line[#line]
                            local cross=math.abs(a.X*d.Y-a.Y*d.X)
                            if a:Dot(d)>0 and cross<.008*a.Magnitude*d.Magnitude then line[#line]=pos
                            elseif #line<110 then line[#line+1]=pos end
                        else line[#line+1]=pos end
                    end
                end
            end
        end
        if trace then record() end
        if not Physics.Shoot(sim,shot.direction,shot.power,shot.spin)then return nil end
        local events=0
        for tick=1,3600 do
            Physics.Step(sim,DT)
            if trace and (tick%4==0 or #sim.Events~=events)then record();events=#sim.Events end
            if yieldWork and tick%16==0 then yieldWork()end
            if sim.Settled then break end
        end
        if not sim.Settled then return nil end
        local result=Rules.ResolveShot(match,sim,context)
        local cue=sim.Balls[cueId]
        local pots,wrong=0,0
        local group=Rules.GetGroup(rules,rules.Turn)or Rules.GetGroup(match,rules.Turn)
        for _,id in result.Potted or {} do
            if id~=cueId then
                if not Rules.IsNineBall(rules) and group and id~=Rules.MoneyBall(rules) and C.GetGroup(id)~=group then wrong+=1
                else pots+=1 end
            end
        end
        local score
        if result.Winner and result.Winner~=rules.Turn then score=-100000
        elseif result.Winner==rules.Turn then score=100000
        elseif result.Legal~=true then 
            score=-10000
            if pots > 0 then
                score = -5000 + pots*500
            else
                if cue and not cue.Pocketed then
                    local distance
                    for _,id in sim.Order do
                        local b=sim.Balls[id]
                        if not b.Pocketed and Rules.IsLegalFirstContact(match,sim,id) then
                            local d=(b.Position-cue.Position).Magnitude
                            if not distance or d<distance then distance=d end
                        end
                    end
                    if distance then score-=distance*10 end
                end
            end
        else
            score=pots*1200-wrong*700+(result.Continues and 500 or 0)
            score=score-shot.power*15
        end
        if cue and cue.Pocketed then score-=15000 end
        if result.WrongPocket then score-=2000 end
        return {shot=shot,score=score,result=result,paths=paths,sim=sim,pots=pots}
    end
    function self:search(base,rules,yieldWork,expired,onProgress)
        local candidates=self:candidates(base,rules,yieldWork)
        local best
        local evaluated,total=0,#candidates
        if onProgress then onProgress(0,total,nil)end
        for i,shot in candidates do
            if expired()then break end
            local result=self:simulate(base,rules,shot,yieldWork,false)
            evaluated+=1
            if result and (not best or result.score>best.score)then 
                best=result 
                -- EARLY EXIT: Stop calculating immediately if we found a perfect legal pot
                if best.pots > 0 and best.result.Legal == true and best.score > 500 then
                    break
                end
            end
            if onProgress then onProgress(evaluated,total,best)end
        end
        return best
    end
    return self
end
return Planner
end)()
local planner=Planner.new(Physics,Geometry,Rules,C)

-- Hardcoded calcSpeed to 0.03734 (equivalent to 93% power on old slider)
local state={closed=false,auto=false,lines=true,calcSpeed=0.03734,quality=2,current=nil,seen=0,job=nil,best=nil,paths={},pathResult=nil,
    key=nil,previewKey=nil,failedKey=nil,lastShot=nil,requestPlan=false,showBest=false,scan=0,drawAge=0,uiAge=0,
    tested=0,total=0,fired=0,execution=nil}
local connections={}
local gui=Instance.new('ScreenGui');gui.Name='PoolDuelsClientTools';gui.ResetOnSpawn=false;gui.IgnoreGuiInset=true;gui.DisplayOrder=10000;gui.ZIndexBehavior=Enum.ZIndexBehavior.Global
gui.Parent=child(player,'PlayerGui')
local function make(kind,parent,props)
    local o=Instance.new(kind);for key,value in props do o[key]=value end;o.Parent=parent;return o
end

-- Reduced panel size to fit just the essential buttons
local panel=make('Frame',gui,{Size=UDim2.fromOffset(300,130),Position=UDim2.new(0,18,.5,-65),BackgroundColor3=Color3.fromRGB(16,18,25),BorderSizePixel=0,Active=true,ZIndex=10})
make('UICorner',panel,{CornerRadius=UDim.new(0,12)})
local function button(value,y)
    local b=make('TextButton',panel,{Text=value,Position=UDim2.fromOffset(12,y),Size=UDim2.new(1,-24,0,30),BackgroundColor3=Color3.fromRGB(33,43,62),TextColor3=Color3.fromRGB(238,243,255),Font=Enum.Font.GothamBold,TextSize=13,BorderSizePixel=0,ZIndex=11,Visible=true,Active=true,Selectable=true})
    make('UICorner',b,{CornerRadius=UDim.new(0,6)});return b
end

local autoButton=button('Autoplay: OFF',8)
local linesButton=button('Lines: ON',46)
local progress=make('TextLabel',panel,{Text='Autoplay off',Position=UDim2.fromOffset(12,84),Size=UDim2.new(1,-24,0,40),
    BackgroundTransparency=1,TextColor3=Color3.fromRGB(198,213,235),Font=Enum.Font.Gotham,TextSize=12,TextWrapped=true,ZIndex=12,Visible=true})
local status={Text='Waiting for your pool match'}
local lineObjects={}

local function hideLines()
    for _,line in lineObjects do line.Visible=false end
end
local function matchState(c)
    return type(c)=='table' and c.Replica and c.Replica.Data and c.Replica.Data.poolMatch
end
local function ready(c)
    local m=matchState(c)
    return m and m.Phase=='Playing' and c.Seat and c.Seat>0 and m.Turn==c.Seat and
        type(c.Simulation)=='table' and c.Simulation.Settled and not c.AwaitingShot and not c.Covered and
        type(c.Input)=='table' and c.Input.ShotBindable and c.Input.State~='Camera' and c.Input.State~='GameOver' and type(c.Rules)=='table' and c.Rules.Turn==c.Seat and c.Rules.Phase~='GameOver' and
        not c.Pending and #(c.ShotQueue or {})==0 and #(c.PatchQueue or {})==0
end
local function fingerprint(c)
    if not c or not c.Simulation or not c.Rules then return nil end
    local parts={tostring(c),tostring(c.Simulation),tostring(c.Seat),tostring(c.Rules.Turn),tostring(c.Rules.ShotCount),tostring(c.AppliedShot),
        tostring(c.Rules.Phase),tostring(c.Rules.BallInHand),tostring(c.Rules.BehindHeadString),tostring(c.Rules.GameType),tostring(c.Rules.CallPocket),
        tostring(c.Rules.Groups and c.Rules.Groups[1]),tostring(c.Rules.Groups and c.Rules.Groups[2]),tostring(c.Simulation.Table),tostring(c.Simulation.Modifiers)}
    for _,id in c.Simulation.Order do
        local b=c.Simulation.Balls[id]
        parts[#parts+1]=string.format('%s:%.5f:%.5f:%s:%s',tostring(id),b.Position.X,b.Position.Y,tostring(b.Pocketed),tostring(b.Destroyed))
    end
    return table.concat(parts,'|')
end
local function cancelExecution()
    local e=state.execution
    state.execution=nil
    if not e then return end
    local c=e.client
    if ready(c) then
        if e.oldPosition and c.Rules.BallInHand then
            local ball=c.Simulation.Balls[Physics.CueNumber(c.Simulation)]
            if ball and (ball.Position-e.best.shot.placement).Magnitude<.001 then
                ball.Position=e.oldPosition
                View.SetBall(c.View,ball.Number,ball.Position)
            end
        end
        c.Input.ChargeMode=e.saved.ChargeMode;c.Input.Dial=e.saved.Dial
        c.Input.Dragging=e.saved.Dragging;c.Input.AimLocked=e.saved.AimLocked
        c.Input.Direction=e.saved.Direction;c.Hud.Spin=e.saved.Spin
        Input.SetState(c.Input,e.saved.State or 'Aiming')
        Input.SetPower(c.Input,e.saved.Power or 0)
    end
end
local function clearWork()
    cancelExecution()
    state.job=nil;state.best=nil;state.paths={};state.pathResult=nil;state.previewKey=nil;state.showBest=false;hideLines()
end
local function fail(err)
    cancelExecution()
    state.auto=false;autoButton.Text='Autoplay: OFF';state.job=nil
    status.Text='Stopped: '..string.sub(tostring(err),1,130)
    warn('[Pool tools] '..tostring(err))
end
local function jobFor(kind,c,key,shot)
    local sim=Physics.Clone(c.Simulation)
    local rules=planner:copyRules(c.Rules)
    local started=os.clock()
    local job={kind=kind,client=c,key=key,best=nil,checked=0,total=0,started=started}
    job.expires=kind=='search' and math.huge or started+5
    if kind=='search' then state.tested=0;state.total=0 end
    local function yieldWork()
        if os.clock()>=job.expires then coroutine.yield('expired')end
        if os.clock()>=job.sliceEnd then coroutine.yield()end
    end
    job.work=coroutine.create(function()
        if kind=='search' then
            return planner:search(sim,rules,yieldWork,function()return os.clock()>=job.expires end,
                function(i,total,best)job.checked=i;job.total=total;job.best=best;state.tested=i;state.total=total end)
        end
        return planner:simulate(sim,rules,shot,yieldWork,true)
    end)
    state.job=job
end
local function safeBest(c,best)
    return ready(c) and not Physics.HasPatrol(c.Simulation) and best and best.key==fingerprint(c) and best.result and
        (not best.result.Winner or best.result.Winner==c.Seat)
end
local function fireBest(verified)
    local c,best=state.current,verified or state.best
    if not safeBest(c,best)then status.Text='No current verified legal shot. Find a shot first.';return false end
    if state.lastShot==best.key then status.Text='Shot already requested; waiting for the game';return false end
    local shot=best.shot
    if shot.placement then
        if not c.Rules.BallInHand or not c.Input.CanPlaceCueBall or not planner:validPlacement(c.Simulation,c.Rules,shot.placement)then return false end
        local ball=c.Simulation.Balls[Physics.CueNumber(c.Simulation)]
        ball.Position=shot.placement;ball.Pocketed=false;ball.Sinking=false
        View.SetBall(c.View,ball.Number,shot.placement)
        c.Input.Dragging=false
        Input.SetState(c.Input,'Aiming')
    end
    c.Call=shot.call;c.CallLocked=shot.call~=nil
    c.Hud.Spin=shot.spin
    Input.SetAimDirection(c.Input,shot.direction)
    Input.SetPower(c.Input,shot.power)
    c.Input.CanPlaceCueBall=false
    state.lastShot=best.key
    state.job=nil;hideLines()
    local e=state.execution
    state.execution=nil
    if e then
        c.Input.ChargeMode=e.saved.ChargeMode;c.Input.Dial=e.saved.Dial
        c.Input.AimLocked=e.saved.AimLocked
    end
    Input.SetState(c.Input,'Simulating')
    c.Input.ShotBindable:Fire(shot.direction,shot.power,shot.spin)
    state.fired+=1
    status.Text='Shot requested · waiting for the result'
    return true
end
local function beginExecution(c,best)
    local direction=c.Input.Direction or best.shot.direction
    if direction.Magnitude<.001 then direction=best.shot.direction end
    local startAngle=math.atan2(direction.Y,direction.X)
    local targetAngle=math.atan2(best.shot.direction.Y,best.shot.direction.X)
    local delta=(targetAngle-startAngle+math.pi)%(2*math.pi)-math.pi
    local e={client=c,best=best,started=os.clock(),startAngle=startAngle,delta=delta,
        aimSeconds=math.clamp(.35+math.abs(delta)*.22,.35,1),chargeSeconds=.55+best.shot.power*.3,holdSeconds=.15,
        saved={State=c.Input.State,ChargeMode=c.Input.ChargeMode,Dial=c.Input.Dial,Dragging=c.Input.Dragging,
            AimLocked=c.Input.AimLocked,Direction=c.Input.Direction,Power=c.Input.Power,Spin=c.Hud.Spin}}
    if best.shot.placement then
        if not c.Rules.BallInHand or not c.Input.CanPlaceCueBall or not planner:validPlacement(c.Simulation,c.Rules,best.shot.placement)then return false end
        local ball=c.Simulation.Balls[Physics.CueNumber(c.Simulation)]
        e.oldPosition=ball.Position;ball.Position=best.shot.placement
        View.SetBall(c.View,ball.Number,ball.Position)
        best.key=fingerprint(c);state.key=best.key;state.job=nil;state.previewKey=nil
    end
    c.Input.Dragging=false
    Input.SetState(c.Input,'Aiming')
    c.Hud.Spin=best.shot.spin
    state.execution=e
    return true
end
local function advanceExecution()
    local e=state.execution
    if not e then return end
    local c=e.client
    if not state.auto or c~=state.current or not safeBest(c,e.best)then cancelExecution();return end
    local age=os.clock()-e.started
    local remaining=math.max(0,e.aimSeconds+e.chargeSeconds+e.holdSeconds-age)
    local phase
    if age<e.aimSeconds then
        local t=math.clamp(age/e.aimSeconds,0,1);t=t*t*(3-2*t)
        local angle=e.startAngle+e.delta*t
        Input.SetAimDirection(c.Input,Vector2.new(math.cos(angle),math.sin(angle)))
        Input.SetPower(c.Input,0)
        phase='Aiming'
    else
        if not e.charging then
            e.charging=true;c.Input.ChargeMode='Dial';c.Input.Dial=0
            Input.SetState(c.Input,'Charging')
        end
        Input.SetAimDirection(c.Input,e.best.shot.direction)
        local t=math.clamp((age-e.aimSeconds)/e.chargeSeconds,0,1)
        Input.SetPower(c.Input,e.best.shot.power*t*t*(3-2*t))
        phase=t<1 and 'Charging' or 'Ready'
    end
    View.SetCueStick(c.View,c.Simulation.Balls[Physics.CueNumber(c.Simulation)].Position,c.Input.Direction,c.Input.Power)
    progress.Text=string.format('%s - shot in %.1fs\nTested %d/%d | Shots sent: %d',phase,remaining,state.tested,state.total,state.fired)
    if remaining<=0 then fireBest(e.best)end
end
local function toggleAuto()
    cancelExecution()
    state.auto=not state.auto;state.requestPlan=state.auto and not safeBest(state.current,state.best);state.job=nil
    autoButton.Text=state.auto and 'Autoplay: ON' or 'Autoplay: OFF'
    if not state.auto then status.Text='Autoplay off';progress.Text=string.format('Autoplay off\nTested %d/%d | Shots sent: %d',state.tested,state.total,state.fired)end
end
connections[#connections+1]=linesButton.Activated:Connect(function()
    state.lines=not state.lines;linesButton.Text=state.lines and 'Lines: ON' or 'Lines: OFF'
    if not state.lines then hideLines();if state.job and state.job.kind~='search' then state.job=nil end
    else state.previewKey=nil end
end)
connections[#connections+1]=autoButton.Activated:Connect(toggleAuto)
connections[#connections+1]=UIS.InputBegan:Connect(function(input,processed)
    if not processed and not UIS:GetFocusedTextBox() and input.KeyCode==Enum.KeyCode.F8 then toggleAuto()end
end)
local dragging,dragStart,panelStart,dragTouch
connections[#connections+1]=panel.InputBegan:Connect(function(input)
    if input.UserInputType==Enum.UserInputType.MouseButton1 or input.UserInputType==Enum.UserInputType.Touch then
        dragging=true;dragStart=input.Position;panelStart=panel.Position;dragTouch=input
    end
end)
connections[#connections+1]=UIS.InputChanged:Connect(function(input)
    if dragging and (input==dragTouch or input.UserInputType==Enum.UserInputType.MouseMovement)then
        local delta=input.Position-dragStart
        panel.Position=UDim2.new(panelStart.X.Scale,panelStart.X.Offset+delta.X,panelStart.Y.Scale,panelStart.Y.Offset+delta.Y)
    end
end)
connections[#connections+1]=UIS.InputEnded:Connect(function(input)
    if input==dragTouch or input.UserInputType==Enum.UserInputType.MouseButton1 then dragging=false end
end)
local originalUpdate=Client.Update
assert(type(originalUpdate)=='function','Unsupported PoolMatchClient interface')
local wrappedUpdate
wrappedUpdate=function(c,...)
    if not state.closed then state.current=c;state.seen=os.clock()end
    return originalUpdate(c,...)
end
assert(not table.isfrozen or not table.isfrozen(Client),'PoolMatchClient is read-only in this runner')
Client.Update=wrappedUpdate
local function unload()
    if state.closed then return end
    cancelExecution()
    state.closed=true;state.auto=false;state.job=nil
    if Client.Update==wrappedUpdate then Client.Update=originalUpdate end
    for _,connection in connections do connection:Disconnect()end
    ENV.PoolDuelsToolsUnload=nil
    gui:Destroy()
end
ENV.PoolDuelsToolsUnload=unload
connections[#connections+1]=gui.Destroying:Connect(unload)
local function project(c,point)
    if c.View.Mode=='2D' then
        local origin,size=Renderer.GetScreenRect(c.View.Renderer2D)
        local scale=Renderer.TableToScale(point)
        return origin+Vector2.new(scale.X.Scale*size.X,scale.Y.Scale*size.Y),size.X>0 and size.Y>0
    end
    local camera=workspace.CurrentCamera
    if not camera then return Vector2.zero,false end
    local p,on=camera:WorldToViewportPoint(View.ToWorld(point,C.BallRadius))
    return Vector2.new(p.X,p.Y),on and p.Z>0
end
local function draw()
    local c=state.current
    if not state.lines or not ready(c) or not state.pathResult or state.pathResult.key~=state.key then hideLines();return end
    local n=0
    for id,path in state.paths do
        local color=id==Physics.CueNumber(c.Simulation) and Color3.fromRGB(95,210,255)or Color3.fromRGB(255,206,95)
        local ball=state.pathResult.sim.Balls[id]
        if ball and ball.Pocketed then color=id==Physics.CueNumber(c.Simulation)and Color3.fromRGB(255,90,100)or Color3.fromRGB(90,240,145)end
        for i=2,#path do
            if n>=600 then break end
            local a,visibleA=project(c,path[i-1]);local b,visibleB=project(c,path[i])
            local delta=b-a
            if visibleA and visibleB and delta.Magnitude>.4 then
                n+=1
                local line=lineObjects[n]
                if not line then
                    line=make('Frame',gui,{BorderSizePixel=0,AnchorPoint=Vector2.new(.5,.5),ZIndex=2,BackgroundTransparency=.12})
                    lineObjects[n]=line
                end
                line.Size=UDim2.fromOffset(delta.Magnitude,2)
                line.Position=UDim2.fromOffset((a.X+b.X)*.5,(a.Y+b.Y)*.5)
                line.Rotation=math.deg(math.atan2(delta.Y,delta.X));line.BackgroundColor3=color;line.Visible=true
            end
        end
    end
    for i=n+1,#lineObjects do lineObjects[i].Visible=false end
end
local function tick(dt)
    local c=state.current
    state.scan+=dt
    if (not c or os.clock()-state.seen>2)and state.scan>2 and type(getgc)=='function'then
        state.scan=0
        for _,candidate in getgc(true)do
            if type(candidate)=='table' and rawget(candidate,'Simulation')and rawget(candidate,'Input')and rawget(candidate,'Replica') and rawget(candidate,'Rules')then
                if ready(candidate)then state.current=candidate;state.seen=os.clock();c=candidate;break end
            end
        end
    end
    if not ready(c) then
        clearWork();state.key=nil
        if not c then status.Text='Waiting for your pool match'
        elseif c.AwaitingShot or not c.Simulation.Settled then status.Text='Waiting for the balls to settle'
        else status.Text='Waiting for your turn'end
        progress.Text=(state.auto and status.Text or 'Autoplay off')..' | Shots sent: '..state.fired
        return
    end
    if Physics.HasPatrol(c.Simulation)then
        clearWork();status.Text='Moving table objects: autoplay paused';progress.Text=status.Text;return
    end
    local key=fingerprint(c)
    if key~=state.key then clearWork();state.key=key;state.lastShot=nil;state.failedKey=nil end
    if state.requestPlan then cancelExecution();state.best=nil;state.failedKey=nil end
    if (state.requestPlan or (state.auto and not state.best and state.lastShot~=key and state.failedKey~=key))and(not state.job or state.job.kind~='search')then
        state.requestPlan=false;state.showBest=false
        jobFor('search',c,key)
    end
    local job=state.job
    if job then
        if job.key~=key or job.client~=c then state.job=nil;return end
        job.sliceEnd=os.clock()+(job.kind=='search' and state.calcSpeed or 0.004)
        local ok,result=coroutine.resume(job.work)
        if not ok then fail(result);return end
        local done=coroutine.status(job.work)=='dead' or result=='expired'
        if done then
            state.job=nil
            if result=='expired'then result=job.kind=='search'and job.best or nil end
            if job.kind=='search' then
                state.best=result;state.showBest=result~=nil;state.planFinished=os.clock();state.previewKey=nil
                if result then result.key=key end
                if not result or not safeBest(c,result)then
                    state.failedKey = key
                    status.Text='No safe shot found. Waiting...'
                else status.Text='Best shot: '..result.pots..' predicted pot(s) · '..math.floor(result.shot.power*100)..'% power' end
            elseif result then
                result.key=key;state.paths=result.paths;state.pathResult=result
            end
        elseif job.kind=='search'then
            status.Text='Planning · '..job.checked..'/'..job.total..' shots tested'
        end
    end
    if state.auto and safeBest(c,state.best)and state.lastShot~=state.best.key and not state.execution then beginExecution(c,state.best)end
    if state.execution then advanceExecution()
    elseif state.auto then
        if state.job and state.job.kind=='search' then
            if state.total==0 then progress.Text=string.format('Preparing shots\nShots sent: %d',state.fired)
            else progress.Text=string.format('Testing every candidate: %d/%d\nShots sent: %d',state.tested,state.total,state.fired)end
        elseif state.lastShot==key then progress.Text='Waiting for shot result | Shots sent: '..state.fired
        else progress.Text='Tested '..state.tested..'/'..state.total..' | Shots sent: '..state.fired end
    else progress.Text=string.format('Autoplay off\nTested %d/%d | Shots sent: %d',state.tested,state.total,state.fired)end
    if state.lines and not state.job then
        local shot
        if state.showBest and state.best then shot=state.best.shot
        else
            local direction=c.Input.Direction
            if not direction or direction.Magnitude<.001 then return end
            local power=c.Input.Power or 0
            shot={direction=direction.Unit,power=power>=C.Input.MinimumPower and power or .5,spin=c.Hud.Spin or Vector2.zero,call=c.Call}
        end
        local previewKey=string.format('%.4f:%.4f:%.3f:%.2f:%.2f:%s',shot.direction.X,shot.direction.Y,shot.power,shot.spin.X,shot.spin.Y,tostring(state.showBest))
        if state.previewKey~=previewKey then state.previewKey=previewKey;jobFor('trace',c,key,shot)end
        if not state.showBest then status.Text='Your aim · preview at '..math.floor(shot.power*100)..'% power' end
    end
end
connections[#connections+1]=Run.Heartbeat:Connect(function(dt)
    if state.closed then return end
    local ok,err=pcall(tick,dt);if not ok then fail(err)end
end)
connections[#connections+1]=Run.RenderStepped:Connect(function(dt)
    if state.closed then return end
    state.drawAge+=dt;if state.drawAge<1/30 then return end;state.drawAge=0
    local ok,err=pcall(draw);if not ok then hideLines();status.Text='Prediction display: '..string.sub(tostring(err),1,100)end
end)
return {Unload=unload,State=state,Plan=function()state.requestPlan=true end}
Game page8 Ball Duels · 3 scripts on this siteRoblox ›

More 8 Ball Duels scripts

See all
Get Delta Script first on GoogleAdd us as a preferred source to see our new scripts in your Google results.
Add Delta Script on Google

Leave a Reply

Your email address will not be published. Required fields are marked *