-- PuckAFK | +1 Cut Grass Adventure | v1.6 - Smooth long-distance navigation -- v1.6: smooth base-to-zone travel: direct unobstructed legs no longer get broken into tiny path steps, -- MoveTo commands are held instead of re-issued every frame, and safe look-ahead skips redundant waypoints. -- v1.5: when Auto World is enabled, standing beside the current world's new-world portal immediately -- uses the same world transition path as the game once the next world is unlocked; no base detour first. -- v1.4: precision loot approach uses the prompt's real 3D activation distance; grass is treated as a soft -- navigation obstacle instead of a wall, and touching the target grass counts as reaching cutting range. -- v1.3: wall-aware navigation, immediate path planning, blocked-waypoint repaths, local escape recovery, -- safe arrival-ring goals, obstacle-aware target scoring, plus all v1.2 farming/progression behaviour. -- v1.2: event-invalidated per-zone grass cache, bounded cluster routing, short loot lookup cache, -- event-driven backpack refresh, selling on natural base visits, independent progress schedules. -- v1.1: choose highest practical zone (or essence-rate mode), then a local grass target. -- Uses zone-index health modifiers, sustained strength, attack cooldown, hit/time limits. -- Includes promotion debounce, streamed-zone approach, hit feedback, stall backoff, -- live loot rarity/expiry checks, optional unlocked-world progression, and cash reserve. -- Based on place 90086669327265 and the supplied 2026-09-06 client/network capture. -- Execute client-side. PuckUI supplies profiles, autosave, layout and the shared K bind. -- Auto Sell sells all sellable backpack loot. It is OFF by default. -- Live server testing is still required; no server scripts were present in the dump. if not game:IsLoaded() then game.Loaded:Wait() end local Players = game:GetService("Players") local RS = game:GetService("ReplicatedStorage") local CS = game:GetService("CollectionService") local PF = game:GetService("PathfindingService") local player = Players.LocalPlayer local env = type(getgenv) == "function" and getgenv() or _G local key = "__PUCK_CUT_GRASS" if env[key] and env[key].Stop then pcall(env[key].Stop) end local app = {alive = true, connections = {}, tasks = {}, pending = {}, message = "Ready", generation = 0} env[key] = app local settings = {Farm=false, Train=false, Loot=true, Sell=false, Cutter=false, Quests=false, Rebirth=false, AntiAFK=true, Zone="Auto", Hits=4, PickupDistance=90, TrainInterval=0.2, Carry=false, AttackSpeed=false, AttackRange=false, Speed=false, SmartTrain=true, ClearTime=2, ZoneMode="Highest suitable", AutoWorld=false, Reserve=0} local window, statusLabel, bagLabel, detailLabel, zoneLabel, nextZoneLabel local telemetry={cuts=0,pickups=0,returns=0,failures=0} local zonePlan, replan, targetLoot local damageState={strength=0,damage=0,interval=0.5,frenzy=false} local zoneBlocked={} local farmSeconds=0 local function character() local c=player.Character local h=c and c:FindFirstChildOfClass("Humanoid") local r=c and c:FindFirstChild("HumanoidRootPart") if h and r and h.Health>0 then return c,h,r end end local function connect(signal, fn) local c=signal:Connect(fn); table.insert(app.connections,c); return c end local function stopMovement() local _,h,r=character(); if h then h:MoveTo(r.Position); h:Move(Vector3.zero) end end function app.Stop() if not app.alive then return end app.alive=false; app.generation=app.generation+1 stopMovement() for _,c in ipairs(app.connections) do c:Disconnect() end for _,t in ipairs(app.tasks) do if t~=coroutine.running() then pcall(task.cancel,t) end end for _,t in pairs(app.pending) do pcall(task.cancel,t) end if window then pcall(function() window:Destroy() end) end if env[key]==app then env[key]=nil end end local function worker(fn) local t=task.spawn(fn); table.insert(app.tasks,t); return t end local function loadUI() local ok, result=pcall(function() return loadstring(game:HttpGet("https://raw.githubusercontent.com/PuckAFK/Puck-Loader/main/ui/PuckUI.lua"))() end) if not ok or type(result)~="table" then error("PuckUI could not load: "..tostring(result)) end return result end local UI=loadUI() window=UI:CreateWindow({Name="PuckAFK | Cut Grass Adventure",Title="PuckAFK | Cut Grass Adventure v1.6", GuiName="PuckAFK_CutGrass",ConfigId="Cut_Grass_Adventure",Width=560,Height=570}) window.CloseCallback=app.Stop local function say(message) app.message=tostring(message) end local services local deadline=os.clock()+20 repeat local packages=RS:FindFirstChild("Packages") if packages then for _,x in ipairs(packages:GetDescendants()) do if x.Name=="Services" and x:FindFirstChild("AttackService") then services=x; break end end end if not services then task.wait(0.5) end until services or os.clock()>deadline if not services then UI:Notify({Title="Game unavailable",Content="Cut Grass services were not found. Join the game, then run again.",Duration=8}) return end local function remote(service, method, kind) local s=services:FindFirstChild(service); local f=s and s:FindFirstChild(kind) return f and f:FindFirstChild(method) end -- Each RemoteFunction has at most one outstanding invocation, including after timeout. local function invoke(service, method, ...) if not app.alive then return nil,"Unloaded" end local r=remote(service,method,"RF") if not r then return nil,"Missing "..service.."."..method end if app.pending[r] then return nil,"Request pending: "..method end local args=table.pack(...); local done,ok,value=false,false,nil local thread=coroutine.create(function() ok,value=pcall(function() return r:InvokeServer(table.unpack(args,1,args.n)) end) done=true; app.pending[r]=nil end) app.pending[r]=thread; task.spawn(thread) local untilTime=os.clock()+7 repeat task.wait(0.05) until done or not app.alive or os.clock()>untilTime if not done then return nil,"Timed out: "..method end if not ok then return nil,tostring(value) end return value end local function fire(service,method,...) local r=remote(service,method,"RE"); if app.alive and r then r:FireServer(...); return true end return false end local function listen(service,method,fn) local r=remote(service,method,"RE"); if r then connect(r.OnClientEvent,fn) end end local function module(folder,name) local shared=RS:FindFirstChild("Shared"); local f=shared and shared:FindFirstChild(folder) local m=f and f:FindFirstChild(name) if m then local ok,v=pcall(require,m); if ok then return v end end end local zonesConfig=module("Configs","Zone") or {} local worlds=module("Configs","Worlds") or {} local grassExperiment=module("Configs","GrassHpExperiment") local lootConfig=module("Configs","Loot") or {} local frenzyConfig=module("Configs","Frenzy") or {} local attackController, worldTransitionController pcall(function() local knit=require(RS.Packages.knit) attackController=knit.GetController("AttackController") worldTransitionController=knit.GetController("WorldTransitionController") end) local bag={load=0,capacity=0,revision=-1,known=false,updated=0} local money=0 local cooldown=0.5 local function applyBag(s) if type(s)~="table" then return end local revision=tonumber(s.Revision) or bag.revision if revision0; bag.updated=os.clock() end local function refreshBag() local s,e=invoke("DataService","GetBackpackSlotsState"); applyBag(s) if type(s)~="table" and os.clock()-bag.updated>15 then bag.known=false end return s,e end listen("DataService","BackpackSlotsUpdated",applyBag) listen("DataService","UpdateMoney",function(v) money=tonumber(v) or money end) listen("DataService","AttackCooldownChanged",function(v) cooldown=math.max(0.05,tonumber(v) or cooldown) end) local health=setmetatable({},{__mode="k"}) listen("GrassService","GrassHitVisuals",function(packet) if type(packet)~="table" then return end for _,u in ipairs(packet.Updates or {}) do local x=u.GrassModel; local ratio=tonumber(u.RemainingHealthRatio) if typeof(x)=="Instance" and ratio then local old=health[x] if settings.Farm and ratio<=0 and (not old or old.ratio>0) then telemetry.cuts=telemetry.cuts+1 end health[x]={ratio=math.clamp(ratio,0,1),time=os.clock()} end end end) listen("GrassService","ResetPlayerGrassState",function() table.clear(health) end) local prompts={} local ignored=setmetatable({},{__mode="k"}) local function track(x) if x:IsA("ProximityPrompt") then prompts[x]=true end end for _,x in ipairs(workspace:GetDescendants()) do track(x) end connect(workspace.DescendantAdded,track) connect(workspace.DescendantRemoving,function(x) prompts[x]=nil end) connect(player.CharacterAdded,function() app.generation=app.generation+1; bag.known=false; zonePlan=nil; table.clear(health); table.clear(zoneBlocked) end) connect(player:GetAttributeChangedSignal("CurrentWorld"),function() app.generation=app.generation+1; zonePlan=nil; table.clear(health); table.clear(zoneBlocked); stopMovement() end) local function position(x) if not x or not x.Parent then return end if x:IsA("Attachment") then return x.WorldPosition end if x:IsA("BasePart") then return x.Position end if x:IsA("Model") then return x:GetPivot().Position end end local function active(token) return app.alive and settings.Farm and token==app.generation end local function updateDamage() local strength=math.max(0,tonumber(player:GetAttribute("StrengthRaw")) or 0) local frenzy=false; local nativeAuto=false if attackController then local ok,debugState=pcall(function() return attackController:GetDebugState() end) if ok and type(debugState)=="table" then frenzy=debugState.IsFrenzyActive==true; nativeAuto=debugState.IsAutoAttackRunning==true end end damageState={strength=strength,damage=strength*(frenzy and (tonumber(frenzyConfig.DamageMultiplier) or 5) or 1), nativeAuto=nativeAuto,interval=cooldown/(frenzy and (tonumber(frenzyConfig.AttackSpeedMultiplier) or 3) or 1),frenzy=frenzy} end local lastAttack=0 local function cut() if not settings.Farm or os.clock()-lastAttack(tolerance or 1.5) then return result end return elseif inst then table.insert(ignore,inst); params.FilterDescendantsInstances=ignore from=result.Position+direction.Unit*0.2 else return end end end local function lineBlocked(from,to,tolerance) local horizontal=flat(to-from) if horizontal.Magnitude<0.5 then return false end local target1=Vector3.new(to.X,from.Y+1.25,to.Z) local target2=Vector3.new(to.X,from.Y+3.25,to.Z) return solidRayHit(from+Vector3.new(0,1.25,0),target1,tolerance)~=nil or solidRayHit(from+Vector3.new(0,3.25,0),target2,tolerance)~=nil end local function navigationGoal(from,destination,arrivalRadius) local radius=math.max(0,tonumber(arrivalRadius) or 3) local away=flat(from-destination) if radius>1 and away.Magnitude>radius then local backoff=math.max(0.75,radius-0.65) local p=destination+away.Unit*backoff return Vector3.new(p.X,from.Y,p.Z) end return Vector3.new(destination.X,from.Y,destination.Z) end local function candidateGoals(from,destination,arrivalRadius) local goals={navigationGoal(from,destination,arrivalRadius)} local radius=math.max(0,tonumber(arrivalRadius) or 0) if radius<1.75 then return goals end local toward=flat(from-destination) if toward.Magnitude<0.1 then toward=Vector3.new(0,0,1) else toward=toward.Unit end local right=Vector3.new(-toward.Z,0,toward.X) local ring=math.max(1.1,radius-0.7) for _,dir in ipairs({(toward+right).Unit,(toward-right).Unit,right,-right,-toward}) do local p=destination+dir*ring table.insert(goals,Vector3.new(p.X,from.Y,p.Z)) end return goals end local function buildPath(from,destination,arrivalRadius) local bestPath,bestPoints,bestCost for i,goal in ipairs(candidateGoals(from,destination,arrivalRadius)) do local path=PF:CreatePath(NAV_AGENT) local ok=pcall(function() path:ComputeAsync(from,goal) end) if ok and path.Status==Enum.PathStatus.Success then local points=path:GetWaypoints() if #points>=2 then local cost=0; local previous=from for _,wp in ipairs(points) do cost=cost+(wp.Position-previous).Magnitude; previous=wp.Position end if not bestCost or cost=untilTime end local function walk(destination, token, timeout, finished, arrivalRadius, allowReplan, arrivalObject) local _,h,r=character(); if not h then return false,"respawn" end local radius=math.max(0.5,arrivalRadius or 3) local initialDistance=(r.Position-destination).Magnitude local travelBudget=math.min(30,initialDistance/math.max(8,h.WalkSpeed)*1.9+4) local endTime=os.clock()+math.max(timeout or 15,travelBudget) local checkPlan=0; local startingZone=zonePlan and zonePlan.zone local route,points,index,blockedConnection local routeBlocked=false; local lastRepath=-100; local recoveries=0 local lastProgress=os.clock(); local trackedPoint; local bestTrackedDistance=math.huge local lastRootPosition=r.Position -- Keep a direct leg fixed while it is usable. v1.5 recalculated the arrival-ring goal from the -- avatar every 0.07s and re-issued MoveTo every loop, which could produce a start/stop "tap walk" -- on long, open base-to-zone runs. local directGoal=navigationGoal(r.Position,destination,radius) local lastMoveTarget; local lastMoveAt=-100 local function resetMoveCommand() lastMoveTarget=nil; lastMoveAt=-100 end local function issueMove(hum,target,force) if not target then return end local now=os.clock() if force or not lastMoveTarget or (target-lastMoveTarget).Magnitude>1.25 or now-lastMoveAt>0.65 then hum:MoveTo(target) lastMoveTarget=target lastMoveAt=now end end local function cleanup() if blockedConnection then blockedConnection:Disconnect(); blockedConnection=nil end end local function finish(ok,reason) cleanup(); stopMovement(); return ok,reason end local function installRoute(root,force) if not force and os.clock()-lastRepath<0.35 then return points~=nil end lastRepath=os.clock(); cleanup(); routeBlocked=false route,points=buildPath(root.Position,destination,radius); index=2 if not points then route=nil; index=nil directGoal=navigationGoal(root.Position,destination,radius) resetMoveCommand() return false end if #points<2 then index=1 end blockedConnection=route.Blocked:Connect(function(blockedIndex) if index and blockedIndex>=index then routeBlocked=true end end) trackedPoint=nil; bestTrackedDistance=math.huge; lastProgress=os.clock() resetMoveCommand() return true end -- Do not invoke PathfindingService just because the target is far away. If the whole direct leg is -- visibly clear, one sustained MoveTo is smoother and faster. Pathfinding is introduced only when -- a real hard obstacle is on the line or when movement actually stalls. if lineBlocked(r.Position,directGoal,1) then installRoute(r,true) end while active(token) and os.clock()checkPlan and replan then checkPlan=os.clock()+1; replan() if startingZone and (not zonePlan or zonePlan.zone~=startingZone) then return finish(false,"replan") end end cut() if routeBlocked then -- Dynamic grass is ignored as a hard wall. Repath only when the next path leg is genuinely blocked. local probe=(points and index and points[index] and points[index].Position) or directGoal if lineBlocked(root.Position,probe,1) then installRoute(root,true) else routeBlocked=false end elseif not points and lineBlocked(root.Position,directGoal,1) then installRoute(root,true) end local nextPoint=directGoal if points and index and points[index] then local waypoint=points[index] local reach=math.max(1.65,math.min(2.75,hum.WalkSpeed*0.075)) while waypoint and (root.Position-waypoint.Position).Magnitude<=reach do index=index+1; waypoint=points[index] lastProgress=os.clock(); trackedPoint=nil; bestTrackedDistance=math.huge; recoveries=0 resetMoveCommand() end if waypoint then -- Smooth path following: use the farthest nearby waypoint that is still directly visible. -- This avoids the animation repeatedly decelerating for 3-7 stud waypoint hops, while the -- line-of-sight check prevents cutting across actual walls/corners. Never skip a jump action. local chosen=index local maxIndex=math.min(#points,index+7) for j=index+1,maxIndex do local prev=points[j-1] local candidate=points[j] if (prev and prev.Action==Enum.PathWaypointAction.Jump) or (candidate and candidate.Action==Enum.PathWaypointAction.Jump) then break end if lineBlocked(root.Position,candidate.Position,0.8) then break end chosen=j end if chosen~=index then index=chosen; waypoint=points[index] trackedPoint=nil; bestTrackedDistance=math.huge resetMoveCommand() end nextPoint=waypoint.Position if waypoint.Action==Enum.PathWaypointAction.Jump then hum.Jump=true end else cleanup(); route=nil; points=nil; index=nil directGoal=navigationGoal(root.Position,destination,radius) nextPoint=directGoal trackedPoint=nil; bestTrackedDistance=math.huge resetMoveCommand() end end local trackDistance=(root.Position-nextPoint).Magnitude if not trackedPoint or (trackedPoint-nextPoint).Magnitude>0.5 then trackedPoint=nextPoint; bestTrackedDistance=trackDistance; lastProgress=os.clock() elseif trackDistance1.25 and trackDistance<=bestTrackedDistance+0.15 then -- Count real motion only when it is not sliding away from the current waypoint. lastProgress=os.clock(); bestTrackedDistance=math.min(bestTrackedDistance,trackDistance) end lastRootPosition=root.Position if os.clock()-lastProgress>1.35 then if arrivalObject and arrivalObject.Parent and objectSurfaceDistanceXZ(arrivalObject,root.Position)<=1.35 then return finish(true,"contact") end recoveries=recoveries+1 if recoveries>5 then return finish(false,"blocked") end local repathed=installRoute(root,true) if not repathed then escapeStep(hum,root,destination,token) if not active(token) then cleanup(); return false,"cancelled" end local _,_,after=character() if after then directGoal=navigationGoal(after.Position,destination,radius) installRoute(after,true) end else hum.Jump=true end lastProgress=os.clock(); bestTrackedDistance=math.huge resetMoveCommand() end issueMove(hum,nextPoint,false) task.wait(0.07) end return finish(false,active(token) and "leg" or "cancelled") end local function currentWorld() return tonumber(player:GetAttribute("CurrentWorld")) or 1 end local function worldInfo() return worlds.Worlds and worlds.Worlds[currentWorld()] end local function returnBase(token) say("Returning to base") stopMovement() local _,err=invoke("BaseTeleportService","TeleportToSpawn") if not active(token) then return false end local deadline=os.clock()+4 repeat task.wait(0.15) until not active(token) or player:GetAttribute("GD_IsInsideGrassZone")~=true or os.clock()>deadline if player:GetAttribute("GD_IsInsideGrassZone")==true or err then local info=worldInfo(); local root=workspace:FindFirstChild("Worlds") local world=root and info and root:FindFirstChild(info.WorldFolderName) local spawn=world and world:FindFirstChild(info.SpawnPointName,true) local p=position(spawn) if not p or not walk(p+Vector3.new(0,3,0),token,40,nil,3,false) then say("Base return failed; retrying"); return false end end for _=1,3 do if not active(token) then return false end invoke("LootInventoryService","ResetBackpackLoadAtBase") refreshBag() if bag.known and bag.load==0 then telemetry.returns=telemetry.returns+1; table.clear(health); return true end task.wait(0.4) end say("Waiting for server to clear carried load") return false end local function grassAlive(x) if not x.Parent or x:GetAttribute("GD_LocalGrassHidden")==true then return false end if health[x] and health[x].ratio<=0 then return false end if (ignored[x] or 0)>os.clock() then return false end if x:IsA("BasePart") then return x.Transparency<1 end local p=x.PrimaryPart or x:FindFirstChildWhichIsA("BasePart") return p and (p.Transparency<1 or x:GetAttribute("GD_LocalGrassVisualProxy")==true) end local tags=GRASS_TAGS local grassCache={}; local grassByZone={}; local lastScan=0; local scanWorld=-1 local grassDirty=true for _,tag in ipairs(tags) do connect(CS:GetInstanceAddedSignal(tag),function() grassDirty=true end) connect(CS:GetInstanceRemovedSignal(tag),function() grassDirty=true end) end local function refreshGrass() if lastScan>0 and scanWorld==currentWorld() and os.clock()-lastScan<(grassDirty and 0.75 or 20) then return end lastScan=os.clock(); scanWorld=currentWorld(); grassCache={}; grassByZone={}; grassDirty=false local seen={}; local root=workspace:FindFirstChild("Zones") if not root then grassDirty=true; lastScan=0; return end local info=worldInfo() for _,tag in ipairs(tags) do for _,x in ipairs(CS:GetTagged(tag)) do if x:IsDescendantOf(root) then local model=x:IsA("Model") and x or x:FindFirstAncestorOfClass("Model") if model and not model.Name:match("^Zone_%d+$") then x=model end local zone=x while zone and zone~=root and not zone.Name:match("^Zone_%d+$") do zone=zone.Parent end local n=zone and tonumber(zone.Name:match("^Zone_(%d+)$")) if n and not seen[x] and (not info or (n>=info.FirstZoneIndex and n<=info.LastZoneIndex)) then seen[x]=true local entry={object=x,zone=n} table.insert(grassCache,entry) grassByZone[n]=grassByZone[n] or {}; table.insert(grassByZone[n],entry) end end end end end -- Pure policy: choose a zone before choosing grass. No distance bonus can trap us in Zone 1. local function assessZone(index, strength, hp, interval, maxHits, maxSeconds, reward) if not hp or hp<=0 or strength<=0 then return nil end local hits=math.max(1,math.ceil(hp/strength)) local seconds=hits*math.max(0.05,interval) return {zone=index,hp=hp,hits=hits,seconds=seconds, suitable=hits<=maxHits and seconds<=maxSeconds, rate=(reward or 1)/(seconds+0.4),required=hp/math.max(1,math.min(maxHits,math.floor(maxSeconds/math.max(0.05,interval))))} end local function zoneHealth(index) local cell=zonesConfig.Cells and zonesConfig.Cells["Zone_"..index] local hp=cell and tonumber(cell.GrassHealth) or (zonesConfig.GrassHealthByIndex and tonumber(zonesConfig.GrassHealthByIndex[index])) if not hp then return end if grassExperiment and grassExperiment.GetHealthMultiplier then local ok,mult=pcall(grassExperiment.GetHealthMultiplier,player:GetAttribute(grassExperiment.PlayerAttributeName),index) if ok and tonumber(mult) then hp=hp*mult end end return hp end local function zoneAssessment(index) local reward=zonesConfig.GrassEssencePerGrassByIndex and zonesConfig.GrassEssencePerGrassByIndex[index] -- Travel decisions use sustained strength/cooldown, so a short Frenzy cannot strand us. return assessZone(index,damageState.strength,zoneHealth(index),cooldown,settings.Hits,settings.ClearTime,reward) end local lastPlanTime=0; local candidateZone,candidateSince replan=function(force) if not force and os.clock()-lastPlanTime<0.8 then return zonePlan end lastPlanTime=os.clock(); updateDamage() local info=worldInfo() if not info then zonePlan=nil; return end local choice for n=info.FirstZoneIndex,info.LastZoneIndex do local a=zoneAssessment(n) if a and a.suitable and (zoneBlocked[n] or 0)choice.rate*1.05) or (settings.ZoneMode~="Best essence rate" and n>choice.zone) then choice=a end end end local previous=zonePlan and zoneAssessment(zonePlan.zone) if choice and previous and previous.suitable and choice.zone~=previous.zone and (zoneBlocked[previous.zone] or 0)12 then table.remove(candidates) end end end end end local best,bestScore local radius=math.max(2,tonumber(player:GetAttribute("AttackRadiusRaw")) or 5) for _,candidate in ipairs(candidates) do local neighbours=0 for _,other in ipairs(candidates) do if other~=candidate and (candidate.point-other.point).Magnitude<=radius then neighbours=neighbours+1 end end local delta=candidate.point-r.Position local behind=delta.Magnitude>0.1 and math.max(0,-r.CFrame.LookVector:Dot(delta.Unit)) or 0 local ratio=health[candidate.object] and health[candidate.object].ratio or 1 local obstaclePenalty=lineBlocked(r.Position,candidate.point,radius+1) and 1.25 or 0 local score=clusterScore(candidate.distance,neighbours,ratio,behind,hum.WalkSpeed,damageState.interval)+obstaclePenalty if not bestScore or score=bag.capacity then return end local _,hum,r=character(); if not r then return end local selectedZone=zonePlan and zonePlan.zone if os.clock()-lootScanTime<0.25 and lootScanZone==selectedZone and lootScanPosition and (r.Position-lootScanPosition).Magnitude<3 then if not lootChoice then return end local p=lootChoice.prompt local expires=tonumber(inherited(p,"LootExpiresAt")) if p.Parent and p.Enabled and (ignored[p] or 0)pickupEstimate((r.Position-lootChoice.point).Magnitude,hum.WalkSpeed,p.HoldDuration)+1) then return lootChoice end end lootScanTime=os.clock(); lootScanPosition=r.Position; lootScanZone=selectedZone; lootChoice=nil local best,score for prompt in pairs(prompts) do if prompt.Parent and prompt.Enabled and prompt:GetAttribute("LootPickupPrompt")==true and (ignored[prompt] or 0)=info.FirstZoneIndex and zone<=info.LastZoneIndex and assessment and assessment.suitable and (not zonePlan or math.abs(zone-zonePlan.zone)<=1))) then local distance=(r.Position-p).Magnitude if distance<=settings.PickupDistance and remaining>distance/math.max(1,hum and hum.WalkSpeed or 16)+prompt.HoldDuration+1 then -- Rarity remains the priority; among equal rarities choose the faster pickup. local rank=rarity(prompt)*1000-pickupEstimate(distance,hum and hum.WalkSpeed or 16,prompt.HoldDuration) if not score or rank>score then best={prompt=prompt,point=p}; score=rank end end end end end lootChoice=best return best end local function promptRange(prompt) return math.max(1,tonumber(prompt and prompt.MaxActivationDistance) or 5) end local function inPromptRange(prompt,root,pos,margin) if not prompt or not root or not pos then return false end return (root.Position-pos).Magnitude<=math.max(0.75,promptRange(prompt)-(margin or 0.2)) end local function pickupArrivalRadius(rootPosition,promptPosition,maxDistance) -- walk() navigates in X/Z while ProximityPrompt uses a real 3D distance. Account for the avatar's -- height above ground loot so an arrival ring can never stop just outside activation distance. local usable=math.max(0.9,maxDistance-math.min(1.0,maxDistance*0.22)) local vertical=math.abs(rootPosition.Y-promptPosition.Y) local sq=usable*usable-vertical*vertical if sq<=0 then return 0.65 end return math.max(0.65,math.min(2.25,math.sqrt(sq))) end local function collect(item,token) local p=item.prompt if not p or not p.Parent then return end say("Collecting loot") local _,_,root=character(); if not root then return end local pos=position(p.Parent) or item.point; if not pos then return end local maxDistance=promptRange(p) local precisionRadius=pickupArrivalRadius(root.Position,pos,maxDistance) local goal=Vector3.new(pos.X,root.Position.Y,pos.Z) walk(goal,token,12,function() local _,_,r=character(); local live=position(p.Parent) return not p.Parent or not p.Enabled or bag.load>=bag.capacity or (r and live and inPromptRange(p,r,live,0.35)) end,precisionRadius,false) if not active(token) or not p.Parent or not p.Enabled or not bag.known or bag.load>=bag.capacity then return end -- The first route can still end slightly short on sloped floors or large loot models. Do one short -- precision approach to the prompt itself instead of abandoning a perfectly reachable pickup. local _,_,r=character(); pos=position(p.Parent) if r and pos and not inPromptRange(p,r,pos,0.15) then say("Closing in on loot") goal=Vector3.new(pos.X,r.Position.Y,pos.Z) walk(goal,token,4,function() local _,_,rr=character(); local live=position(p.Parent) return not p.Parent or not p.Enabled or bag.load>=bag.capacity or (rr and live and inPromptRange(p,rr,live,0.15)) end,0.6,false) end if not active(token) or not p.Parent or not p.Enabled or bag.load>=bag.capacity then return end local _,_,finalRoot=character(); local finalPos=position(p.Parent) if not finalRoot or not finalPos or not inPromptRange(p,finalRoot,finalPos,0.05) then -- Retry soon; do not blacklist it for 12 seconds just because the first arrival ring was marginal. ignored[p]=os.clock()+2.5 return end local oldLoad=bag.load; local oldRevision=bag.revision if type(fireproximityprompt)=="function" then fireproximityprompt(p) else p:InputHoldBegin() local deadline=os.clock()+p.HoldDuration+0.1 repeat task.wait(0.05) until not active(token) or os.clock()>deadline p:InputHoldEnd() end local deadline=os.clock()+1.5 repeat task.wait(0.1) until not active(token) or bag.revision>oldRevision or not p.Parent or os.clock()>deadline if not active(token) then return end if bag.revision<=oldRevision and p.Parent and p.Enabled then refreshBag() end if bag.load>oldLoad then telemetry.pickups=telemetry.pickups+1; ignored[p]=os.clock()+2 else ignored[p]=os.clock()+6 end end local cutterRetry={} local function buyCutter() local s=invoke("CuttersShopService","GetShopState") if type(s)~="table" or type(s.CuttersData)~="table" then return end local owned=s.OwnedCutters or {}; local current=s.CuttersData[s.CurrentCutter] or {} local power=tonumber(current.DamageMult) or 0; local best,bestPower=nil,power money=tonumber(invoke("DataService","GetData","Money")) or money for name,info in pairs(s.CuttersData) do if type(info)=="table" then local damage=tonumber(info.DamageMult); local price=tonumber(info.Price) if damage and damage>bestPower and (cutterRetry[name] or 0)=0 and price<=math.max(0,money-settings.Reserve) and info.HiddenUntilOwned~=true)) then best=name; bestPower=damage end end end if best and app.alive and settings.Cutter then local result,err=invoke("CuttersShopService","BuyCutter",best) if err or result==false or (type(result)=="table" and result.Success==false) then cutterRetry[best]=os.clock()+60; say("Cutter purchase unavailable: "..best) else local check=invoke("CuttersShopService","GetShopState") if type(check)=="table" and check.CurrentCutter==best then say("Equipped cutter: "..best) else cutterRetry[best]=os.clock()+60; say("Cutter not confirmed; will try another") end end end end local function claimQuests() local s=invoke("QuestProgressService","GetState") if type(s)~="table" then return end for _,category in pairs(s.Categories or {}) do for _,q in ipairs(category.Items or {}) do if not app.alive or not settings.Quests then return end if q.Complete==true and q.Claimed~=true and q.Id then invoke("QuestProgressService","ClaimQuest",q.Id); task.wait(0.2) end end end end local upgradeState={}; local upgradeLast={} for _,name in ipairs({"Carry","AttackSpeed","AttackRange","Speed"}) do listen("UpgradesService",name.."UpgradeUpdated",function(_,_,cost,maxed) upgradeState[name]={cost=tonumber(cost),maxed=maxed==true,time=os.clock()} end) end local function buyUpgrade(name) if not app.alive or not settings.Farm or not settings[name] then return end local state=upgradeState[name] if not state and settings.Reserve>0 then return end if state and (state.maxed or (state.cost and state.cost>math.max(0,money-settings.Reserve))) then return end -- One discovery request per minute if no initial state was replicated. if os.clock()-(upgradeLast[name] or -100)<(state and 4 or 60) then return end upgradeLast[name]=os.clock() fire("UpgradesService",name.."ButtonClicked") end local nextWorldCheck=0 local portalRetryAt=0 -- WorldsController considers the player "at" a world portal when their horizontal distance to a -- TriggerPlace/ClaimPart is <= 5 studs. Teleport_1 is the special portal that advertises world N+1. local function horizontalDistanceToPart(point,part) if not part or not part.Parent or not part:IsA("BasePart") then return math.huge end local q=part.CFrame:PointToObjectSpace(point) local half=part.Size*0.5 local dx=math.max(math.abs(q.X)-half.X,0) local dz=math.max(math.abs(q.Z)-half.Z,0) return math.sqrt(dx*dx+dz*dz) end local function nextWorldPortalPart(worldId) local info=worlds.Worlds and worlds.Worlds[worldId] local root=workspace:FindFirstChild("Worlds") local world=root and info and root:FindFirstChild(info.WorldFolderName) if not world then return end local teleport=world:FindFirstChild("Teleport_1") local trigger=teleport and teleport:FindFirstChild("TriggerPlace") local claim=trigger and trigger:FindFirstChild("ClaimPart") if claim and claim:IsA("BasePart") then return claim end -- Streaming can rebuild descendants; retain a strict hierarchy fallback instead of relying on position/name alone. for _,x in ipairs(world:GetDescendants()) do if x:IsA("BasePart") and x.Name=="ClaimPart" then local p=x.Parent; local tp=p and p.Parent if p and p.Name=="TriggerPlace" and tp and tp.Name=="Teleport_1" and tp.Parent==world then return x end end end end local function worldUnlocked(worldId,current) local unlocked=tonumber(player:GetAttribute("HighestUnlockedWorld")) or current if unlocked>=worldId then return true end -- The billboard can update just before the replicated attribute reaches our worker. Sync once when nearby. local state=invoke("WorldService","GetState") if type(state)=="table" then unlocked=tonumber(state.HighestUnlockedWorld) or unlocked end return unlocked>=worldId end local function performWorldTeleport(worldId,current,fromPortal) if not app.alive then return false end stopMovement() say((fromPortal and "Using new-world portal -> World " or "Travelling to World ")..worldId) local started=false if worldTransitionController then local ok,transitionOk,result=pcall(function() return worldTransitionController:TeleportToWorld(worldId) end) started=ok and transitionOk==true and (type(result)~="table" or result.Success~=false) end if not started and app.alive then local result,err=invoke("WorldService","TeleportToWorld",worldId) started=not err and result~=false and (type(result)~="table" or result.Success~=false) end local untilTime=os.clock()+10 repeat task.wait(0.15) until not app.alive or currentWorld()~=current or os.clock()>untilTime if currentWorld()~=current then portalRetryAt=0 return true end if fromPortal then say(started and "Portal transition did not complete; retrying soon" or "Portal use failed; retrying soon") end return false end local function advanceWorld(token) if not settings.AutoWorld or settings.Zone~="Auto" then return false end local current=currentWorld(); local nextId=current+1 local nextInfo=worlds.Worlds and worlds.Worlds[nextId] if not nextInfo then return false end -- Priority path: if normal farming naturally puts us beside the NEW WORLD portal, use it immediately. -- Do not return to base first: that would walk away from the portal the player has already reached. local _,_,root=character() local portal=root and nextWorldPortalPart(current) if portal and horizontalDistanceToPart(root.Position,portal)<=5.25 and os.clock()>=portalRetryAt then portalRetryAt=os.clock()+2 if worldUnlocked(nextId,current) then return performWorldTeleport(nextId,current,true) end local required=tonumber(nextInfo.RequiredLevel) if required then say("At new-world portal | World "..nextId.." unlocks at level "..required) end return false end -- Existing long-range progression policy remains conservative when we are not already at the portal. if os.clock()0 and not returnBase(token) then return false end if not active(token) then return false end return performWorldTeleport(nextId,current,false) end local farm=window:CreateTab("Farm") farm:CreateSection("Automation") local controls={} local function toggle(tab,name,keyName) local control=tab:CreateToggle({Name=name,Flag="CGA_"..keyName,CurrentValue=settings[keyName],Callback=function(v) settings[keyName]=v==true if keyName=="Farm" then app.generation=app.generation+1; if not v then stopMovement(); say("Stopped") end end end}) controls[keyName]=control if control.Get then settings[keyName]=control:Get()==true end end toggle(farm,"Auto farm grass + loot","Farm") toggle(farm,"Auto train (also when farming is off)","Train") toggle(farm,"Train strength while farming","SmartTrain") toggle(farm,"Collect dropped loot","Loot") toggle(farm,"Auto sell ALL sellable loot","Sell") farm:CreateParagraph({Title="Backpack",Content="A full carried load returns to base. Auto Sell sells all sellable loot on base visits; keep it off to retain your collection.",Height=64}) farm:CreateDropdown({Name="Automatic zone strategy",Flag="CGA_ZoneMode",Options={"Highest suitable","Best essence rate"},CurrentOption={"Highest suitable"},Callback=function(v) settings.ZoneMode=type(v)=="table" and v[1] or v; zonePlan=nil; replan(true) end}) farm:CreateSlider({Name="Maximum seconds per grass",Flag="CGA_ClearTime",Range={0.5,5},Increment=0.1,CurrentValue=2,Callback=function(v) settings.ClearTime=v; replan(true) end}) toggle(farm,"Use nearby new-world portal / auto advance","AutoWorld") local options={"Auto"}; for i=1,57 do table.insert(options,tostring(i)) end farm:CreateDropdown({Name="Zone",Flag="CGA_Zone",Options=options,CurrentOption={"Auto"},Callback=function(v) settings.Zone=type(v)=="table" and v[1] or v; zonePlan=nil; app.generation=app.generation+1; replan(true) end}) farm:CreateSlider({Name="Maximum hits per grass",Flag="CGA_Hits",Range={1,20},Increment=1,CurrentValue=4,Callback=function(v) settings.Hits=v; replan(true) end}) farm:CreateSlider({Name="Loot search distance",Flag="CGA_LootDistance",Range={10,200},Increment=5,CurrentValue=90,Callback=function(v) settings.PickupDistance=v end}) local progress=window:CreateTab("Progress") progress:CreateSection("Purchases & rewards") toggle(progress,"Buy / equip stronger affordable cutter","Cutter") toggle(progress,"Claim completed quests","Quests") toggle(progress,"Auto rebirth when eligible (resets progress)","Rebirth") progress:CreateButton({Name="Claim free offline reward",Callback=function() invoke("DataService","ClaimOfflineReward") end}) progress:CreateSlider({Name="Keep cash in reserve",Flag="CGA_Reserve",Range={0,100000},Increment=100,CurrentValue=0,Callback=function(v) settings.Reserve=v end}) progress:CreateSection("Cash upgrades") progress:CreateLabel("Skips known maxed / unaffordable upgrades. Unknown upgrades probe at most once per minute.") for _,v in ipairs({{"Carry capacity","Carry"},{"Attack speed","AttackSpeed"},{"Attack range","AttackRange"},{"Walk speed","Speed"}}) do toggle(progress,v[1],v[2]) end local stats=window:CreateTab("Status") stats:CreateSection("Live status") statusLabel=stats:CreateParagraph({Title="Activity",Content="Reading...",Height=68}) zoneLabel=stats:CreateParagraph({Title="Selected zone",Content="Reading...",Height=76}) nextZoneLabel=stats:CreateParagraph({Title="Next zone",Content="Reading...",Height=68}) bagLabel=stats:CreateParagraph({Title="Backpack & cash",Content="Reading...",Height=64}) detailLabel=stats:CreateParagraph({Title="Session",Content="Reading...",Height=130}) stats:CreateButton({Name="Stop all automation",Callback=function() for _,k in ipairs({"Farm","Train","Cutter","Quests","Rebirth","Carry","AttackSpeed","AttackRange","Speed","SmartTrain","AutoWorld"}) do settings[k]=false; if controls[k] then controls[k]:Set(false) end end app.generation=app.generation+1; stopMovement(); say("Stopped") end}) local config=window:CreateTab("Settings") config:CreateSection("Session") toggle(config,"Anti-AFK","AntiAFK") config:CreateSlider({Name="Training interval",Flag="CGA_TrainInterval",Range={0.15,1},Increment=0.05,CurrentValue=0.2,Callback=function(v) settings.TrainInterval=v end}) config:CreateButton({Name="Unload PuckAFK",Callback=app.Stop}) connect(player.Idled,function() if not app.alive or not settings.AntiAFK then return end pcall(function() local vu=game:GetService("VirtualUser"); vu:CaptureController(); vu:ClickButton2(Vector2.zero) end) end) worker(function() refreshBag(); money=tonumber(invoke("DataService","GetData","Money")) or 0 cooldown=math.max(0.05,tonumber(invoke("DataService","GetAttackCooldown")) or 0.5) local nextSell=0; local emptySince; local failures={} while app.alive do local ok,err=pcall(function() if not settings.Farm then task.wait(0.3); return end if os.clock()<(app.transitionUntil or 0) then say("Waiting for rebirth to settle"); task.wait(0.3); return end local _,_,r=character(); if not r then say("Waiting for respawn"); task.wait(0.5); return end local token=app.generation if not bag.known or os.clock()-bag.updated>12 then refreshBag() end if not active(token) then return end if not bag.known then say("Waiting for backpack state"); task.wait(1); return end replan() if bag.load>=bag.capacity then if not returnBase(token) then task.wait(2); return end end if not active(token) then return end if settings.Sell and bag.load==0 and player:GetAttribute("GD_IsInsideGrassZone")~=true and os.clock()>nextSell then -- Sell during natural base visits, never interrupt a partly filled run. nextSell=os.clock()+5 if not active(token) then return end local result,e=invoke("DataService","SellAllBackpackLoot") if e or result==false or (type(result)=="table" and result.Success==false) then say("Sale unavailable; will retry") end refreshBag() end if not active(token) then return end if advanceWorld(token) or not active(token) then return end replan() if not zonePlan then say(settings.SmartTrain and "Training: no zone fits the hit / time limits" or "No suitable zone: enable training or adjust limits") task.wait(0.5); return end local loot=targetLoot() if loot then collect(loot,token); return end local target=targetGrass() if not target then local approach=zoneApproach(zonePlan.zone) local _,_,rootNow=character() if approach and rootNow and (rootNow.Position-approach).Magnitude>8 then say("Approaching Zone "..zonePlan.zone.." to load grass") local _,reason=walk(approach,token,8,nil,4) if reason=="blocked" and zonePlan then local n=zonePlan.zone failures[n]=(failures[n] or 0)+1 say("Zone route blocked: recalculating") if failures[n]>=3 then zoneBlocked[n]=os.clock()+15; failures[n]=0; replan(true) end end lastScan=0; emptySince=nil; return end emptySince=emptySince or os.clock() say("Waiting for Zone "..zonePlan.zone.." grass to stream / reset") if os.clock()-emptySince>8 then local n=zonePlan.zone if player:GetAttribute("GD_IsInsideGrassZone")==true then returnBase(token) end zoneBlocked[n]=os.clock()+15; emptySince=nil; replan(true) end task.wait(0.5); return end emptySince=nil say("Farming Zone "..target.zone.." | "..zonePlan.hits.." expected hits") local _,_,root=character(); if not root then return end local point=Vector3.new(target.point.X,root.Position.Y,target.point.Z) local radius=math.max(1,math.min(tonumber(player:GetAttribute("AttackRadiusRaw")) or 5,6)-1) local diversion=false local reached,reason=walk(point,token,8,function() if not grassAlive(target.object) or bag.load>=bag.capacity then return true end local item=targetLoot() if item then diversion=true; return true end return false end,radius,true,target.object) if not active(token) or diversion or bag.load>=bag.capacity then return end if reason=="replan" or reason=="leg" then return end if not reached then if reason=="blocked" then ignored[target.object]=os.clock()+10; telemetry.failures=telemetry.failures+1 failures[target.zone]=(failures[target.zone] or 0)+1 lastScan=0 say("Route blocked: choosing another grass") if failures[target.zone]>=6 then failures[target.zone]=0; grassDirty=true end end return end local untilTime=os.clock()+math.max(2,settings.ClearTime+1) while active(token) and grassAlive(target.object) and bag.loadradius and objectSurfaceDistanceXZ(target.object,rootNow.Position)>1.15 then h:MoveTo(navigationGoal(rootNow.Position,point,radius)) end cut(); task.wait(0.08) end stopMovement() if active(token) and grassAlive(target.object) and os.clock()>=untilTime then ignored[target.object]=os.clock()+15 failures[target.zone]=(failures[target.zone] or 0)+1 if failures[target.zone]>=3 then zoneBlocked[target.zone]=os.clock()+30; failures[target.zone]=0; replan(true) say("Zone stalled: trying an easier zone") end elseif not grassAlive(target.object) then failures[target.zone]=0 end end) if not ok then say("Farm error: "..tostring(err)); warn("[PuckAFK]",err); task.wait(2) end task.wait(0.04) end end) worker(function() while app.alive do if (settings.Train or (settings.Farm and settings.SmartTrain)) and character() then pcall(fire,"StrengthService","ClickRequested") end task.wait(math.max(0.15,settings.TrainInterval)) end end) worker(function() local due={cutter=0,quests=0,upgrades=0,rebirth=0} while app.alive do task.wait(0.5) if app.alive then local ok,err=pcall(function() local now=os.clock() if settings.Cutter and now>=due.cutter then due.cutter=now+8; buyCutter() end if settings.Quests and now>=due.quests then due.quests=now+20; claimQuests() end if settings.Farm and now>=due.upgrades then due.upgrades=now+5 for _,name in ipairs({"Carry","AttackSpeed","AttackRange","Speed"}) do if app.alive and settings[name] then buyUpgrade(name); task.wait(0.7) end end end if settings.Rebirth and now>=due.rebirth then due.rebirth=now+5 local s=invoke("RebirtService","GetState") if app.alive and settings.Rebirth and type(s)=="table" and s.CanRebirth==true then app.generation=app.generation+1; app.transitionUntil=os.clock()+3; zonePlan=nil; table.clear(zoneBlocked); stopMovement(); fire("RebirtService","RebirthButtonClicked") end end end) if not ok then say("Progress error: "..tostring(err)) end end end end) worker(function() local lastTick=os.clock() while app.alive do local now=os.clock() if settings.Farm and character() then farmSeconds=farmSeconds+math.min(1,now-lastTick) end lastTick=now replan() statusLabel:Set(app.message) local plan=zonePlan zoneLabel:Set(plan and ("Selected Zone "..plan.zone.." | HP "..string.format("%.4g",plan.hp).." | "..plan.hits.." hits / "..string.format("%.2f",plan.seconds).."s") or "No suitable zone under current limits") local info=worldInfo(); local nextIndex=plan and plan.zone+1 or (info and info.FirstZoneIndex) local nextInfo=nextIndex and info and nextIndex<=info.LastZoneIndex and zoneAssessment(nextIndex) nextZoneLabel:Set(damageState.strength<=0 and "Waiting for strength data / first training click" or nextInfo and ("Zone "..nextIndex.." needs about "..string.format("%.4g",nextInfo.required).." sustained strength") or "End of current world's zones") bagLabel:Set("Carried: "..bag.load.." / "..bag.capacity.." | Cash: "..string.format("%.4g",money)) detailLabel:Set("World "..currentWorld().." | Strength "..string.format("%.4g",tonumber(player:GetAttribute("StrengthRaw")) or 0).." | Damage: "..string.format("%.4g",damageState.damage)..(damageState.frenzy and " (Frenzy)" or "").." | Cuts: "..telemetry.cuts.." | Pickups: "..telemetry.pickups.." | Returns: "..telemetry.returns.." | Nav retries: "..telemetry.failures.." | Cuts/min: "..string.format("%.1f",telemetry.cuts*60/math.max(1,farmSeconds))) task.wait(0.5) end end) UI:Notify({Title="PuckAFK ready",Content="Enable Auto farm. Power training and practical zone selection run together. K toggles the UI.",Duration=6})