xcel
Key Required

xcel

[🚂 UPD] Storage Hunters: Open Worldmatejkkt150matejkkt150 1.4K views 1.8K executesUpdated Jun 27, 2026
Unrated
00
Lua Script•1006 lines (26843 chars)
--[[
	Auto Auction / Auto Sell / Pathfinder - Storage Hunters: Open World
	Uses WindUI for the interface.
	- Auction tab: monitors the bidding minigame and auto-bids when cursor is in the zone.
	- Sell tab: monitors Quick Sell rate + vehicle load weight and auto-sells.
	- Pathfinder tab: state machine automates the full auction loop (walk → trigger → bid → collect).
]]

-- Load WindUI from the dist URL directly
local WindUI = loadstring(game:HttpGet("https://raw.githubusercontent.com/Footagesus/WindUI/main/dist/main.lua"))()

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PathfindingService = game:GetService("PathfindingService")
local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer:WaitForChild("PlayerGui")

-- Remote references
local AuctionEvents = ReplicatedStorage:WaitForChild("Events"):WaitForChild("Auction")
local BidRemote = AuctionEvents:WaitForChild("Bid")
local LeaveAuctionRemote = AuctionEvents:WaitForChild("LeaveAuction")

local Pawn = ReplicatedStorage:WaitForChild("Events"):WaitForChild("Pawn")
local GetPawnState = Pawn:WaitForChild("GetPawnState")
local GetSellableItems = Pawn:WaitForChild("GetSellableItems")
local SellItems = Pawn:WaitForChild("SellItems")
local RateChanged = Pawn:WaitForChild("RateChanged")

local UIEvents = ReplicatedStorage:WaitForChild("Events"):WaitForChild("UI")
local VehicleWeightUpdate = UIEvents:WaitForChild("VehicleWeightUpdate")

-- Items module — used to look up category for trophy/accessory filtering
-- Loaded via decompile to avoid "Cannot require a RobloxScript module from a non RobloxScript context" errors
local ItemsModule = (function()
	local ok, result = pcall(function()
		return loadstring(decompile(ReplicatedStorage.Modules.Items))()
	end)
	if ok then return result end
	return {}
end)()

-- ── Area-to-garage mapping for pathfinder toggles ──
local AREA_GARAGES = {
	["Junk Yard"] = { "Scrap Garage" },
	["Back Alley"] = { "Shop Front" },
	["Farmyard"] = { "Stable Garage", "Barn Garage" },
	["Shipyard"] = { "Small Container Garage", "Large Container Garage", "Warehouse Garage" },
}

-- ── State via getgenv() so re-running cleans up old state ──
getgenv().AutoAuction = getgenv().AutoAuction or {}
local State = getgenv().AutoAuction

-- Auction state
State.BidEnabled = false

-- Sell state
State.SellEnabled = false
State.MinSellRate = -15   -- minimum rate percentage (slider: -50..+50)
State.MinWeight = 0        -- minimum weight in kg (slider: 0..100)
State.CurrentWeight = 0    -- current vehicle load weight
State.CurrentRate = 1.0    -- current pawn rate multiplier
State.SellCooldown = 0     -- cooldown timer to avoid spam
State.SellSyncing = false  -- guard against overlapping sell cycles
State.SaveTrophies = true  -- exclude Trophy-category items from auto-sell
State.SaveAccessories = true -- exclude Accessories-category items from auto-sell

-- Pathfinder state
State.PathfinderEnabled = false
State.AreaToggles = State.AreaToggles or {}
for area in pairs(AREA_GARAGES) do
	if State.AreaToggles[area] == nil then
		State.AreaToggles[area] = true
	end
end
State.PathfinderStatus = "Idle"
State.PathfinderPhase = "Idle"
State._pathfinderRunning = false

-- ── Helper: format rate as percentage string ──
local function formatRate(rate)
	local pct = math.floor((rate - 1) * 100 + 0.5)
	if pct >= 0 then
		return "+" .. pct .. "%"
	end
	return pct .. "%"
end

-- ── Helper: get rate as integer percent ──
local function ratePercent(rate)
	return math.floor((rate - 1) * 100 + 0.5)
end

-- ── Helper: get character root position ──
local function getRoot()
	local char = LocalPlayer.Character
	if not char then return nil end
	local root = char:FindFirstChild("HumanoidRootPart")
	if not root then return nil end
	return root
end

-- ── Helper: get humanoid ──
local function getHumanoid()
	local char = LocalPlayer.Character
	if not char then return nil end
	return char:FindFirstChildOfClass("Humanoid")
end

-- ── Helper: find nearest EnterAuction prompt matching enabled areas ──
local function findNearestAuction()
	local root = getRoot()
	if not root then return nil end

	local rootPos = root.Position
	local bestDist = math.huge
	local bestPrompt = nil

	for _, prompt in ipairs(workspace:GetDescendants()) do
		if not (prompt:IsA("ProximityPrompt") and prompt.Name == "EnterAuction") then
			continue
		end

		local garageType = prompt.ObjectText
		-- Check if this garage type belongs to an enabled area
		local areaMatch = false
		local areaName = nil
		for area, garages in pairs(AREA_GARAGES) do
			if State.AreaToggles[area] then
				for _, g in ipairs(garages) do
					if g == garageType then
						areaMatch = true
						areaName = area
						break
					end
				end
			end
			if areaMatch then break end
		end
		if not areaMatch then continue end

		local promptPart = prompt.Parent
		if not promptPart then continue end

		local dist = (promptPart.Position - rootPos).Magnitude
		if dist < bestDist then
			bestDist = dist
			bestPrompt = {
				prompt = prompt,
				promptPart = promptPart,
				position = promptPart.Position,
				garageType = garageType,
				areaName = areaName,
				distance = dist,
			}
		end
	end

	return bestPrompt
end

-- ── Helper: pathfind and walk to a target position ──
local function walkTo(targetPos, timeoutSeconds)
	local hum = getHumanoid()
	local root = getRoot()
	if not hum or not root then
		task.wait(1)
		return false
	end

	local startTime = tick()

	while tick() - startTime < (timeoutSeconds or 30) do
		root = getRoot()
		hum = getHumanoid()
		if not root or not hum then
			return false
		end

		local dist = (root.Position - targetPos).Magnitude
		if dist < 5 then
			hum:MoveTo(targetPos)
			return true
		end

		-- Compute a path
		local pathParams = {
			AgentRadius = 2,
			AgentHeight = 5,
			AgentCanJump = true,
			AgentMaxSlope = 45,
			WaypointSpacing = 4,
		}
		local ok, path = pcall(function()
			return PathfindingService:CreatePath(pathParams):ComputeAsync(root.Position, targetPos)
		end)

		if ok and path and path.Status == Enum.PathStatus.Success then
			local waypoints = path:GetWaypoints()
			hum:MoveTo(waypoints[#waypoints].Position)

			-- Walk along the path
			for i, wp in ipairs(waypoints) do
				if tick() - startTime >= (timeoutSeconds or 30) then
					hum:MoveTo(root.Position)
					return false
				end

				if wp.Action == Enum.PathWaypointAction.Jump then
					hum.Jump = true
				end

				-- Move to next waypoint
				hum:MoveTo(wp.Position)
				repeat
					task.wait(0.1)
					root = getRoot()
					if not root then
						hum:MoveTo(root and root.Position or Vector3.new())
						return false
					end
				until (root.Position - wp.Position).Magnitude < 5 or not hum
			end
		else
			-- Direct approach as fallback
			hum:MoveTo(targetPos)
			task.wait(1)
		end

		-- Re-check distance
		root = getRoot()
		if root and (root.Position - targetPos).Magnitude < 5 then
			hum:MoveTo(targetPos)
			return true
		end
	end

	return false
end

-- ── Helper: find prompts by name near a position (e.g. PickupPrompt or OpenBoxPrompt) ──
local function findPromptsNear(centerPos, radius, promptName)
	local results = {}
	for _, desc in ipairs(workspace:GetDescendants()) do
		if desc:IsA("ProximityPrompt") and desc.Name == promptName then
			local parentPart = desc.Parent
			if parentPart and parentPart:IsA("BasePart") then
				local dist = (parentPart.Position - centerPos).Magnitude
				if dist <= radius then
					table.insert(results, {
						prompt = desc,
						part = parentPart,
						position = parentPart.Position,
						distance = dist,
					})
				end
			end
		end
	end
	table.sort(results, function(a, b) return a.distance < b.distance end)
	return results
end

-- ── Helper: trigger a ProximityPrompt instantly using the executor-native function ──
local function triggerPrompt(prompt)
	local ok = pcall(function()
		fireproximityprompt(prompt)
	end)
	task.wait(0.1)
	return ok
end

-- ── Disconnect old connections if re-running ──
local function disconnectAll()
	if State.BidHeartbeat then
		State.BidHeartbeat:Disconnect()
		State.BidHeartbeat = nil
	end
	if State.SellHeartbeat then
		State.SellHeartbeat:Disconnect()
		State.SellHeartbeat = nil
	end
	if State.WeightConnection then
		State.WeightConnection:Disconnect()
		State.WeightConnection = nil
	end
	if State.RateConnection then
		State.RateConnection:Disconnect()
		State.RateConnection = nil
	end
	if State.PickupConnection then
		State.PickupConnection:Disconnect()
		State.PickupConnection = nil
	end
end
disconnectAll()

-- ── Vehicle weight tracking ──
State.CurrentWeight = 0
State.WeightConnection = VehicleWeightUpdate.OnClientEvent:Connect(function(currentKg, maxKg)
	State.CurrentWeight = tonumber(currentKg) or 0
end)

-- ── Pawn rate tracking ──
State.RateConnection = RateChanged.OnClientEvent:Connect(function(data)
	if type(data) == "table" and data.rate then
		State.CurrentRate = data.rate
	end
end)

-- ── Auction Bid Heartbeat ──
State.BidHeartbeat = RunService.Heartbeat:Connect(function()
	if not State.BidEnabled then
		return
	end

	local gui = PlayerGui:FindFirstChild("UIControllerGui")
	if not gui then
		return
	end

	local container = gui:FindFirstChild("AuctionBiddingContainer")
	if not container or not container.Visible then
		return
	end

	local now = tick()
	if State.LastBidTime and now - State.LastBidTime < 0.35 then
		return
	end

	local barRow = container:FindFirstChild("BidBarRow")
	if not barRow or not barRow.Visible then
		return
	end

	local track = barRow:FindFirstChild("Track")
	if not track then
		return
	end

	local cursor = track:FindFirstChild("Cursor")
	local bidZone = track:FindFirstChild("BidZone")
	if not cursor or not bidZone then
		return
	end

	local cPos = cursor.Position.X.Scale
	local zPos = bidZone.Position.X.Scale
	local zWidth = bidZone.Size.X.Scale
	local tolerance = 0.0075

	if zPos <= cPos + tolerance and cPos - tolerance <= zPos + zWidth then
		local ok = pcall(function()
			BidRemote:FireServer()
		end)
		if not ok then
			warn("[AutoAuction] Bid failed")
		end
		State.LastBidTime = tick()
	end
end)

-- ── Sell Heartbeat ──
State.SellHeartbeat = RunService.Heartbeat:Connect(function()
	if not State.SellEnabled then
		return
	end
	if State.SellSyncing then
		return
	end
	if tick() < State.SellCooldown then
		return
	end

	local pct = ratePercent(State.CurrentRate)
	if pct < State.MinSellRate then
		return
	end

	local weight = State.CurrentWeight
	if weight < State.MinWeight then
		return
	end

	-- All conditions met — attempt to sell
	State.SellSyncing = true

	task.spawn(function()
		local ok, items = pcall(function()
			return GetSellableItems:InvokeServer()
		end)

		if not ok or type(items) ~= "table" then
			State.SellSyncing = false
			return
		end

		-- Collect GUIDs of non-favorited items, respecting filters
		local toSell = {}
		for guid, info in pairs(items) do
			if not info.Favorited then
				local itemDef = ItemsModule[info.ItemId]
				local skip = false
				if itemDef then
					if State.SaveTrophies and itemDef.Category == "Trophy" then
						skip = true
					end
					if State.SaveAccessories and itemDef.Category == "Accessories" then
						skip = true
					end
				end
				if not skip then
					table.insert(toSell, guid)
				end
			end
		end

		if #toSell == 0 then
			State.SellSyncing = false
			return
		end

		local ok2, result = pcall(function()
			return SellItems:InvokeServer(toSell)
		end)

		if ok2 then
			-- Reset cooldown: 15 seconds before checking again
			State.SellCooldown = tick() + 15
		end

		State.SellSyncing = false
	end)
end)

-- ── Auction pickup event (fires when items spawn after winning) ──
State._itemsAvailable = false
State.PickupConnection = AuctionEvents:WaitForChild("AuctionPickupStart").OnClientEvent:Connect(function(bidAmount, totalValue)
	State._itemsAvailable = true
end)
AuctionEvents:WaitForChild("AuctionPickupEnd").OnClientEvent:Connect(function()
	State._itemsAvailable = false
end)

-- ── Pathfinder state machine runner ──
local function pathfinderLoop()
	while State.PathfinderEnabled and State._pathfinderRunning do
		-- STATE 1: FIND AUCTION
		State.PathfinderPhase = "Finding Auction"
		local target = findNearestAuction()
		if not target then
			State.PathfinderStatus = "No eligible auctions found, waiting..."
			task.wait(5)
			continue
		end

		State.PathfinderStatus = "Walking to " .. target.garageType .. " (" .. target.areaName .. ")"

		-- STATE 2: WALK TO AUCTION
		State.PathfinderPhase = "Walking to Auction"
		local walked = walkTo(target.position, 45)
		if not walked then
			State.PathfinderStatus = "Failed to reach auction, retrying..."
			task.wait(3)
			continue
		end

		-- STATE 3: TRIGGER AUCTION
		State.PathfinderPhase = "Triggering Auction"
		State.PathfinderStatus = "Starting auction..."
		-- Walk closer (within 6 studs for the 7-stud prompt range)
		walkTo(target.position, 5)

		-- Trigger the EnterAuction prompt instantly via the native executor function
		triggerPrompt(target.prompt)
		task.wait(1)

		-- Check if bidding window opened
		local gui = PlayerGui:FindFirstChild("UIControllerGui")
		local container = gui and gui:FindFirstChild("AuctionBiddingContainer")
		if not (container and container.Visible) then
			-- Try once more
			triggerPrompt(target.prompt)
			task.wait(1)
		end

		-- STATE 4: WAIT FOR BIDDING TO FINISH
		State.PathfinderPhase = "Waiting for Bidding"
		State.PathfinderStatus = "Auction in progress, waiting for result..."

		-- Wait for the auction to resolve (bidding UI closes AND we either win or lose)
		State._itemsAvailable = false
		local biddingEndTime = tick()
		local inAuction = true

		while inAuction and State.PathfinderEnabled do
			task.wait(0.5)

			gui = PlayerGui:FindFirstChild("UIControllerGui")
			container = gui and gui:FindFirstChild("AuctionBiddingContainer")

			-- If the bidding UI is gone, the auction resolved
			if not (container and container.Visible) then
				-- Double-check: if we won, items should be spawning
				if State._itemsAvailable then
					inAuction = false
				else
					-- Wait a bit more to see if items appear (winning) or it was just a loss
					if tick() - biddingEndTime > 8 then
						inAuction = false
					end
				end
			else
				biddingEndTime = tick()
			end

			-- Safety: max wait of 3 minutes for an auction
			if tick() - biddingEndTime > 180 then
				inAuction = false
			end
		end

		-- STATE 5: COLLECT ITEMS (if we won) — with verification loop
		if State._itemsAvailable then
			State.PathfinderPhase = "Collecting Items"
			State.PathfinderStatus = "Auction won! Collecting items..."
			task.wait(1)

			-- Helper: find the garage model in workspace matching the won auction
			local function findGarageModel()
				for _, model in ipairs(workspace:GetChildren()) do
					local name = model.Name
					if name == target.garageType or name:find(target.garageType) then
						return model
					end
				end
				return nil
			end

			local garageModel = findGarageModel()
			local garagePos = garageModel and garageModel:GetPivot().Position or target.position

			-- Blacklist for glitched/unreachable objects
			local blacklist = {}
			local function isBlacklisted(key)
				return blacklist[key] and blacklist[key].attempts >= 3
			end
			local function recordAttempt(key)
				blacklist[key] = blacklist[key] or { attempts = 0 }
				blacklist[key].attempts = blacklist[key].attempts + 1
			end

			-- Verification loop: repeat until garage is fully cleared or timeout
			local garageLoopStart = tick()
			local garageCleared = false

			while not garageCleared and State.PathfinderEnabled do
				-- Safety: if we've been in this garage > 20 seconds, bail out
				if tick() - garageLoopStart > 20 then
					State.PathfinderStatus = "Garage timeout (20s), moving on"
					task.wait(0.5)
					break
				end

				-- PHASE A: OPEN BOXES/CRATES
				State.PathfinderStatus = "Opening boxes in garage..."
				local boxes = findPromptsNear(garagePos, 80, "OpenBoxPrompt")
				for i, box in ipairs(boxes) do
					if not State.PathfinderEnabled then break end

					local key = tostring(box.prompt)
					if isBlacklisted(key) then continue end

					State.PathfinderStatus = "Opening box " .. i .. "/" .. #boxes
					local arrived = walkTo(box.position, 15)
					if arrived then
						triggerPrompt(box.prompt)
						task.wait(0.3)
					else
						recordAttempt(key)
					end
				end

				-- PHASE B: COLLECT ALL ITEMS
				State.PathfinderStatus = "Collecting items..."

				-- Find all PickupPrompt instances near the garage area
				local pickups = findPromptsNear(target.position, 60, "PickupPrompt")

				if #pickups == 0 then
					-- Try at the garage model position
					if garageModel then
						pickups = findPromptsNear(garagePos, 80, "PickupPrompt")
					end
				end

				if #pickups == 0 then
					-- Last resort: search the entire workspace for pickup prompts
					for _, desc in ipairs(workspace:GetDescendants()) do
						if desc:IsA("ProximityPrompt") and desc.Name == "PickupPrompt" then
							local parent = desc.Parent
							if parent and parent:IsA("BasePart") then
								table.insert(pickups, {
									prompt = desc,
									part = parent,
									position = parent.Position,
									distance = 0,
								})
							end
						end
					end
					table.sort(pickups, function(a, b) return a.distance < b.distance end)
				end

				-- Walk to each pickup and collect using fireproximityprompt
				for i, pickup in ipairs(pickups) do
					if not State.PathfinderEnabled then break end

					local key = tostring(pickup.prompt)
					if isBlacklisted(key) then continue end

					State.PathfinderStatus = "Collecting item " .. i .. "/" .. #pickups
					local arrived = walkTo(pickup.position, 15)
					if arrived then
						triggerPrompt(pickup.prompt)
						task.wait(0.3)
					else
						recordAttempt(key)
					end
				end

				-- VERIFICATION: check if anything remains
				task.wait(0.3)

				local remainingBoxes = findPromptsNear(garagePos, 80, "OpenBoxPrompt")
				local remainingItems = findPromptsNear(garagePos, 80, "PickupPrompt")

				if #remainingBoxes == 0 and #remainingItems == 0 then
					garageCleared = true
					State.PathfinderStatus = "Garage cleared!"
				else
					State.PathfinderStatus = "Verification: " .. #remainingBoxes .. " boxes, " .. #remainingItems .. " items remain"
				end

				task.wait(0.3)
			end

			-- ── EXIT GARAGE: walk back to the auction board ──
			State.PathfinderPhase = "Exiting Garage"
			State.PathfinderStatus = "Exiting garage, returning to auction board..."
			walkTo(target.position, 20)

			-- Ensure we're safely outside before transitioning back to State 1
			local exitStart = tick()
			local root = getRoot()
			while root and (root.Position - target.position).Magnitude >= 6 and State.PathfinderEnabled do
				if tick() - exitStart > 15 then
					break
				end
				root = getRoot()
				if root then
					local hum = getHumanoid()
					if hum then
						hum:MoveTo(target.position)
					end
				end
				task.wait(0.5)
			end
		else
			State.PathfinderStatus = "Auction lost or no items to collect"
			task.wait(2)
		end

		State.PathfinderStatus = "Cycle complete, searching for next auction..."
		task.wait(1)
	end

	State.PathfinderPhase = "Idle"
	if not State.PathfinderEnabled then
		State.PathfinderStatus = "Disabled"
	else
		State.PathfinderStatus = "Stopped"
	end
	State._pathfinderRunning = false
end

-- ── Start/stop pathfinder ──
local function setPathfinderEnabled(value)
	State.PathfinderEnabled = value

	if value and not State._pathfinderRunning then
		State._pathfinderRunning = true
		State.PathfinderStatus = "Starting..."
		task.spawn(pathfinderLoop)
	elseif not value then
		State.PathfinderStatus = "Disabled"
	end
end

-- ═══════════════════════════════════════════════════════════════
-- WindUI
-- ═══════════════════════════════════════════════════════════════
local Window = WindUI:CreateWindow({
	Title = "Auto Auction",
	Folder = "AutoAuction",
	Icon = "solar:cursor-square-bold-duotone",
	NewElements = true,
	HideSearchBar = true,
	Topbar = {
		Height = 38,
		ButtonsType = "Mac",
	},
})

-- ═══════════════════════════════════════════════════════════════
-- Auction Tab
-- ═══════════════════════════════════════════════════════════════
local AuctionTab = Window:Tab({
	Title = "Auction",
	Desc = "Auto bidding controls",
	Icon = "solar:cursor-square-bold-duotone",
})

AuctionTab:Toggle({
	Title = "Auto Bid",
	Desc = "Automatically bid when the cursor is in the green zone",
	Value = State.BidEnabled,
	Callback = function(value)
		State.BidEnabled = value
	end,
})

AuctionTab:Space()

AuctionTab:Paragraph({
	Title = "How it works",
	Desc = "Monitors the bidding bar during garage auctions and automatically fires a bid when the moving cursor overlaps the green target zone.",
})

AuctionTab:Space()

AuctionTab:Button({
	Title = "Leave Auction",
	Icon = "solar:logout-2-bold",
	Callback = function()
		local ok, result = pcall(function()
			return LeaveAuctionRemote:InvokeServer()
		end)
		if ok and result then
			local UIController = require(ReplicatedStorage.Modules.UIController)
			UIController:Close("AuctionBidding")
			UIController:Close("AuctionPowers")
			UIController:Close("AuctionWinningBid")
		end
	end,
})

-- ═══════════════════════════════════════════════════════════════
-- Auto Sell Tab
-- ═══════════════════════════════════════════════════════════════
local SellTab = Window:Tab({
	Title = "Auto Sell",
	Desc = "Auto quick-sell controls",
	Icon = "solar:wallet-bold-duotone",
})

-- Master Toggle
SellTab:Toggle({
	Title = "Master Toggle",
	Desc = "Enable auto-selling when conditions are met",
	Value = State.SellEnabled,
	Callback = function(value)
		State.SellEnabled = value
	end,
})

SellTab:Space()

-- Min Sell Rate slider (range -50% to +50%)
local RateSlider
RateSlider = SellTab:Slider({
	Title = "Sell Rate",
	Desc = "Minimum quick-sell rate (%) to trigger a sale",
	IsTooltip = true,
	IsTextbox = true,
	Width = 200,
	Step = 1,
	Value = {
		Min = -50,
		Max = 50,
		Default = -15,
	},
	Callback = function(value)
		State.MinSellRate = value
	end,
})
State.MinSellRate = -15

SellTab:Space()

-- Min Load Weight slider
local WeightSlider
WeightSlider = SellTab:Slider({
	Title = "Load Weight",
	Desc = "Minimum vehicle load weight (kg) to trigger a sale",
	IsTooltip = true,
	IsTextbox = true,
	Width = 200,
	Step = 1,
	Value = {
		Min = 0,
		Max = 100,
		Default = 20,
	},
	Callback = function(value)
		State.MinWeight = value
	end,
})
State.MinWeight = 20

SellTab:Space()

-- Save Trophies toggle
SellTab:Toggle({
	Title = "Save Trophies",
	Desc = "Don't auto-sell items with the Trophy category (e.g. Gavel Trophy)",
	Value = State.SaveTrophies,
	Callback = function(value)
		State.SaveTrophies = value
	end,
})

SellTab:Space()

-- Save Accessories toggle
SellTab:Toggle({
	Title = "Save Accessories",
	Desc = "Don't auto-sell items with the Accessories category",
	Value = State.SaveAccessories,
	Callback = function(value)
		State.SaveAccessories = value
	end,
})

SellTab:Space()

-- Live status readout
local StatusSection = SellTab:Section({
	Title = "Status",
})

local RateLabel
local WeightLabel
local StatusLabel

-- Build a small live-status group
local StatusGroup = SellTab:Group({})

RateLabel = StatusGroup:Paragraph({
	Title = "Current Rate",
	Desc = "Waiting for data...",
})

WeightLabel = StatusGroup:Paragraph({
	Title = "Vehicle Load",
	Desc = "Waiting for data...",
})

StatusLabel = StatusGroup:Paragraph({
	Title = "Auto Sell Status",
	Desc = "Inactive",
})

-- ── Live status updater ──
if State.StatusUpdater then
	State.StatusUpdater:Disconnect()
end
State.StatusUpdater = RunService.Heartbeat:Connect(function()
	if not RateLabel or not WeightLabel or not StatusLabel then
		return
	end

	local pct = ratePercent(State.CurrentRate)
	RateLabel:SetDesc("Rate: " .. formatRate(State.CurrentRate))

	WeightLabel:SetDesc("Weight: " .. math.floor(State.CurrentWeight) .. " kg")

	if not State.SellEnabled then
		StatusLabel:SetDesc("Disabled — toggle Master Toggle on")
		return
	end

	if pct < State.MinSellRate then
		StatusLabel:SetDesc("Waiting for rate (" .. pct .. "% < " .. State.MinSellRate .. "%)")
		return
	end

	if State.CurrentWeight < State.MinWeight then
		StatusLabel:SetDesc("Waiting for weight (" .. math.floor(State.CurrentWeight) .. "kg < " .. State.MinWeight .. "kg)")
		return
	end

	if State.SellSyncing then
		StatusLabel:SetDesc("Selling in progress...")
		return
	end

	if tick() < State.SellCooldown then
		local remaining = math.floor(State.SellCooldown - tick())
		StatusLabel:SetDesc("Cooldown (" .. remaining .. "s)")
		return
	end

	StatusLabel:SetDesc("Conditions met — ready to sell")
end)

-- Manual refresh button
SellTab:Space()
SellTab:Button({
	Title = "Refresh Now",
	Icon = "solar:refresh-bold",
	Callback = function()
		task.spawn(function()
			local ok, state = pcall(function()
				return GetPawnState:InvokeServer()
			end)
			if ok and type(state) == "table" and state.rate then
				State.CurrentRate = state.rate
			end
		end)
	end,
})

-- ═══════════════════════════════════════════════════════════════
-- Pathfinder Tab
-- ═══════════════════════════════════════════════════════════════
local PathfinderTab = Window:Tab({
	Title = "Pathfinder",
	Desc = "Auto auction loop",
	Icon = "solar:map-arrow-right-bold-duotone",
})

-- Master Pathfinder Toggle
PathfinderTab:Toggle({
	Title = "Master Pathfinder",
	Desc = "Enable the full auto-auction loop (walk → trigger → bid → collect)",
	Value = State.PathfinderEnabled,
	Callback = function(value)
		setPathfinderEnabled(value)
	end,
})

PathfinderTab:Space()

-- Section label for area toggles
PathfinderTab:Section({
	Title = "Target Areas",
	TextSize = 16,
})

-- Area toggle for each garage tier
for _, area in ipairs({ "Junk Yard", "Back Alley", "Farmyard", "Shipyard" }) do
	PathfinderTab:Toggle({
		Title = area,
		Desc = "Target " .. area .. " auctions",
		Value = State.AreaToggles[area],
		Callback = function(value)
			State.AreaToggles[area] = value
		end,
	})
	PathfinderTab:Space()
end

-- Status readout
PathfinderTab:Section({
	Title = "Status",
})

local PFGroup = PathfinderTab:Group({})

local PhaseLabel = PFGroup:Paragraph({
	Title = "Current Phase",
	Desc = "Idle",
})

local StateLabel = PFGroup:Paragraph({
	Title = "State",
	Desc = "Waiting for activation",
})

-- ── Pathfinder status updater ──
if State.PFStatusUpdater then
	State.PFStatusUpdater:Disconnect()
end
State.PFStatusUpdater = RunService.Heartbeat:Connect(function()
	if PhaseLabel then
		PhaseLabel:SetDesc(State.PathfinderPhase or "Idle")
	end
	if StateLabel then
		StateLabel:SetDesc(State.PathfinderStatus or "Waiting for activation")
	end
end)

print("[AutoAuction] Loaded successfully")

Features & Information

autobid autosell
#autofarm

Community Comments (0)

No comments yet for this script.

More Scripts for [🚂 UPD] Storage Hunters: Open World