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.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.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 dbest.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 age0 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}