GARDEN HORIZON OPEN SOURCE REAL
Key Required

GARDEN HORIZON OPEN SOURCE REAL

[MYTHICAL] Garden Horizons 🌱pradaxcapradaxca 578 views 138 executesUpdated Mar 15, 2026
0.0/ 5(1 votes)
010% Positive
Lua Script•1572 lines (63897 chars)
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local CoreGui = game:GetService("CoreGui")
local StarterGui = game:GetService("StarterGui")
local TweenService = game:GetService("TweenService")

local LocalPlayer = Players.LocalPlayer
local PlantRemote = ReplicatedStorage.RemoteEvents:WaitForChild("PlantSeed")
local PurchaseShopItemRemote = ReplicatedStorage.RemoteEvents:FindFirstChild("PurchaseShopItem")
local GetShopDataRemote = ReplicatedStorage.RemoteEvents:FindFirstChild("GetShopData")
local ClaimQuestRemote = ReplicatedStorage.RemoteEvents:FindFirstChild("ClaimQuest")
local RequestQuestsRemote = ReplicatedStorage.RemoteEvents:FindFirstChild("RequestQuests")
local UpdateQuestsRemote = ReplicatedStorage.RemoteEvents:FindFirstChild("UpdateQuests")
local SellItemsRemote = ReplicatedStorage.RemoteEvents:FindFirstChild("SellItems")

local ItemInventory = nil
pcall(function()
    ItemInventory = require(ReplicatedStorage:WaitForChild("Inventory"):WaitForChild("ItemInventory"))
end)

local SeedShopData = nil
pcall(function()
    SeedShopData = require(ReplicatedStorage:WaitForChild("Shop"):WaitForChild("ShopData"):WaitForChild("SeedShopData"))
end)

local GearShopData = nil
pcall(function()
    GearShopData = require(ReplicatedStorage:WaitForChild("Shop"):WaitForChild("ShopData"):WaitForChild("GearShopData"))
end)

local Settings = {
    Enabled = false,
    AutoHarvestTeleport = false,
    IgnoreFavorited = true,
    AutoPlantAtCharacter = false,
    AutoEquipPlantSeeds = false,
    SavedPlantPosition = nil,
    SelectedSeed = nil,
    TeleportToShopOnBuy = true,
    CheckSeedStockBeforeBuy = true,
    AutoBuyLoop = false,
    AutoBuyDelay = 1.0,
    SeedShopNpcPosition = Vector3.new(177, 204, 672),
    SelectedGear = nil,
    TeleportToGearShopOnBuy = true,
    CheckGearStockBeforeBuy = true,
    AutoBuyGearLoop = false,
    AutoBuyGearDelay = 1.0,
    GearShopNpcPosition = Vector3.new(212, 204, 609),
    AutoSellLoop = false,
    AutoSellDelay = 1.0,
    AutoSellOnlyWhenInventoryFull = false,
    InventoryFullSellCooldown = 1.0,
    SellMode = "Sell All",
    TeleportToSellNpcOnSell = true,
    SellNpcPosition = Vector3.new(150, 204, 674),
    AutoClaimQuests = false,
    AutoClaimQuestDelay = 1.0,
    Range = 50,
    HarvestBatchSize = 10,
    Delay = 0.1,
}

local lastPlantTime = 0
local lastHarvestTeleportTime = 0
local lastAutoBuyTime = 0
local lastAutoBuyGearTime = 0
local lastAutoSellTime = 0
local lastInventoryFullSellTime = 0
local lastAutoClaimQuestTime = 0
local warnedMissingSavedPosition = false
local warnedMissingQuestRemotes = false
local warnedMissingSellRemotes = false
local harvestCooldownByPrompt = {}
local harvestFailCountByPrompt = {}
local harvestBlacklistUntilByPrompt = {}
local HARVEST_PROMPT_SCAN_INTERVAL = 0.35
local harvestPromptScanCache = {}
local harvestPromptScanCacheAt = 0
local latestQuestData = nil
local INVENTORY_FULL_TEXT = "Your inventory is full! Sell or remove items to make space"

if UpdateQuestsRemote and UpdateQuestsRemote:IsA("RemoteEvent") then
    UpdateQuestsRemote.OnClientEvent:Connect(function(data)
        if type(data) == "table" then
            latestQuestData = data
        end
    end)
end

local function Notify(text, duration)
    pcall(function()
        StarterGui:SetCore("SendNotification", {
            Title = "Garden Horizons",
            Text = text,
            Duration = duration or 3
        })
    end)
end

local function CreateSimpleUI()
    local ui = { Tabs = {} }
    
    local oldGui = CoreGui:FindFirstChild("GardenHorizonsPinkUI")
    if oldGui then oldGui:Destroy() end

    local sg = Instance.new("ScreenGui")
    sg.Name = "GardenHorizonsPinkUI"
    sg.ResetOnSpawn = false
    pcall(function() sg.Parent = CoreGui end)
    if not sg.Parent then sg.Parent = LocalPlayer:WaitForChild("PlayerGui") end

    local function MakeDraggable(guiObject)
        local dragging, dragInput, dragStart, startPos
        guiObject.InputBegan:Connect(function(input)
            if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
                dragging = true
                dragStart = input.Position
                startPos = guiObject.Position
            end
        end)
        guiObject.InputChanged:Connect(function(input)
            if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then 
                dragInput = input 
            end
        end)
        UserInputService.InputChanged:Connect(function(input)
            if input == dragInput and dragging then
                local delta = input.Position - dragStart
                guiObject.Position = UDim2.new(
                    startPos.X.Scale, startPos.X.Offset + delta.X, 
                    startPos.Y.Scale, startPos.Y.Offset + delta.Y
                )
            end
        end)
        UserInputService.InputEnded:Connect(function(input)
            if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then 
                dragging = false 
            end
        end)
    end

    local main = Instance.new("Frame", sg)
    main.AnchorPoint = Vector2.new(0.5, 0.5)
    main.Size = UDim2.new(0, 550, 0, 380)
    main.Position = UDim2.new(0.5, 0, 0.5, 0)
    main.BackgroundColor3 = Color3.fromRGB(255, 224, 235)
    main.BackgroundTransparency = 1 
    main.BorderSizePixel = 0
    main.Active = true
    main.ClipsDescendants = true
    Instance.new("UICorner", main).CornerRadius = UDim.new(0, 10)
    MakeDraggable(main)

    local bgImage = Instance.new("ImageLabel", main)
    bgImage.Name = "CustomBackground"
    bgImage.Size = UDim2.new(1, 0, 1, 0)
    bgImage.Position = UDim2.new(0, 0, 0, 0)
    bgImage.BackgroundTransparency = 0
    bgImage.BackgroundColor3 = Color3.fromRGB(255, 224, 235)
    bgImage.Image = "rbxassetid://95101112877359" 
    bgImage.ScaleType = Enum.ScaleType.Crop
    bgImage.ZIndex = 0
    Instance.new("UICorner", bgImage).CornerRadius = UDim.new(0, 10)

    local topBar = Instance.new("Frame", main)
    topBar.Size = UDim2.new(1, 0, 0, 50)
    topBar.BackgroundColor3 = Color3.fromRGB(255, 192, 216)
    topBar.BackgroundTransparency = 0.25 
    topBar.BorderSizePixel = 0
    topBar.ZIndex = 1
    Instance.new("UICorner", topBar).CornerRadius = UDim.new(0, 10)
    
    local cover = Instance.new("Frame", main)
    cover.Size = UDim2.new(1, 0, 0, 10)
    cover.Position = UDim2.new(0, 0, 0, 40)
    cover.BackgroundColor3 = Color3.fromRGB(255, 192, 216)
    cover.BackgroundTransparency = 0.25 
    cover.BorderSizePixel = 0
    cover.ZIndex = 1

    local tabContainer = Instance.new("Frame", topBar)
    tabContainer.Size = UDim2.new(1, -50, 1, 0) 
    tabContainer.BackgroundTransparency = 1
    tabContainer.ZIndex = 2

    local tabLayout = Instance.new("UIListLayout", tabContainer)
    tabLayout.FillDirection = Enum.FillDirection.Horizontal
    tabLayout.SortOrder = Enum.SortOrder.LayoutOrder
    tabLayout.Padding = UDim.new(0, 8)
    tabLayout.VerticalAlignment = Enum.VerticalAlignment.Center

    local topPad = Instance.new("UIPadding", tabContainer)
    topPad.PaddingLeft = UDim.new(0, 15)

    local contentContainer = Instance.new("Frame", main)
    contentContainer.Size = UDim2.new(1, 0, 1, -50)
    contentContainer.Position = UDim2.new(0, 0, 0, 50)
    contentContainer.BackgroundTransparency = 1
    contentContainer.ClipsDescendants = true
    contentContainer.ZIndex = 2

    local colorText = Color3.fromRGB(100, 50, 80)
    local colorPanelBg = Color3.fromRGB(255, 240, 246)
    local colorActive = Color3.fromRGB(255, 140, 190)
    local colorInactive = Color3.fromRGB(240, 190, 210)

    -- Toggle Button Diubah Menjadi ImageButton
    local toggleUIBtn = Instance.new("ImageButton", sg)
    toggleUIBtn.AnchorPoint = Vector2.new(0.5, 0.5)
    toggleUIBtn.Size = UDim2.new(0, 50, 0, 50) -- Ukuran normal
    toggleUIBtn.Position = UDim2.new(0, 50, 0.5, 0)
    toggleUIBtn.BackgroundTransparency = 1 -- Transparan agar gambar dominan
    toggleUIBtn.Image = "rbxassetid://126475933417799"
    toggleUIBtn.ScaleType = Enum.ScaleType.Fit
    toggleUIBtn.Visible = false
    MakeDraggable(toggleUIBtn)

    local minimizeBtn = Instance.new("TextButton", topBar)
    minimizeBtn.Size = UDim2.new(0, 30, 0, 30)
    minimizeBtn.Position = UDim2.new(1, -40, 0.5, -15)
    minimizeBtn.BackgroundColor3 = Color3.fromRGB(255, 140, 190)
    minimizeBtn.Text = "-"
    minimizeBtn.Font = Enum.Font.GothamBold
    minimizeBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
    minimizeBtn.TextSize = 20
    minimizeBtn.ZIndex = 3
    Instance.new("UICorner", minimizeBtn).CornerRadius = UDim.new(0, 6)

    local isUIOpen = true
    local function ToggleUI()
        isUIOpen = not isUIOpen
        if isUIOpen then
            TweenService:Create(toggleUIBtn, TweenInfo.new(0.3, Enum.EasingStyle.Back, Enum.EasingDirection.In), {Size = UDim2.new(0, 0, 0, 0)}):Play()
            task.delay(0.3, function() toggleUIBtn.Visible = false end)
            
            main.Visible = true
            main.Size = UDim2.new(0, 0, 0, 0)
            TweenService:Create(main, TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Size = UDim2.new(0, 550, 0, 380)}):Play()
        else
            TweenService:Create(main, TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.In), {Size = UDim2.new(0, 0, 0, 0)}):Play()
            task.delay(0.4, function() 
                if not isUIOpen then 
                    main.Visible = false 
                    toggleUIBtn.Visible = true
                    toggleUIBtn.Size = UDim2.new(0, 0, 0, 0)
                    -- Ukuran balikan Tween disesuaikan ke ukuran icon normal (50x50)
                    TweenService:Create(toggleUIBtn, TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Size = UDim2.new(0, 50, 0, 50)}):Play()
                end 
            end)
        end
    end

    minimizeBtn.MouseButton1Click:Connect(ToggleUI)
    toggleUIBtn.MouseButton1Click:Connect(ToggleUI)

    UserInputService.InputBegan:Connect(function(input, gameProcessed)
        if not gameProcessed and input.KeyCode == Enum.KeyCode.RightShift then ToggleUI() end
    end)

    local activePage = nil

    function ui:AddTab(name)
        local tabBtn = Instance.new("TextButton", tabContainer) 
        tabBtn.Size = UDim2.new(0, 100, 0, 34)
        tabBtn.BackgroundColor3 = colorActive
        tabBtn.Text = name
        tabBtn.Font = Enum.Font.GothamBold
        tabBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
        tabBtn.TextSize = 13
        tabBtn.ZIndex = 3
        Instance.new("UICorner", tabBtn).CornerRadius = UDim.new(0, 8)

        local page = Instance.new("ScrollingFrame", contentContainer)
        page.Size = UDim2.new(1, 0, 1, 0)
        page.Position = UDim2.new(1, 0, 0, 0)
        page.BackgroundTransparency = 1
        page.Visible = false
        page.ScrollBarThickness = 5
        page.ScrollBarImageColor3 = colorActive
        page.AutomaticCanvasSize = Enum.AutomaticSize.Y
        page.CanvasSize = UDim2.new(0, 0, 0, 0)
        page.ZIndex = 3

        local list = Instance.new("UIListLayout", page)
        list.SortOrder = Enum.SortOrder.LayoutOrder
        list.Padding = UDim.new(0, 8)

        local pad = Instance.new("UIPadding", page)
        pad.PaddingTop = UDim.new(0, 15)
        pad.PaddingLeft = UDim.new(0, 15)
        pad.PaddingRight = UDim.new(0, 15)
        pad.PaddingBottom = UDim.new(0, 20)

        tabBtn.MouseButton1Click:Connect(function()
            if activePage == page then return end
            
            for _, t in pairs(ui.Tabs) do
                t.Button.BackgroundColor3 = Color3.fromRGB(255, 204, 224)
                t.Button.TextColor3 = colorText
            end
            
            tabBtn.BackgroundColor3 = colorActive
            tabBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
            
            if activePage then
                local oldPage = activePage
                TweenService:Create(oldPage, TweenInfo.new(0.35, Enum.EasingStyle.Quart, Enum.EasingDirection.Out), {Position = UDim2.new(-1, 0, 0, 0)}):Play()
                task.delay(0.35, function() if activePage ~= oldPage then oldPage.Visible = false end end)
            end
            
            page.Visible = true
            page.Position = UDim2.new(1, 0, 0, 0)
            TweenService:Create(page, TweenInfo.new(0.35, Enum.EasingStyle.Quart, Enum.EasingDirection.Out), {Position = UDim2.new(0, 0, 0, 0)}):Play()
            
            activePage = page
        end)

        table.insert(ui.Tabs, {Button = tabBtn, Page = page})
        
        if #ui.Tabs == 1 then
            page.Visible = true
            page.Position = UDim2.new(0, 0, 0, 0)
            activePage = page
            tabBtn.BackgroundColor3 = colorActive
            tabBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
        else
            tabBtn.BackgroundColor3 = Color3.fromRGB(255, 204, 224)
            tabBtn.TextColor3 = colorText
        end

        local tabObj = {}

        function tabObj:AddLabel(text)
            local lbl = Instance.new("TextLabel", page)
            lbl.Size = UDim2.new(1, 0, 0, 25)
            lbl.BackgroundTransparency = 1
            lbl.Text = text
            lbl.TextColor3 = Color3.fromRGB(255, 255, 255)
            lbl.Font = Enum.Font.GothamBold
            lbl.TextSize = 15
            lbl.TextXAlignment = Enum.TextXAlignment.Left
            lbl.ZIndex = 4
            return lbl
        end

        function tabObj:AddToggle(text, default, callback)
            local container = Instance.new("Frame", page)
            container.Size = UDim2.new(1, 0, 0, 35)
            container.BackgroundColor3 = colorPanelBg
            container.BackgroundTransparency = 0.35
            container.ZIndex = 4
            Instance.new("UICorner", container).CornerRadius = UDim.new(0, 8)

            local lbl = Instance.new("TextLabel", container)
            lbl.Size = UDim2.new(1, -50, 1, 0)
            lbl.Position = UDim2.new(0, 10, 0, 0)
            lbl.BackgroundTransparency = 1
            lbl.Text = text
            lbl.TextColor3 = colorText
            lbl.Font = Enum.Font.GothamBold
            lbl.TextSize = 14
            lbl.TextXAlignment = Enum.TextXAlignment.Left
            lbl.ZIndex = 5

            local btn = Instance.new("TextButton", container)
            btn.Size = UDim2.new(0, 40, 0, 20)
            btn.Position = UDim2.new(1, -50, 0.5, -10)
            btn.BackgroundColor3 = default and colorActive or colorInactive
            btn.Text = ""
            btn.ZIndex = 5
            Instance.new("UICorner", btn).CornerRadius = UDim.new(1, 0)
            
            local circle = Instance.new("Frame", btn)
            circle.Size = UDim2.new(0, 16, 0, 16)
            circle.Position = default and UDim2.new(1, -18, 0, 2) or UDim2.new(0, 2, 0, 2)
            circle.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
            circle.ZIndex = 6
            Instance.new("UICorner", circle).CornerRadius = UDim.new(1, 0)

            local state = default
            btn.MouseButton1Click:Connect(function()
                state = not state
                TweenService:Create(btn, TweenInfo.new(0.2), {BackgroundColor3 = state and colorActive or colorInactive}):Play()
                TweenService:Create(circle, TweenInfo.new(0.25, Enum.EasingStyle.Quart, Enum.EasingDirection.Out), {Position = state and UDim2.new(1, -18, 0, 2) or UDim2.new(0, 2, 0, 2)}):Play()
                callback(state)
            end)
            callback(state)
        end

        function tabObj:AddButton(text, callback)
            local btn = Instance.new("TextButton", page)
            btn.Size = UDim2.new(1, 0, 0, 35)
            btn.BackgroundColor3 = colorActive
            btn.Text = text
            btn.TextColor3 = Color3.fromRGB(255, 255, 255)
            btn.Font = Enum.Font.GothamBold
            btn.TextSize = 14
            btn.ZIndex = 4
            Instance.new("UICorner", btn).CornerRadius = UDim.new(0, 8)
            btn.MouseButton1Click:Connect(callback)
        end

        function tabObj:AddSlider(text, min, max, default, callback)
            local container = Instance.new("Frame", page)
            container.Size = UDim2.new(1, 0, 0, 55)
            container.BackgroundColor3 = colorPanelBg
            container.BackgroundTransparency = 0.35
            container.ZIndex = 4
            Instance.new("UICorner", container).CornerRadius = UDim.new(0, 8)

            local lbl = Instance.new("TextLabel", container)
            lbl.Size = UDim2.new(1, -20, 0, 25)
            lbl.Position = UDim2.new(0, 10, 0, 5)
            lbl.BackgroundTransparency = 1
            lbl.Text = text .. ": " .. tostring(default)
            lbl.TextColor3 = colorText
            lbl.Font = Enum.Font.GothamBold
            lbl.TextSize = 14
            lbl.TextXAlignment = Enum.TextXAlignment.Left
            lbl.ZIndex = 5

            local bar = Instance.new("TextButton", container)
            bar.Size = UDim2.new(1, -20, 0, 8)
            bar.Position = UDim2.new(0, 10, 0, 35)
            bar.BackgroundColor3 = colorInactive
            bar.Text = ""
            bar.AutoButtonColor = false
            bar.ZIndex = 5
            Instance.new("UICorner", bar).CornerRadius = UDim.new(1, 0)

            local fill = Instance.new("Frame", bar)
            fill.Size = UDim2.new(math.clamp((default - min) / (max - min), 0, 1), 0, 1, 0)
            fill.BackgroundColor3 = colorActive
            fill.ZIndex = 6
            Instance.new("UICorner", fill).CornerRadius = UDim.new(1, 0)

            local dragging = false
            local function update(input)
                local pos = math.clamp((input.Position.X - bar.AbsolutePosition.X) / bar.AbsoluteSize.X, 0, 1)
                fill.Size = UDim2.new(pos, 0, 1, 0)
                local val = min + pos * (max - min)
                val = math.floor(val * 100) / 100
                lbl.Text = text .. ": " .. tostring(val)
                callback(val)
            end

            bar.InputBegan:Connect(function(input)
                if input.UserInputType == Enum.UserInputType.MouseButton1 then
                    dragging = true; update(input)
                end
            end)
            UserInputService.InputEnded:Connect(function(input)
                if input.UserInputType == Enum.UserInputType.MouseButton1 then dragging = false end
            end)
            UserInputService.InputChanged:Connect(function(input)
                if dragging and input.UserInputType == Enum.UserInputType.MouseMovement then update(input) end
            end)
            callback(default)
        end

        function tabObj:AddDropdown(text, options, multi, callback)
            local container = Instance.new("Frame", page)
            container.Size = UDim2.new(1, 0, 0, 35)
            container.BackgroundColor3 = colorPanelBg
            container.BackgroundTransparency = 0.35
            container.ClipsDescendants = true
            container.ZIndex = 4
            Instance.new("UICorner", container).CornerRadius = UDim.new(0, 8)

            local btn = Instance.new("TextButton", container)
            btn.Size = UDim2.new(1, 0, 0, 35)
            btn.BackgroundTransparency = 1
            btn.Text = "  " .. text .. " (Click to open)"
            btn.TextColor3 = colorText
            btn.Font = Enum.Font.GothamBold
            btn.TextSize = 13
            btn.TextXAlignment = Enum.TextXAlignment.Left
            btn.ZIndex = 5

            local list = Instance.new("UIListLayout", container)
            list.SortOrder = Enum.SortOrder.LayoutOrder

            local open = false
            local selected = multi and {} or nil
            local optionBtns = {}

            btn.MouseButton1Click:Connect(function()
                open = not open
                local newHeight = open and (35 + (#options * 30)) or 35
                TweenService:Create(container, TweenInfo.new(0.3, Enum.EasingStyle.Quart, Enum.EasingDirection.Out), {Size = UDim2.new(1, 0, 0, newHeight)}):Play()
            end)

            for _, opt in ipairs(options) do
                local optBtn = Instance.new("TextButton", container)
                optBtn.Size = UDim2.new(1, 0, 0, 30)
                optBtn.BackgroundColor3 = colorPanelBg
                optBtn.BackgroundTransparency = 1
                optBtn.Text = "  " .. opt
                optBtn.TextColor3 = colorText
                optBtn.Font = Enum.Font.Gotham
                optBtn.TextSize = 13
                optBtn.TextXAlignment = Enum.TextXAlignment.Left
                optBtn.BorderSizePixel = 0
                optBtn.ZIndex = 5

                optBtn.MouseButton1Click:Connect(function()
                    if multi then
                        if selected[opt] then
                            selected[opt] = nil
                            optBtn.TextColor3 = colorText
                            optBtn.Font = Enum.Font.Gotham
                        else
                            selected[opt] = true
                            optBtn.TextColor3 = colorActive
                            optBtn.Font = Enum.Font.GothamBold
                        end
                        callback(selected)
                    else
                        selected = opt
                        for _, b in ipairs(optionBtns) do 
                            b.TextColor3 = colorText 
                            b.Font = Enum.Font.Gotham
                        end
                        optBtn.TextColor3 = colorActive
                        optBtn.Font = Enum.Font.GothamBold
                        btn.Text = "  " .. text .. ": " .. options[1]
                        open = false
                        TweenService:Create(container, TweenInfo.new(0.3, Enum.EasingStyle.Quart, Enum.EasingDirection.Out), {Size = UDim2.new(1, 0, 0, 35)}):Play()
                        callback(selected)
                    end
                end)
                table.insert(optionBtns, optBtn)
            end
            
            if not multi and #options > 0 then
                selected = options[1]
                optionBtns[1].TextColor3 = colorActive
                optionBtns[1].Font = Enum.Font.GothamBold
                btn.Text = "  " .. text .. ": " .. options[1]
                callback(selected)
            elseif multi then
                callback(selected)
            end
        end

        return tabObj
    end

    return ui, sg
end

local seedOptions = {}
local seedPriceByName = {}
if SeedShopData and SeedShopData.ShopData then
    local seedEntries = {}
    for _, shopEntry in pairs(SeedShopData.ShopData) do
        if type(shopEntry) == "table" and type(shopEntry.Name) == "string" then
            if shopEntry.DisplayInShop ~= false then
                table.insert(seedEntries, { Name = shopEntry.Name, Price = tonumber(shopEntry.Price) or math.huge })
                seedPriceByName[shopEntry.Name] = tonumber(shopEntry.Price) or nil
            end
        end
    end
    table.sort(seedEntries, function(a, b)
        if a.Price == b.Price then return a.Name < b.Name end
        return a.Price < b.Price
    end)
    for _, entry in ipairs(seedEntries) do table.insert(seedOptions, entry.Name) end
end
if #seedOptions == 0 then seedOptions = {"Carrot"} end

Settings.SelectedSeed = seedOptions[1]
local selectedAutoPlantSeedsMap = {}
local autoPlantEquipCycleIndex = 1
local selectedBuySeedsMap = {}
local buySeedCycleIndex = 1

local function pickFirstSelectedValue(selection, valueMap)
    if type(selection) == "table" then
        local labels = {}
        for label, isSelected in pairs(selection) do
            if isSelected then table.insert(labels, label) end
        end
        table.sort(labels)
        local firstLabel = labels[1]
        if not firstLabel then return nil end
        if valueMap then return valueMap[firstLabel] or firstLabel end
        return firstLabel
    end
    if valueMap then return valueMap[selection] or selection end
    return selection
end

local gearOptions = {}
local gearPriceByName = {}
if GearShopData and GearShopData.ShopData then
    local gearEntries = {}
    for _, shopEntry in pairs(GearShopData.ShopData) do
        if type(shopEntry) == "table" and type(shopEntry.Name) == "string" then
            if shopEntry.DisplayInShop ~= false then
                table.insert(gearEntries, { Name = shopEntry.Name, Price = tonumber(shopEntry.Price) or math.huge })
                gearPriceByName[shopEntry.Name] = tonumber(shopEntry.Price) or nil
            end
        end
    end
    table.sort(gearEntries, function(a, b)
        if a.Price == b.Price then return a.Name < b.Name end
        return a.Price < b.Price
    end)
    for _, entry in ipairs(gearEntries) do table.insert(gearOptions, entry.Name) end
end
if #gearOptions == 0 then gearOptions = {"Recall Wrench"} end

Settings.SelectedGear = gearOptions[1]
local selectedBuyGearsMap = {}
local buyGearCycleIndex = 1

local UI = CreateSimpleUI()

local MainTab = UI:AddTab("Main")
local ShopTab = UI:AddTab("Shop")

MainTab:AddLabel("--- Auto Harvest ---")
MainTab:AddToggle("Enable Auto Harvest", false, function(value) Settings.Enabled = value end)
MainTab:AddToggle("Ignore Favorited", true, function(value) Settings.IgnoreFavorited = value end)
MainTab:AddToggle("Auto Teleport", false, function(value) Settings.AutoHarvestTeleport = value end)
MainTab:AddSlider("Harvest Delay (s)", 0.05, 1.0, Settings.Delay, function(value) Settings.Delay = value end)
MainTab:AddToggle("Auto Claim Quests", false, function(value) 
    Settings.AutoClaimQuests = value
    if value and RequestQuestsRemote and RequestQuestsRemote:IsA("RemoteEvent") then pcall(function() RequestQuestsRemote:FireServer() end) end
end)

MainTab:AddLabel("--- Auto Plant ---")
MainTab:AddToggle("Auto Plant at Character", false, function(value)
    if value and not Settings.SavedPlantPosition then
        Settings.AutoPlantAtCharacter = false
        Notify("Set a plant position first.", 3)
        return
    end
    Settings.AutoPlantAtCharacter = value
    warnedMissingSavedPosition = false
end)
MainTab:AddToggle("Auto Equip Seeds", false, function(value) Settings.AutoEquipPlantSeeds = value end)
MainTab:AddDropdown("Auto Plant Seeds", seedOptions, true, function(value)
    selectedAutoPlantSeedsMap = {}
    if type(value) == "table" then
        for seedName, isSelected in pairs(value) do
            if isSelected then selectedAutoPlantSeedsMap[tostring(seedName)] = true end
        end
    end
    autoPlantEquipCycleIndex = 1
end)

local SavedPositionLabel = MainTab:AddLabel("Saved Position: Not set")
MainTab:AddButton("Save Plant Position", function()
    local char = LocalPlayer.Character
    if not char then SavedPositionLabel.Text = "Saved Position: Failed (no character)"; return end
    local root = char:FindFirstChild("HumanoidRootPart") or char.PrimaryPart
    if not root then SavedPositionLabel.Text = "Saved Position: Failed (no root)"; return end

    Settings.SavedPlantPosition = root.Position
    warnedMissingSavedPosition = false
    SavedPositionLabel.Text = string.format("Saved Position: X %.2f | Y %.2f | Z %.2f", root.Position.X, root.Position.Y, root.Position.Z)
end)

ShopTab:AddLabel("--- Seed Shop ---")
ShopTab:AddDropdown("Seed", seedOptions, true, function(value)
    selectedBuySeedsMap = {}
    if type(value) == "table" then
        for key, isSelected in pairs(value) do
            if type(key) == "number" then selectedBuySeedsMap[tostring(isSelected)] = true
            elseif isSelected then selectedBuySeedsMap[tostring(key)] = true end
        end
    elseif type(value) == "string" and value ~= "" then
        selectedBuySeedsMap[value] = true
    end
    local selected = pickFirstSelectedValue(value, nil)
    if selected then Settings.SelectedSeed = tostring(selected) end
    buySeedCycleIndex = 1
end)
ShopTab:AddButton("Open Seed Shop Menu", function()
    local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
    if playerGui and playerGui:FindFirstChild("SeedShop") then playerGui.SeedShop.Enabled = true end
end)
ShopTab:AddToggle("Teleport To NPC On Buy", true, function(value) Settings.TeleportToShopOnBuy = value end)
ShopTab:AddToggle("Check Stock Before Buy", Settings.CheckSeedStockBeforeBuy, function(value) Settings.CheckSeedStockBeforeBuy = value end)
ShopTab:AddToggle("Auto Buy Loop", false, function(value) Settings.AutoBuyLoop = value end)
ShopTab:AddSlider("Seed Buy Delay (s)", 0.1, 5.0, Settings.AutoBuyDelay, function(value) Settings.AutoBuyDelay = value end)

ShopTab:AddLabel("--- Gear Shop ---")
ShopTab:AddDropdown("Gear", gearOptions, true, function(value)
    selectedBuyGearsMap = {}
    if type(value) == "table" then
        for key, isSelected in pairs(value) do
            if type(key) == "number" then selectedBuyGearsMap[tostring(isSelected)] = true
            elseif isSelected then selectedBuyGearsMap[tostring(key)] = true end
        end
    elseif type(value) == "string" and value ~= "" then
        selectedBuyGearsMap[value] = true
    end
    local selected = pickFirstSelectedValue(value, nil)
    if selected then Settings.SelectedGear = tostring(selected) end
    buyGearCycleIndex = 1
end)
ShopTab:AddButton("Open Gear Shop Menu", function()
    local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
    if playerGui and playerGui:FindFirstChild("GearShop") then playerGui.GearShop.Enabled = true end
end)
ShopTab:AddToggle("Teleport To NPC On Buy (Gear)", true, function(value) Settings.TeleportToGearShopOnBuy = value end)
ShopTab:AddToggle("Check Stock Before Buy (Gear)", Settings.CheckGearStockBeforeBuy, function(value) Settings.CheckGearStockBeforeBuy = value end)
ShopTab:AddToggle("Auto Buy Loop (Gear)", false, function(value) Settings.AutoBuyGearLoop = value end)
ShopTab:AddSlider("Gear Buy Delay (s)", 0.1, 5.0, Settings.AutoBuyGearDelay, function(value) Settings.AutoBuyGearDelay = value end)

ShopTab:AddLabel("--- Auto Sell ---")
ShopTab:AddDropdown("Sell Mode", {"Sell All", "Sell Held Item", "Sell All on Inventory Full"}, false, function(value)
    local selected = pickFirstSelectedValue(value, nil)
    if selected == "Sell All" then
        Settings.SellMode = "SellAll"; Settings.AutoSellOnlyWhenInventoryFull = false
    elseif selected == "Sell Held Item" then
        Settings.SellMode = "Sell Held Item"; Settings.AutoSellOnlyWhenInventoryFull = false
    elseif selected == "Sell All on Inventory Full" then
        Settings.SellMode = "SellAll"; Settings.AutoSellOnlyWhenInventoryFull = true
    end
end)
ShopTab:AddToggle("Teleport To NPC On Sell", true, function(value) Settings.TeleportToSellNpcOnSell = value end)
ShopTab:AddToggle("Auto Sell Loop", false, function(value) Settings.AutoSellLoop = value end)
ShopTab:AddSlider("Auto Sell Delay (s)", 0.1, 10.0, Settings.AutoSellDelay, function(value) Settings.AutoSellDelay = value end)

ShopTab:AddButton("Buy Selected Seed", function() tryBuyNextSelectedSeed(false) end)
ShopTab:AddButton("Buy Selected Gear", function() tryBuyNextSelectedGear(false) end)
ShopTab:AddButton("Sell Now", function() trySell(Settings.SellMode, false) end)

local function getCharacterRoot()
    local char = LocalPlayer.Character
    if not char then return nil end
    return char:FindFirstChild("HumanoidRootPart") or char.PrimaryPart
end

local function isNearNpc(rootPos, targetPos, horizontalDist, maxYDiff)
    local dx = rootPos.X - targetPos.X
    local dz = rootPos.Z - targetPos.Z
    local horizontal = math.sqrt(dx * dx + dz * dz)
    local yDiff = math.abs(rootPos.Y - targetPos.Y)
    return horizontal <= horizontalDist and yDiff <= maxYDiff
end

local function teleportRootAndWait(root, targetPos, timeoutSec, horizontalDist, maxYDiff, stableFramesRequired)
    local timeout = timeoutSec or 0.75
    local nearHorizontal = horizontalDist or 2.5
    local nearY = maxYDiff or 10
    local stableFrames = stableFramesRequired or 5
    local started = tick()
    local stableCount = 0

    while tick() - started < timeout do
        root.CFrame = CFrame.new(targetPos + Vector3.new(0, 3, 0))
        task.wait()

        local currentRoot = getCharacterRoot()
        if not currentRoot then return false end
        root = currentRoot

        if isNearNpc(root.Position, targetPos, nearHorizontal, nearY) then
            stableCount = stableCount + 1
            if stableCount >= stableFrames then
                for _ = 1, 3 do
                    root.CFrame = CFrame.new(targetPos + Vector3.new(0, 3, 0))
                    task.wait()
                end
                return true
            end
        else
            stableCount = 0
        end
    end

    return isNearNpc(root.Position, targetPos, nearHorizontal, nearY)
end

local function trySell(mode, silent)
    if not SellItemsRemote then
        if not silent then Notify("SellItems remote not found.", 3) end
        return false
    end

    local sellMode = mode
    if sellMode == "Sell Held Item" then sellMode = "SellSingle"
    elseif sellMode ~= "SellSingle" and sellMode ~= "SellAll" then
        sellMode = Settings.SellMode == "Sell Held Item" and "SellSingle" or "SellAll"
    end

    local originalPos = nil
    local didTeleport = false
    local root = getCharacterRoot()

    if Settings.TeleportToSellNpcOnSell then
        if not root then
            if not silent then Notify("Character root not found.", 3) end
            return false
        end
        local npcPos = Settings.SellNpcPosition
        if not npcPos then return false end

        originalPos = root.Position
        local reached = teleportRootAndWait(root, npcPos, 1.2, 2.5, 10, 5)
        if not reached then
            if not silent then Notify("Could not reach sell NPC.", 3) end
            return false
        end
        didTeleport = true
        task.wait(0.15)
    end

    local ok, result = pcall(function()
        if SellItemsRemote:IsA("RemoteFunction") then return SellItemsRemote:InvokeServer(sellMode) end
        SellItemsRemote:FireServer(sellMode)
        return true
    end)

    if didTeleport and originalPos then
        local backRoot = getCharacterRoot()
        if backRoot then backRoot.CFrame = CFrame.new(originalPos) end
    end

    if not ok then
        if not silent then Notify("Sell failed (invoke error).", 3) end
        return false
    end

    local response = tostring(result or "")
    local responseLower = string.lower(response)
    local sold = result == true or string.find(responseLower, "here's", 1, true) ~= nil or string.find(responseLower, "sold", 1, true) ~= nil

    if sold then return true end

    if not silent then
        if response ~= "" and response ~= "nil" then Notify(response, 3)
        else Notify("Nothing to sell.", 2) end
    end
    return false
end

local function isInventoryFullNotificationText(text)
    if type(text) ~= "string" then return false end
    local normalized = string.gsub(text, "^%s+", "")
    normalized = string.gsub(normalized, "%s+$", "")

    if normalized == INVENTORY_FULL_TEXT then return true end
    if string.sub(normalized, 1, #INVENTORY_FULL_TEXT) ~= INVENTORY_FULL_TEXT then return false end

    local suffix = string.sub(normalized, #INVENTORY_FULL_TEXT + 1)
    suffix = string.gsub(suffix, "^%s+", "")
    if suffix == "" then return true end
    if string.match(suffix, "^%.[%s]*%[X%d+%]$") then return true end
    if string.match(suffix, "^%[X%d+%]$") then return true end

    return false
end

local function shouldSellFromInventoryFullNotification()
    local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
    if not playerGui then return false end

    local notification = playerGui:FindFirstChild("Notification")
    if not notification then return false end

    local frame = notification:FindFirstChild("Frame")
    if not frame then return false end

    local frameChildren = frame:GetChildren()
    local slot = frameChildren[5]
    if slot then
        local content = slot:FindFirstChild("CONTENT")
        local shadow = content and content:FindFirstChild("CONTENT_SHADOW")
        if shadow and shadow:IsA("TextLabel") and isInventoryFullNotificationText(shadow.Text) then
            return true
        end
    end

    for _, node in ipairs(frame:GetDescendants()) do
        if node:IsA("TextLabel") and isInventoryFullNotificationText(node.Text) then
            return true
        end
    end
    return false
end

local function getSeedStockAmount(seedName)
    if not GetShopDataRemote then return nil, "GetShopData remote not found." end
    local ok, data = pcall(function() return GetShopDataRemote:InvokeServer("SeedShop") end)
    if not ok or type(data) ~= "table" or type(data.Items) ~= "table" then return nil, "Failed to fetch stock." end

    for itemName, itemData in pairs(data.Items) do
        if tostring(itemName):lower() == tostring(seedName):lower() then
            if type(itemData) == "table" then return tonumber(itemData.Amount) or 0, nil end
            return 0, nil
        end
    end
    return 0, nil
end

local function getPlayerShillings()
    local stats = LocalPlayer:FindFirstChild("leaderstats")
    local function readNumericStat(container, name)
        if not container then return nil end
        local valueObj = container:FindFirstChild(name)
        if valueObj and (valueObj:IsA("IntValue") or valueObj:IsA("NumberValue")) then return tonumber(valueObj.Value) end
        return nil
    end

    local amount = readNumericStat(stats, "Shillings")
    if amount ~= nil then return amount end
    amount = readNumericStat(LocalPlayer, "Shillings")
    if amount ~= nil then return amount end

    if stats then
        local numericValues = {}
        for _, child in ipairs(stats:GetChildren()) do
            if child:IsA("IntValue") or child:IsA("NumberValue") then table.insert(numericValues, child) end
        end
        if #numericValues == 1 then
            local val = tonumber(numericValues[1].Value)
            if val ~= nil then return val end
        end
    end
    return nil
end

local function canAffordPrice(price)
    local numericPrice = tonumber(price)
    if not numericPrice or numericPrice <= 0 then return true end
    local shillings = getPlayerShillings()
    if shillings == nil then return true end
    return shillings >= numericPrice
end

local function tryBuySelectedSeed(silent, forcedSeedName)
    local seedName = forcedSeedName or Settings.SelectedSeed
    if not PurchaseShopItemRemote then
        if not silent then Notify("Purchase remote not found.", 3) end
        return false
    end
    if not seedName then
        if not silent then Notify("Select a seed first.", 3) end
        return false
    end

    if Settings.CheckSeedStockBeforeBuy then
        local stockAmount, stockErr = getSeedStockAmount(seedName)
        if stockAmount == nil then
            if not silent then Notify(stockErr or "Could not check stock.", 3) end
            return false
        end
        if stockAmount <= 0 then
            if not silent then Notify(seedName .. " is out of stock.", 3) end
            return false
        end
    end

    local seedPrice = seedPriceByName[seedName]
    if not canAffordPrice(seedPrice) then
        if not silent then Notify("Not enough shillings to buy " .. seedName .. ".", 3) end
        return false
    end

    local originalPos = nil
    local didTeleport = false
    local root = getCharacterRoot()

    if Settings.TeleportToShopOnBuy then
        if not root then
            if not silent then Notify("Character root not found.", 3) end
            return false
        end
        local npcPos = Settings.SeedShopNpcPosition
        if not npcPos then return false end
        originalPos = root.Position
        local reached = teleportRootAndWait(root, npcPos, 1.2, 2.5, 10, 5)
        if not reached then
            if not silent then Notify("Could not reach seed shop NPC.", 3) end
            return false
        end
        didTeleport = true
        task.wait(0.2)
    end

    local ok, result, reason = pcall(function() return PurchaseShopItemRemote:InvokeServer("SeedShop", seedName) end)
    if not ok then
        if not silent then Notify("Purchase failed (invoke error).", 3) end
        return false
    end

    local reasonText = string.lower(tostring(reason or ""))
    local outOfStock = string.find(reasonText, "out of stock", 1, true) ~= nil or string.find(reasonText, "no stock", 1, true) ~= nil

    if result then
        if didTeleport and originalPos then
            local backRoot = getCharacterRoot()
            if backRoot then backRoot.CFrame = CFrame.new(originalPos) end
        end
        if not silent then Notify("Bought: " .. seedName, 2) end
        return true
    else
        if outOfStock and didTeleport and originalPos then
            local backRoot = getCharacterRoot()
            if backRoot then backRoot.CFrame = CFrame.new(originalPos) end
        end
        if not silent then Notify("Purchase failed: " .. tostring(reason or "Unknown"), 3) end
        return false
    end
end

local function getSelectedBuySeedList()
    local selectedSeedList = {}
    for _, seedName in ipairs(seedOptions) do
        if selectedBuySeedsMap[seedName] then table.insert(selectedSeedList, seedName) end
    end
    if #selectedSeedList == 0 and Settings.SelectedSeed then table.insert(selectedSeedList, Settings.SelectedSeed) end
    return selectedSeedList
end

local function tryBuyNextSelectedSeed(silent)
    local selectedSeedList = getSelectedBuySeedList()
    if #selectedSeedList == 0 then
        if not silent then Notify("Select a seed first.", 3) end
        return false
    end

    if buySeedCycleIndex < 1 or buySeedCycleIndex > #selectedSeedList then buySeedCycleIndex = 1 end
    local startIndex = buySeedCycleIndex
    for offset = 0, #selectedSeedList - 1 do
        local idx = ((startIndex - 1 + offset) % #selectedSeedList) + 1
        local seedName = selectedSeedList[idx]
        if tryBuySelectedSeed(silent, seedName) then
            buySeedCycleIndex = (idx % #selectedSeedList) + 1
            return true
        end
    end
    buySeedCycleIndex = (startIndex % #selectedSeedList) + 1
    return false
end

local function getGearStockAmount(gearName)
    if not GetShopDataRemote then return nil, "GetShopData remote not found." end
    local ok, data = pcall(function() return GetShopDataRemote:InvokeServer("GearShop") end)
    if not ok or type(data) ~= "table" or type(data.Items) ~= "table" then return nil, "Failed to fetch stock." end

    for itemName, itemData in pairs(data.Items) do
        if tostring(itemName):lower() == tostring(gearName):lower() then
            if type(itemData) == "table" then return tonumber(itemData.Amount) or 0, nil end
            return 0, nil
        end
    end
    return 0, nil
end

local function tryBuySelectedGear(silent, forcedGearName)
    local gearName = forcedGearName or Settings.SelectedGear
    if not PurchaseShopItemRemote then
        if not silent then Notify("Purchase remote not found.", 3) end
        return false
    end
    if not gearName then
        if not silent then Notify("Select a gear first.", 3) end
        return false
    end
    if Settings.CheckGearStockBeforeBuy then
        local stockAmount, stockErr = getGearStockAmount(gearName)
        if stockAmount == nil then
            if not silent then Notify(stockErr or "Could not check stock.", 3) end
            return false
        end
        if stockAmount <= 0 then
            if not silent then Notify(gearName .. " is out of stock.", 3) end
            return false
        end
    end

    local gearPrice = gearPriceByName[gearName]
    if not canAffordPrice(gearPrice) then
        if not silent then Notify("Not enough shillings to buy " .. gearName .. ".", 3) end
        return false
    end

    local originalPos = nil
    local didTeleport = false
    local root = getCharacterRoot()

    if Settings.TeleportToGearShopOnBuy then
        if not root then return false end
        local npcPos = Settings.GearShopNpcPosition
        if not npcPos then return false end

        originalPos = root.Position
        local reached = teleportRootAndWait(root, npcPos, 1.2, 2.5, 10, 5)
        if not reached then return false end
        didTeleport = true
        task.wait(0.2)
    end

    local ok, result, reason = pcall(function() return PurchaseShopItemRemote:InvokeServer("GearShop", gearName) end)
    if not ok then return false end

    if result then
        if didTeleport and originalPos then
            local backRoot = getCharacterRoot()
            if backRoot then backRoot.CFrame = CFrame.new(originalPos) end
        end
        if not silent then Notify("Bought: " .. gearName, 2) end
        return true
    else
        local reasonText = string.lower(tostring(reason or ""))
        local outOfStock = string.find(reasonText, "out of stock", 1, true) ~= nil or string.find(reasonText, "no stock", 1, true) ~= nil
        if outOfStock and didTeleport and originalPos then
            local backRoot = getCharacterRoot()
            if backRoot then backRoot.CFrame = CFrame.new(originalPos) end
        end
        if not silent then Notify("Purchase failed: " .. tostring(reason or "Unknown"), 3) end
        return false
    end
end

local function getSelectedBuyGearList()
    local selectedGearList = {}
    for _, gearName in ipairs(gearOptions) do
        if selectedBuyGearsMap[gearName] then table.insert(selectedGearList, gearName) end
    end
    if #selectedGearList == 0 and Settings.SelectedGear then table.insert(selectedGearList, Settings.SelectedGear) end
    return selectedGearList
end

local function tryBuyNextSelectedGear(silent)
    local selectedGearList = getSelectedBuyGearList()
    if #selectedGearList == 0 then return false end
    if buyGearCycleIndex < 1 or buyGearCycleIndex > #selectedGearList then buyGearCycleIndex = 1 end

    local startIndex = buyGearCycleIndex
    for offset = 0, #selectedGearList - 1 do
        local idx = ((startIndex - 1 + offset) % #selectedGearList) + 1
        local gearName = selectedGearList[idx]
        if tryBuySelectedGear(silent, gearName) then
            buyGearCycleIndex = (idx % #selectedGearList) + 1
            return true
        end
    end
    buyGearCycleIndex = (startIndex % #selectedGearList) + 1
    return false
end

local function getPromptWorldPosition(prompt)
    if not prompt or not prompt.Parent then return nil end
    if prompt.Parent:IsA("Attachment") then return prompt.Parent.WorldPosition end
    if prompt.Parent:IsA("BasePart") then return prompt.Parent.Position end
    return nil
end

local function getPromptModel(promptObj)
    if not promptObj or not promptObj.Parent then return nil end
    local node = promptObj.Parent
    if node:IsA("Attachment") then node = node.Parent end
    if node and node:IsA("BasePart") then node = node.Parent end
    while node and node ~= workspace and not node:IsA("Model") do node = node.Parent end
    if node and node:IsA("Model") then return node end
    return nil
end

local function getOwnerModel(model)
    local node = model
    while node and node ~= workspace do
        if node:IsA("Model") and node:GetAttribute("OwnerUserId") ~= nil then return node end
        node = node.Parent
    end
    return nil
end

local function isIgnoredSignPrompt(promptObj)
    local node = promptObj
    while node and node ~= workspace do
        if node.Name == "PlayerSign" or node.Name == "GrowAllSign" then
            local parentNode = node.Parent
            while parentNode and parentNode ~= workspace do
                if parentNode.Name == "Plots" then return true end
                parentNode = parentNode.Parent
            end
        end
        node = node.Parent
    end
    return false
end

local function refreshHarvestPromptScanCache()
    local clientPlants = workspace:FindFirstChild("ClientPlants")
    if not clientPlants then
        harvestPromptScanCache = {}
        harvestPromptScanCacheAt = tick()
        return
    end

    local entries = {}
    for _, d in ipairs(clientPlants:GetDescendants()) do
        if d:IsA("ProximityPrompt") and d.Parent and d.Enabled and (not isIgnoredSignPrompt(d)) then
            local model = getPromptModel(d)
            local ownerUserId = nil
            local isFavorited = false
            if model then
                local ownerModel = getOwnerModel(model)
                if ownerModel then
                    local ownerAttr = ownerModel:GetAttribute("OwnerUserId")
                    ownerUserId = tonumber(ownerAttr) or ownerAttr
                    isFavorited = ownerModel:GetAttribute("Favorited") == true
                end
            end
            local pos = getPromptWorldPosition(d)
            if pos then
                table.insert(entries, {
                    Prompt = d, Pos = pos, OwnerUserId = ownerUserId, IsFavorited = isFavorited,
                })
            end
        end
    end
    harvestPromptScanCache = entries
    harvestPromptScanCacheAt = tick()
end

local function getClosestHarvestPrompts(limit, maxDistOverride)
    local root = getCharacterRoot()
    if not root then return {} end

    local now = tick()
    if now - harvestPromptScanCacheAt >= HARVEST_PROMPT_SCAN_INTERVAL or #harvestPromptScanCache == 0 then
        refreshHarvestPromptScanCache()
    end

    local myPos = root.Position
    local maxDist = tonumber(maxDistOverride) or Settings.Range
    local candidates = {}

    for _, entry in ipairs(harvestPromptScanCache) do
        local prompt = entry.Prompt
        if prompt and prompt.Parent and prompt.Enabled then
            local blacklistedUntil = harvestBlacklistUntilByPrompt[prompt]
            if not (blacklistedUntil and now < blacklistedUntil) then
                local cooldownUntil = harvestCooldownByPrompt[prompt]
                if not (cooldownUntil and now < cooldownUntil) then
                    if entry.OwnerUserId == nil or entry.OwnerUserId == LocalPlayer.UserId then
                        if (not Settings.IgnoreFavorited) or (not entry.IsFavorited) then
                            local dist = (myPos - entry.Pos).Magnitude
                            if dist < maxDist then
                                table.insert(candidates, { Prompt = prompt, Pos = entry.Pos, Dist = dist })
                            end
                        end
                    end
                end
            end
        end
    end

    table.sort(candidates, function(a, b) return a.Dist < b.Dist end)

    local out = {}
    local take = math.max(1, math.floor(tonumber(limit) or 1))
    for i = 1, math.min(#candidates, take) do table.insert(out, candidates[i].Prompt) end
    return out
end

local function markHarvestFailure(prompt)
    local fails = (harvestFailCountByPrompt[prompt] or 0) + 1
    harvestFailCountByPrompt[prompt] = fails
    harvestCooldownByPrompt[prompt] = tick() + 0.2
    if fails >= 4 then
        harvestBlacklistUntilByPrompt[prompt] = tick() + 3
        harvestFailCountByPrompt[prompt] = 0
    end
end

local function triggerHarvestPrompt(prompt)
    if typeof(fireproximityprompt) == "function" then
        return pcall(function() fireproximityprompt(prompt, 0) end)
    end
    return pcall(function() prompt:InputHoldBegin(); prompt:InputHoldEnd() end)
end

local function harvestPromptBatch(prompts)
    if type(prompts) ~= "table" or #prompts == 0 then return false end
    local anySuccess = false
    for _, prompt in ipairs(prompts) do
        if prompt and prompt.Parent and prompt.Enabled then
            local ok = triggerHarvestPrompt(prompt)
            if ok then
                anySuccess = true
                harvestCooldownByPrompt[prompt] = tick() + 0.35
                harvestFailCountByPrompt[prompt] = 0
            else markHarvestFailure(prompt) end
        end
    end
    return anySuccess
end

local function getEquippedSeedTool()
    local char = LocalPlayer.Character
    if not char then return nil end

    local function normalizeSeedName(seedName)
        return string.lower((tostring(seedName or "")):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", ""))
    end

    local function resolvePlantTypeFromTool(tool)
        if not tool then return nil end
        local plantType = tool:GetAttribute("PlantType")
        if plantType and tostring(plantType) ~= "" then return tostring(plantType) end
        local name = tostring(tool.Name or "")
        local parsed = string.match(name, "^[xX]%d+%s+(.+)%s+[Ss]eed")
        if parsed and parsed ~= "" then return parsed end
        parsed = string.match(name, "^(.+)%s+[Ss]eed")
        if parsed and parsed ~= "" then return parsed end
        return nil
    end

    local function isSeedAllowed(plantType)
        if not plantType then return false end
        if next(selectedAutoPlantSeedsMap) == nil then return true end
        return selectedAutoPlantSeedsMap[tostring(plantType)] == true
    end

    local function isValidSeedTool(tool)
        if not tool or not tool:IsA("Tool") then return false end
        if tool:GetAttribute("IsCrate") or tool:GetAttribute("IsHarvested") then return false end

        local plantType = resolvePlantTypeFromTool(tool)
        if not isSeedAllowed(plantType) then
            local toolNameLower = string.lower(tostring(tool.Name or ""))
            local looksLikeSeedTool = string.find(toolNameLower, "seed", 1, true) ~= nil
            if not looksLikeSeedTool then return false end
            if next(selectedAutoPlantSeedsMap) == nil then return true end

            local matchedSelected = false
            for seedName, isSelected in pairs(selectedAutoPlantSeedsMap) do
                if isSelected and string.find(toolNameLower, normalizeSeedName(seedName), 1, true) then
                    matchedSelected = true; break
                end
            end
            if not matchedSelected then return false end
            return true
        end

        local toolNameLower = string.lower(tostring(tool.Name or ""))
        if string.find(toolNameLower, "seed", 1, true) == nil then return false end

        if ItemInventory and ItemInventory.getItemCount then
            local ok, count = pcall(ItemInventory.getItemCount, tool)
            if ok and count ~= nil then
                local numericCount = tonumber(count)
                if numericCount ~= nil then return numericCount > 0 end
                return true
            end
        end
        return true
    end

    for _, tool in ipairs(char:GetChildren()) do
        if isValidSeedTool(tool) then return tool end
    end

    if not Settings.AutoEquipPlantSeeds then return nil end

    local backpack = LocalPlayer:FindFirstChild("Backpack") or LocalPlayer:WaitForChild("Backpack", 2)
    if not backpack then return nil end

    local candidateTools = {}
    local selectedSeedList = {}
    if next(selectedAutoPlantSeedsMap) == nil then
        for _, seedName in ipairs(seedOptions) do table.insert(selectedSeedList, seedName) end
    else
        for _, seedName in ipairs(seedOptions) do
            if selectedAutoPlantSeedsMap[seedName] then table.insert(selectedSeedList, seedName) end
        end
    end

    for _, tool in ipairs(backpack:GetChildren()) do
        if isValidSeedTool(tool) then table.insert(candidateTools, tool) end
    end
    if #candidateTools == 0 then return nil end

    local orderedSeedNames = {}
    for _, seedName in ipairs(selectedSeedList) do table.insert(orderedSeedNames, seedName) end
    if #orderedSeedNames == 0 then
        for _, tool in ipairs(candidateTools) do
            local parsed = resolvePlantTypeFromTool(tool)
            if parsed and not table.find(orderedSeedNames, parsed) then table.insert(orderedSeedNames, parsed) end
        end
    end
    if #orderedSeedNames == 0 then
        for _, tool in ipairs(candidateTools) do table.insert(orderedSeedNames, tostring(tool.Name)) end
    end

    if autoPlantEquipCycleIndex > #orderedSeedNames then autoPlantEquipCycleIndex = 1 end
    local preferredSeedName = orderedSeedNames[autoPlantEquipCycleIndex]
    autoPlantEquipCycleIndex = autoPlantEquipCycleIndex + 1
    if autoPlantEquipCycleIndex > #orderedSeedNames then autoPlantEquipCycleIndex = 1 end

    local toolToEquip = nil
    local preferredLower = normalizeSeedName(preferredSeedName)
    for _, tool in ipairs(candidateTools) do
        local toolLower = string.lower(tostring(tool.Name or ""))
        if string.find(toolLower, preferredLower, 1, true) and string.find(toolLower, "seed", 1, true) then
            toolToEquip = tool; break
        end
    end
    if not toolToEquip then
        for _, tool in ipairs(candidateTools) do
            local parsed = resolvePlantTypeFromTool(tool)
            if parsed and normalizeSeedName(parsed) == preferredLower then
                toolToEquip = tool; break
            end
        end
    end
    if not toolToEquip then toolToEquip = candidateTools[1] end
    if not toolToEquip then return nil end

    local humanoid = char:FindFirstChildOfClass("Humanoid")
    if not humanoid then return nil end

    for _ = 1, 4 do
        pcall(function() humanoid:UnequipTools(); humanoid:EquipTool(toolToEquip) end)
        task.wait(0.08)
        if toolToEquip.Parent == char or toolToEquip:IsDescendantOf(char) then return toolToEquip end

        pcall(function() toolToEquip.Parent = char end)
        task.wait(0.05)
        if toolToEquip.Parent == char or toolToEquip:IsDescendantOf(char) then return toolToEquip end
    end
    return nil
end

local function plantAtCharacterPosition()
    if not Settings.AutoPlantAtCharacter then return end
    local now = tick()
    if now - lastPlantTime < Settings.Delay then return end

    local tool = getEquippedSeedTool()
    if not tool then return end

    local plantType = tool:GetAttribute("PlantType")
    if not plantType then
        local name = tostring(tool.Name or "")
        plantType = string.match(name, "^[xX]%d+%s+(.+)%s+[Ss]eed") or string.match(name, "^(.+)%s+[Ss]eed")
    end
    if not plantType then return end

    local plantPos = Settings.SavedPlantPosition
    if not plantPos then
        Settings.AutoPlantAtCharacter = false
        if not warnedMissingSavedPosition then
            warnedMissingSavedPosition = true
            Notify("Set a plant position first.", 3)
        end
        return
    end

    lastPlantTime = now
    pcall(function()
        if PlantRemote:IsA("RemoteFunction") then PlantRemote:InvokeServer(plantType, plantPos)
        else PlantRemote:FireServer(plantType, plantPos) end
    end)
end

local function isQuestEntryClaimable(entry)
    if type(entry) ~= "table" then return false end
    if entry.Claimed == true or entry.IsClaimed == true then return false end
    if entry.Completed == true or entry.IsCompleted == true or entry.Done == true then return true end

    local status = tostring(entry.Status or ""):lower()
    if status == "completed" or status == "complete" then return true end

    local progress = tonumber(entry.Progress or entry.Current or entry.Value or entry.Amount or 0) or 0
    local goal = tonumber(entry.Goal or entry.Target or entry.Required or entry.Max or 0) or 0
    return goal > 0 and progress >= goal
end

local function autoClaimQuests()
    if not (ClaimQuestRemote and ClaimQuestRemote:IsA("RemoteEvent")) then return end
    if RequestQuestsRemote and RequestQuestsRemote:IsA("RemoteEvent") then pcall(function() RequestQuestsRemote:FireServer() end) end
    if type(latestQuestData) ~= "table" then return end

    for _, questType in ipairs({ "Daily", "Weekly" }) do
        local bucket = latestQuestData[questType]
        local active = bucket and bucket.Active
        if type(active) == "table" then
            for i = 1, 5 do
                local questIndex = tostring(i)
                if isQuestEntryClaimable(active[questIndex]) then
                    pcall(function() ClaimQuestRemote:FireServer(questType, questIndex) end)
                end
            end
        end
    end
end

task.spawn(function()
    while task.wait(Settings.Delay) do
        local ok, err = pcall(function()
            local now = tick()

            if Settings.Enabled then
                local batchSize = math.max(1, math.floor(tonumber(Settings.HarvestBatchSize) or 5))
                local closestPrompts = getClosestHarvestPrompts(batchSize)
                if Settings.AutoHarvestTeleport and now - lastHarvestTeleportTime >= 0.5 then
                    local teleportPrompts = getClosestHarvestPrompts(1, math.huge)
                    local teleportPrompt = teleportPrompts[1]
                    if teleportPrompt then
                        local root = getCharacterRoot()
                        local promptPos = getPromptWorldPosition(teleportPrompt)
                        if root and promptPos then
                            root.CFrame = CFrame.new(promptPos + Vector3.new(0, 3, 0))
                            lastHarvestTeleportTime = now
                        end
                    end
                    closestPrompts = getClosestHarvestPrompts(batchSize)
                end

                local firstPrompt = closestPrompts[1]
                if firstPrompt then harvestPromptBatch(closestPrompts) end
            end

            if Settings.AutoPlantAtCharacter then plantAtCharacterPosition() end

            if Settings.AutoBuyLoop then
                if now - lastAutoBuyTime >= Settings.AutoBuyDelay then
                    lastAutoBuyTime = now
                    tryBuyNextSelectedSeed(true)
                end
            end

            if Settings.AutoBuyGearLoop then
                if now - lastAutoBuyGearTime >= Settings.AutoBuyGearDelay then
                    lastAutoBuyGearTime = now
                    tryBuyNextSelectedGear(true)
                end
            end

            if Settings.AutoSellLoop then
                if not SellItemsRemote then
                    if not warnedMissingSellRemotes then warnedMissingSellRemotes = true; Notify("SellItems remote not found.", 3) end
                elseif now - lastAutoSellTime >= Settings.AutoSellDelay then
                    lastAutoSellTime = now
                    if Settings.AutoSellOnlyWhenInventoryFull then
                        if now - lastInventoryFullSellTime >= Settings.InventoryFullSellCooldown and shouldSellFromInventoryFullNotification() then
                            lastInventoryFullSellTime = now
                            trySell(Settings.SellMode, true)
                        end
                    else
                        trySell(Settings.SellMode, true)
                    end
                end
            end

            if Settings.AutoClaimQuests then
                if not (ClaimQuestRemote and RequestQuestsRemote and UpdateQuestsRemote) then
                    if not warnedMissingQuestRemotes then warnedMissingQuestRemotes = true; Notify("Quest remotes not found.", 3) end
                else
                    if now - lastAutoClaimQuestTime >= Settings.AutoClaimQuestDelay then
                        lastAutoClaimQuestTime = now
                        autoClaimQuests()
                    end
                end
            end
        end)
        if not ok then warn("[Garden] Main loop recovered from error:", err) end
    end
end)

Features & Information

JOIN TELEGRAM https://t.me/pradaxcaa

Community Comments (2)

SelisSelisMar 15, 2026

is this up to date with the strange event?

nthanhtainthanhtaiMar 18, 2026

No weather?

More Scripts for [MYTHICAL] Garden Horizons 🌱