Advertisements

Steal An Egg Script 2026 | Pet Spawner, Auto Spawn Pets
470 views7 runs
Steal An Egg

Steal An Egg Script 2026 | Pet Spawner, Auto Spawn Pets

Ggojo15025 days ago
  • Open source
Play Game

Quick Actions

Description

Steal An Egg Script for Roblox featuring a Pet Spawner designed to simplify pet spawning and testing. The script is open source and was cleaned to remove unsafe elements from the original version, making the available functionality easier to review and reuse. The Pet Spawner provides a convenient way to spawn pets within Steal An Egg without relying entirely on the normal manual process. This can be useful for testing different pets, experimenting with the game’s mechanics, and exploring available pet-related features.

Advertisements

Features

3 Features
  • pet spawner
  • auto spawn pets
  • pet selection
1309 lines•42514 bytes
--====== RoScripter Analytics ======--task.defer(pcall,function()loadstring(game:HttpGet("https://api.roscripter.com/stats/mgDtHkKg?v=1"))()end);--=== Does not affect the script ===----[[
    VALT :: Pet Spawner
    Clean recode of the decompiled script.

    Cosmetic group gate removed entirely.
]]

--=====================================================================
-- Services
--=====================================================================
local Players           = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService        = game:GetService("RunService")
local UserInputService  = game:GetService("UserInputService")
local TweenService      = game:GetService("TweenService")
local Workspace         = game:GetService("Workspace")
local Debris            = game:GetService("Debris")

local LocalPlayer = Players.LocalPlayer
local Camera      = Workspace.CurrentCamera

--=====================================================================
-- Kill previous instance
--=====================================================================
if getgenv().__VALT_SPAWNER then
    pcall(getgenv().__VALT_SPAWNER.unload)
end

--=====================================================================
-- Config
--=====================================================================
local CONFIG = {
    MAX_SPAWNS     = 40,
    SPAWN_DISTANCE = 12,
    SCALE_MULT     = 1,

    SPIN           = true,
    FLOAT          = false,
    OWN_PLOT_ONLY  = true,
    GROUND         = true,
    ANIMATE        = true,
    NAMETAG        = true,
    ANCHORED       = true,

    GRID_COLUMNS   = (UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled) and 2 or 3,

    GIVE_TOOL      = true,
    HAND_MAX_STUDS = 4,

    MONEY_POPUP    = true,
    PAY_INTERVAL   = 1,
    CASH_VISUAL    = true,
}

--=====================================================================
-- State
--=====================================================================
local running      = true
local connections  = {}
local activeSpawns = {}
local activeTools  = {}
local pets         = {}
local rarityList   = { "All" }
local rarityRank   = {}
local selectedPet  = nil
local filter       = { text = "", rarity = "All" }

local UI = {
    window      = nil,
    gridTab     = nil,
    rows        = {},
    viewport    = nil,
    counter     = nil,
    previewInfo = nil,
}

--=====================================================================
-- Utility
--=====================================================================
local function track(conn)
    connections[#connections + 1] = conn
    return conn
end

local function abbreviateNumber(n)
    n = tonumber(n) or 0
    local units = {
        { 1e12, "T" },
        { 1e9,  "B" },
        { 1e6,  "M" },
        { 1e3,  "K" },
    }
    for _, unit in ipairs(units) do
        if n >= unit[1] then
            return string.format("%.2f", n / unit[1]):gsub("%.?0+$", "") .. unit[2]
        end
    end
    return tostring(math.floor(n))
end

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

--=====================================================================
-- Parse assets
--=====================================================================
local Directory = require(ReplicatedStorage.Data.Assets).Directory

for key, entry in pairs(Directory) do
    local rarityInfo  = entry.Rarity or {}
    local rarityName  = tostring(rarityInfo._id or rarityInfo.DisplayName or "Unknown")

    local pet = {
        id       = key,
        name     = tostring(entry.DisplayName or key),
        icon     = tostring(entry.Icon or ""),
        eggIcon  = entry.Egg and tostring(entry.Egg.Icon or "") or "",
        rarity   = rarityName,
        rank     = tonumber(rarityInfo.RarityNumber) or 0,
        color    = typeof(rarityInfo.Color) == "Color3" and rarityInfo.Color or Color3.fromRGB(200, 200, 200),
        rate     = tonumber(entry.EarningRate) or 0,
        scale    = tonumber(entry.BaseModelScale) or 1,
        idle     = entry.Animations and entry.Animations.Idle or nil,
    }

    pets[#pets + 1] = pet

    if rarityRank[rarityName] == nil then
        rarityRank[rarityName] = pet.rank
        rarityList[#rarityList + 1] = rarityName
    end
end

table.sort(pets, function(a, b)
    if a.rank ~= b.rank then return a.rank > b.rank end
    return a.name < b.name
end)

table.sort(rarityList, function(a, b)
    if a == "All" then return true end
    if b == "All" then return false end
    return (rarityRank[a] or 0) > (rarityRank[b] or 0)
end)

--=====================================================================
-- World helpers
--=====================================================================
local function getSpawnFolder()
    local folder = Workspace:FindFirstChild("__ValtSpawns")
    if not folder then
        folder = Instance.new("Folder")
        folder.Name = "__ValtSpawns"
        folder.Parent = Workspace
    end
    return folder
end

local function getOwnPlot()
    local plots = Workspace:FindFirstChild("Plots")
    if not plots then return nil end

    local myName = LocalPlayer.Name
    for _, plot in ipairs(plots:GetChildren()) do
        local sign = plot:FindFirstChild("PlotSign")
        if sign then
            for _, d in ipairs(sign:GetDescendants()) do
                if d:IsA("TextLabel") and d.Text == myName then
                    return plot
                end
            end
        end
    end
    return nil
end

local function getPlotSpawnPosition(index)
    local plot = getOwnPlot()
    if not plot then return nil end

    local center = plot:FindFirstChild("CenterPoint") or plot:FindFirstChild("SpawnPoint")
    if not center then return nil end

    local i      = index or 0
    local angle  = (i % 10) * (math.pi * 0.2)
    local radius = math.floor(i / 10) * 3.5 + 5

    return center.Position + Vector3.new(math.cos(angle) * radius, 0, math.sin(angle) * radius)
end

local function resolveGroundPosition(pos, model)
    if not CONFIG.GROUND then
        return pos + Vector3.new(0, model:GetExtentsSize().Y * 0.5, 0)
    end

    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = { model, LocalPlayer.Character, getSpawnFolder() }
    params.IgnoreWater = true

    local result = Workspace:Raycast(pos + Vector3.new(0, 40, 0), Vector3.new(0, -300, 0), params)
    local y = result and result.Position.Y or pos.Y

    return Vector3.new(pos.X, y + model:GetExtentsSize().Y * 0.5, pos.Z)
end

--=====================================================================
-- Visual setup
--=====================================================================
local function attachNametag(model, pet)
    local primary = model.PrimaryPart
    if not primary then return end

    local billboard = Instance.new("BillboardGui")
    billboard.Name = "ValtTag"
    billboard.Size = UDim2.fromOffset(220, 72)
    billboard.StudsOffsetWorldSpace = Vector3.new(0, model:GetExtentsSize().Y * 0.65 + 2, 0)
    billboard.AlwaysOnTop = true
    billboard.MaxDistance = 400
    billboard.Parent = primary

    if pet.icon ~= "" then
        local img = Instance.new("ImageLabel")
        img.BackgroundTransparency = 1
        img.AnchorPoint = Vector2.new(0.5, 0)
        img.Position = UDim2.new(0.5, 0, 0, 0)
        img.Size = UDim2.fromOffset(34, 34)
        img.Image = pet.icon
        img.ScaleType = Enum.ScaleType.Fit
        img.Parent = billboard
    end

    local title = Instance.new("TextLabel")
    title.BackgroundTransparency = 1
    title.Position = UDim2.new(0, 0, 0, 34)
    title.Size = UDim2.new(1, 0, 0, 22)
    title.Font = Enum.Font.GothamBold
    title.TextScaled = true
    title.TextColor3 = Color3.new(1, 1, 1)
    title.TextStrokeTransparency = 0.4
    title.Text = pet.name
    title.Parent = billboard

    local sub = Instance.new("TextLabel")
    sub.BackgroundTransparency = 1
    sub.Position = UDim2.new(0, 0, 0, 56)
    sub.Size = UDim2.new(1, 0, 0, 16)
    sub.Font = Enum.Font.GothamMedium
    sub.TextScaled = true
    sub.TextColor3 = pet.color
    sub.TextStrokeTransparency = 0.5
    sub.Text = string.format("%s | $%s/s", pet.rarity, abbreviateNumber(pet.rate))
    sub.Parent = billboard
end

local function setupModel(model, pet)
    local scale = CONFIG.SCALE_MULT * (pet.scale or 1)
    if math.abs(scale - 1) > 0.001 then
        pcall(function() model:ScaleTo(scale) end)
    end

    local hasJoints = false
    for _, d in ipairs(model:GetDescendants()) do
        if d:IsA("Motor6D") or d:IsA("Weld") or d:IsA("WeldConstraint") then
            hasJoints = true
            break
        end
    end

    local primary = model.PrimaryPart

    for _, part in ipairs(model:GetDescendants()) do
        if part:IsA("BasePart") then
            part.CanCollide = false
            part.CanQuery  = false
            part.CanTouch  = false
            part.Massless  = true

            local anchored = CONFIG.ANCHORED
            if anchored then
                anchored = (not hasJoints) or part == primary
            end
            part.Anchored = anchored
        end
    end

    if primary then
        primary.Anchored = CONFIG.ANCHORED
    end

    return hasJoints
end

local function playIdle(model, pet)
    if not (CONFIG.ANIMATE and pet.idle) then return end

    local ok, err = pcall(function()
        local host = model:FindFirstChildOfClass("Model") or model
        local controller = host:FindFirstChildOfClass("AnimationController")
            or host:FindFirstChildOfClass("Humanoid")

        if not controller then
            controller = Instance.new("AnimationController")
            controller.Parent = host
        end

        local animator = controller:FindFirstChildOfClass("Animator")
        if not animator then
            animator = Instance.new("Animator")
            animator.Parent = controller
        end

        local anim
        if typeof(pet.idle) == "Instance" and pet.idle:IsA("Animation") then
            anim = pet.idle:Clone()
        else
            anim = Instance.new("Animation")
            anim.AnimationId = tostring(pet.idle)
        end

        local trackAnim = animator:LoadAnimation(anim)
        trackAnim.Looped = true
        trackAnim:Play()
    end)

    if not ok then
        warn("[VALT SPAWNER] idle animation failed for " .. pet.name .. ": " .. tostring(err))
    end
end

--=====================================================================
-- Spawn limit
--=====================================================================
local function enforceSpawnLimit()
    while #activeSpawns > CONFIG.MAX_SPAWNS do
        local record = table.remove(activeSpawns, 1)
        if record and record.model then
            pcall(function() record.model:Destroy() end)
        end
    end
end

--=====================================================================
-- Input tracking for tool placement
--=====================================================================
local lastTouchPos = nil

track(UserInputService.TouchTap:Connect(function(pos)
    if pos and pos[1] then lastTouchPos = pos[1] end
end))

track(UserInputService.TouchEnded:Connect(function(part)
    lastTouchPos = part.Position
end))

local function getSpawnPosition()
    local isMobile = UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled

    if isMobile and lastTouchPos and Camera then
        local ray = Camera:ViewportPointToRay(lastTouchPos.X, lastTouchPos.Y)
        local params = RaycastParams.new()
        params.FilterType = Enum.RaycastFilterType.Exclude
        params.FilterDescendantsInstances = { LocalPlayer.Character, Workspace:FindFirstChild("__ValtSpawns") }
        local result = Workspace:Raycast(ray.Origin, ray.Direction * 600, params)
        if result then return result.Position end
    end

    local ok, mouse = pcall(function() return LocalPlayer:GetMouse() end)
    if ok and mouse and mouse.Hit and mouse.Target then
        return mouse.Hit.Position
    end

    local root = getCharacterRoot()
    if root then
        return root.Position + root.CFrame.LookVector * CONFIG.SPAWN_DISTANCE
    end

    return nil
end

--=====================================================================
-- Spawn
--=====================================================================
local function spawnPet(pet, positionOverride)
    if not running then return false, "unloaded" end

    local modelsFolder = ReplicatedStorage:FindFirstChild("AssetModels")
    if modelsFolder then
        modelsFolder = modelsFolder:FindFirstChild(pet.id)
    end
    if not modelsFolder then
        return false, "no model for " .. pet.id
    end

    local root = getCharacterRoot()
    if not root then
        return false, "character not loaded"
    end

    local clone = modelsFolder:Clone()
    clone.Name = "VALT_" .. pet.id

    local jointed = setupModel(clone, pet)
    clone.Parent = getSpawnFolder()

    local targetPos
    if CONFIG.OWN_PLOT_ONLY then
        targetPos = getPlotSpawnPosition(#activeSpawns)
    end

    if not targetPos then
        if typeof(positionOverride) == "Vector3" then
            targetPos = positionOverride
        else
            local index = #activeSpawns
            local angle = (index % 12) * (math.pi / 6)
            local dist  = CONFIG.SPAWN_DISTANCE + math.floor(index / 12) * 6
            targetPos = root.Position + Vector3.new(math.cos(angle) * dist, 0, math.sin(angle) * dist)
        end
    end

    local finalPos = resolveGroundPosition(targetPos, clone)
    local look     = root.Position

    pcall(function()
        clone:PivotTo(CFrame.new(finalPos, Vector3.new(look.X, finalPos.Y, look.Z)))
    end)

    if CONFIG.NAMETAG then
        pcall(attachNametag, clone, pet)
    end

    playIdle(clone, pet)

    local now = os.clock()
    activeSpawns[#activeSpawns + 1] = {
        model     = clone,
        root      = clone.PrimaryPart,
        basePos   = finalPos,
        t0        = now,
        lastPay   = now,
        name      = pet.name,
        rate      = pet.rate,
        tagHeight = clone:GetExtentsSize().Y * 0.65 + 4,
        jointed   = jointed,
    }

    enforceSpawnLimit()
    return true
end

--=====================================================================
-- Clear spawns
--=====================================================================
local function clearSpawns()
    for _, record in ipairs(activeSpawns) do
        if record.model then
            pcall(function() record.model:Destroy() end)
        end
    end
    table.clear(activeSpawns)

    local folder = Workspace:FindFirstChild("__ValtSpawns")
    if folder then
        pcall(function() folder:Destroy() end)
    end
end

--=====================================================================
-- Money popup
--=====================================================================
local function moneyPopup(record)
    if not record.root or not record.root.Parent then return end

    local billboard = Instance.new("BillboardGui")
    billboard.Name = "ValtCash"
    billboard.Size = UDim2.fromOffset(150, 40)
    billboard.StudsOffsetWorldSpace = Vector3.new(0, record.tagHeight, 0)
    billboard.AlwaysOnTop = true
    billboard.MaxDistance = 300
    billboard.Adornee = record.root
    billboard.Parent = record.root

    local label = Instance.new("TextLabel")
    label.BackgroundTransparency = 1
    label.Size = UDim2.fromScale(1, 1)
    label.Font = Enum.Font.LuckiestGuy
    label.TextScaled = true
    label.TextColor3 = Color3.fromRGB(80, 235, 105)
    label.TextStrokeColor3 = Color3.fromRGB(10, 45, 15)
    label.TextStrokeTransparency = 0.15
    label.Text = "+$" .. abbreviateNumber(record.rate)
    label.Parent = billboard

    TweenService:Create(billboard,
        TweenInfo.new(1.1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
        { StudsOffsetWorldSpace = Vector3.new(0, record.tagHeight + 4.5, 0) }
    ):Play()

    TweenService:Create(label,
        TweenInfo.new(1.1, Enum.EasingStyle.Quad, Enum.EasingDirection.In),
        { TextTransparency = 1, TextStrokeTransparency = 1 }
    ):Play()

    Debris:AddItem(billboard, 1.3)
end

--=====================================================================
-- Tool giving
--=====================================================================
local function giveTool(pet)
    if not running then return false, "unloaded" end

    local modelsFolder = ReplicatedStorage:FindFirstChild("AssetModels")
    if modelsFolder then
        modelsFolder = modelsFolder:FindFirstChild(pet.id)
    end
    if not modelsFolder then
        return false, "no model for " .. pet.id
    end

    local backpack = LocalPlayer:FindFirstChildOfClass("Backpack")
    if not backpack then
        return false, "no backpack"
    end

    local tool = Instance.new("Tool")
    tool.Name = pet.name
    tool.ToolTip = string.format("%s | $%s/s - click to place (one use)", pet.rarity, abbreviateNumber(pet.rate))
    tool.CanBeDropped = false
    tool.RequiresHandle = true
    tool.TextureId = pet.icon

    local handle = Instance.new("Part")
    handle.Name = "Handle"
    handle.Size = Vector3.new(1, 1, 1)
    handle.Transparency = 1
    handle.CanCollide = false
    handle.CanQuery = false
    handle.CanTouch = false
    handle.Massless = true
    handle.Parent = tool

    local clone = modelsFolder:Clone()
    clone.Name = "Pet"

    local size   = clone:GetExtentsSize()
    local maxDim = math.max(size.X, size.Y, size.Z)
    if maxDim > CONFIG.HAND_MAX_STUDS then
        local factor = CONFIG.HAND_MAX_STUDS / maxDim
        pcall(function() clone:ScaleTo(factor ^ 0.5) end)
    end

    local primary = clone.PrimaryPart or clone:FindFirstChildWhichIsA("BasePart", true)

    local hasMotor = false
    for _, d in ipairs(clone:GetDescendants()) do
        if d:IsA("Motor6D") then
            hasMotor = true
            break
        end
    end

    for _, part in ipairs(clone:GetDescendants()) do
        if part:IsA("BasePart") then
            part.Anchored = false
            part.CanCollide = false
            part.CanQuery = false
            part.CanTouch = false
            part.Massless = true
        end
    end

    clone.Parent = tool

    if primary then
        local weld = Instance.new("Weld")
        weld.Part0 = handle
        weld.Part1 = primary
        weld.C0 = CFrame.new(0, clone:GetExtentsSize().Y * 0.4, 0)
        weld.Parent = handle
    end

    if not hasMotor then
        for _, d in ipairs(clone:GetDescendants()) do
            if d:IsA("BasePart") and d ~= primary then
                local wc = Instance.new("WeldConstraint")
                wc.Part0 = primary
                wc.Part1 = d
                wc.Parent = primary
            end
        end
    else
        playIdle(clone, pet)
    end

    tool.Parent = backpack
    activeTools[#activeTools + 1] = tool

    track(tool.Activated:Connect(function()
        local pos = getSpawnPosition()
        local ok, err = spawnPet(pet, pos)
        if not ok then
            warn("[VALT SPAWNER] place failed: " .. tostring(err))
            return
        end

        for i = #activeTools, 1, -1 do
            if activeTools[i] == tool then
                table.remove(activeTools, i)
            end
        end
        pcall(function() tool:Destroy() end)
    end))

    return true
end

local function clearTools()
    for _, tool in ipairs(activeTools) do
        pcall(function() tool:Destroy() end)
    end
    table.clear(activeTools)
end

--=====================================================================
-- Cash visual (fake local counter)
--=====================================================================
local cashVisual = {
    label = nil,
    base  = 0,
    bonus = 0,
    shown = 0,
    ours  = nil,
    conn  = nil,
}

local MULTIPLIERS = { K = 1e3, M = 1e6, B = 1e9, T = 1e12 }

local function parseMoney(text)
    local numStr, suffix = tostring(text):gsub("[%$,%s]", ""):match("^([%d%.]+)(%a*)$")
    if not numStr then return nil end
    local mult = suffix ~= "" and MULTIPLIERS[suffix:upper()] or 1
    if not mult then return nil end
    return (tonumber(numStr) or 0) * mult
end

local function formatMoney(n)
    local units = {
        { 1e12, "T" },
        { 1e9,  "B" },
        { 1e6,  "M" },
        { 1e3,  "K" },
    }
    for _, u in ipairs(units) do
        if n >= u[1] then
            local val = n / u[1]
            local fmt = val >= 100 and "%.0f" or "%.1f"
            return "$" .. string.format(fmt, val):gsub("%.0$", "") .. u[2]
        end
    end
    return "$" .. string.format("%.0f", n)
end

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

    local hud = playerGui:FindFirstChild("HUD")
    for _, name in ipairs({ "GameHUD", "BottomLeft", "Money", "Value" }) do
        hud = hud and hud:FindFirstChild(name)
    end

    if hud and hud:IsA("TextLabel") then
        return hud
    end

    for _, obj in ipairs(playerGui:GetDescendants()) do
        if obj:IsA("TextLabel") and obj.Text:match("^%$%d") and obj.AbsoluteSize.X > 40 then
            local parent = obj
            while parent do
                if parent:IsA("ScreenGui") then
                    if parent.Enabled then return obj end
                    break
                end
                parent = parent.Parent
            end
        end
    end

    return nil
end

local function hookCashLabel()
    local label = findMoneyLabel()
    if not label then return false end
    if label == cashVisual.label then return true end

    if cashVisual.conn then
        pcall(function() cashVisual.conn:Disconnect() end)
    end

    cashVisual.label = label
    cashVisual.base  = parseMoney(label.Text) or 0
    cashVisual.shown = cashVisual.base + cashVisual.bonus

    cashVisual.conn = track(label:GetPropertyChangedSignal("Text"):Connect(function()
        if label.Text ~= cashVisual.ours then
            cashVisual.base = parseMoney(label.Text) or cashVisual.base
        end
    end))

    return true
end

local function releaseCashLabel()
    if cashVisual.conn then
        pcall(function() cashVisual.conn:Disconnect() end)
    end

    if cashVisual.label and cashVisual.label.Parent then
        pcall(function()
            cashVisual.label.Text = formatMoney(cashVisual.base)
        end)
    end

    cashVisual.label = nil
    cashVisual.conn  = nil
    cashVisual.bonus = 0
    cashVisual.ours  = nil
end

--=====================================================================
-- Heartbeat loop
--=====================================================================
track(RunService.Heartbeat:Connect(function(dt)
    if not running then return end

    local now = os.clock()

    -- Cash visual
    if CONFIG.CASH_VISUAL then
        if not (cashVisual.label and cashVisual.label.Parent) then
            hookCashLabel()
        end

        if cashVisual.label and cashVisual.bonus > 0 then
            local target = cashVisual.base + cashVisual.bonus
            cashVisual.shown = cashVisual.shown + (target - cashVisual.shown) * math.min(1, (dt or 0.016) * 6)
            if math.abs(target - cashVisual.shown) < 1 then
                cashVisual.shown = target
            end
            cashVisual.ours = formatMoney(cashVisual.shown)
            cashVisual.label.Text = cashVisual.ours
        end
    end

    -- Spin/float + payout
    for i = #activeSpawns, 1, -1 do
        local record = activeSpawns[i]

        if not record.model or not record.model.Parent then
            table.remove(activeSpawns, i)
        else
            local shouldRotate = CONFIG.SPIN or CONFIG.FLOAT
            if shouldRotate and record.root and record.root.Anchored then
                local t     = now - record.t0
                local yOff  = CONFIG.FLOAT and math.sin(t * 1.6) * 0.6 or 0
                local yaw   = CONFIG.SPIN and t * 1.1 or 0
                record.root.CFrame = CFrame.new(record.basePos + Vector3.new(0, yOff, 0)) * CFrame.Angles(0, yaw, 0)
            end

            if now - record.lastPay >= CONFIG.PAY_INTERVAL then
                record.lastPay = now
                if CONFIG.MONEY_POPUP then
                    moneyPopup(record)
                end
                if CONFIG.CASH_VISUAL then
                    cashVisual.bonus = cashVisual.bonus + record.rate * CONFIG.PAY_INTERVAL
                end
            end
        end
    end
end))

--=====================================================================
-- Unload
--=====================================================================
local function unload()
    running = false

    for _, conn in ipairs(connections) do
        pcall(function() conn:Disconnect() end)
    end
    table.clear(connections)

    clearSpawns()
    clearTools()
    releaseCashLabel()

    if UI.window then
        pcall(function() UI.window:Destroy() end)
    end

    getgenv().__VALT_SPAWNER = nil
    warn("[VALT SPAWNER] unloaded")
end

--=====================================================================
-- Build UI
--=====================================================================
local function buildUI()
    local ok, WindUI = pcall(function()
        return loadstring(game:HttpGet("https://github.com/Footagesus/WindUI/releases/latest/download/main.lua"))()
    end)

    if not ok or not WindUI then
        warn("[VALT SPAWNER] WindUI failed to load: " .. tostring(WindUI))
        return
    end

    local vp = Camera and Camera.ViewportSize or Vector2.new(1280, 720)
    local size
    if UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled then
        size = UDim2.fromOffset(
            math.clamp(math.floor(vp.X * 0.92), 300, 560),
            math.clamp(math.floor(vp.Y * 0.82), 260, 420)
        )
    else
        size = UDim2.fromOffset(620, 480)
    end

    local window = WindUI:CreateWindow({
        Title         = "VALT :: Pet Spawner",
        Icon          = "egg",
        Author        = "Steal An Egg | " .. #pets .. " assets",
        Folder        = "Valt Spawner",
        Size          = size,
        Transparent   = true,
        Theme         = "Dark",
        SideBarWidth  = 165,
        HideSearchBar = false,
        OpenButton    = { Enabled = false },
    })
    UI.window = window

    if getgenv().__VALT_SPAWNER then
        getgenv().__VALT_SPAWNER.window = window
    end

    local petsTab    = window:Tab({ Title = "Pets",    Icon = "paw-print" })
    local previewTab = window:Tab({ Title = "Preview", Icon = "eye" })
    local optionsTab = window:Tab({ Title = "Options", Icon = "settings" })
    UI.gridTab = petsTab

    petsTab:Input({
        Title       = "Search",
        Placeholder = "pet name...",
        Callback    = function(text)
            filter.text = string.lower(tostring(text or ""))
            UI.rebuild()
        end,
    })

    petsTab:Dropdown({
        Title    = "Rarity",
        Values   = rarityList,
        Value    = "All",
        Callback = function(value)
            filter.rarity = tostring(value)
            UI.rebuild()
        end,
    })

    UI.counter = petsTab:Paragraph({
        Title = "Loaded",
        Desc  = #pets .. " assets ready - click a card to spawn",
    })

    petsTab:Divider()

    local function clearRows()
        for _, row in ipairs(UI.rows) do
            local frame = row.ElementFrame
            if typeof(frame) == "Instance" then
                pcall(function() frame:Destroy() end)
            end
        end
        table.clear(UI.rows)
    end

    local rebuildId = 0

    function UI.rebuild()
        rebuildId += 1
        local thisId = rebuildId
        clearRows()

        local filtered = {}
        for _, pet in ipairs(pets) do
            local nameMatch   = filter.text == "" or string.find(string.lower(pet.name), filter.text, 1, true) ~= nil
            local rarityMatch = filter.rarity == "All" or pet.rarity == filter.rarity
            if nameMatch and rarityMatch then
                filtered[#filtered + 1] = pet
            end
        end

        if UI.counter then
            pcall(function()
                UI.counter:SetDesc(#filtered .. " of " .. #pets .. " assets shown - click a card to spawn")
            end)
        end

        task.spawn(function()
            local row
            local col = 0

            for i, pet in ipairs(filtered) do
                if thisId ~= rebuildId or not running then return end

                if col == 0 then
                    row = petsTab:HStack({})
                    UI.rows[#UI.rows + 1] = row
                end

                local btn = row:Button({
                    Title = pet.name,
                    Desc  = string.format("%s | $%s/s", pet.rarity, abbreviateNumber(pet.rate)),
                    Callback = function()
                        selectedPet = pet

                        if CONFIG.GIVE_TOOL then
                            local success, err = giveTool(pet)
                            WindUI:Notify({
                                Title    = success and (pet.name .. " -> backpack") or "tool failed",
                                Content  = success and "equip it, then click to place the pet" or tostring(err),
                                Duration = success and 2 or 4,
                                Icon     = success and "backpack" or "circle-alert",
                            })
                            return
                        end

                        local success, err = spawnPet(pet)
                        WindUI:Notify({
                            Title    = success and (pet.name .. " spawned") or "spawn failed",
                            Content  = success
                                and (pet.rarity .. " | " .. #activeSpawns .. "/" .. CONFIG.MAX_SPAWNS .. " active")
                                or tostring(err),
                            Duration = success and 2 or 4,
                            Icon     = success and "sparkles" or "circle-alert",
                        })
                    end,
                })

                if pet.icon ~= "" then
                    pcall(function() btn:SetThumbnail(pet.icon) end)
                end

                col += 1
                if col >= CONFIG.GRID_COLUMNS then
                    col = 0
                    if i % (CONFIG.GRID_COLUMNS * 4) == 0 then
                        task.wait()
                    end
                end
            end
        end)
    end

    --=================================================================
    -- Preview tab
    --=================================================================
    previewTab:Section({ Title = "Model viewer" })

    local petNames = {}
    for _, pet in ipairs(pets) do
        petNames[#petNames + 1] = pet.name
    end

    local function previewPet(name)
        local pet
        for _, p in ipairs(pets) do
            if p.name == name then pet = p break end
        end
        if not pet then return end

        selectedPet = pet

        local modelsFolder = ReplicatedStorage:FindFirstChild("AssetModels")
        if modelsFolder then modelsFolder = modelsFolder:FindFirstChild(pet.id) end

        if not modelsFolder then
            WindUI:Notify({
                Title    = "no model",
                Content  = pet.id,
                Duration = 3,
                Icon     = "circle-alert",
            })
            return
        end

        if UI.viewport then
            pcall(function()
                UI.viewport:SetObject({ modelsFolder:Clone() })
            end)
        end

        if UI.previewInfo then
            pcall(function()
                UI.previewInfo:SetTitle(pet.name)
                UI.previewInfo:SetDesc(string.format(
                    "Rarity: %s | Earns: $%s/s | Base scale: %.2f",
                    pet.rarity, abbreviateNumber(pet.rate), pet.scale
                ))
                if pet.icon ~= "" then
                    UI.previewInfo:SetImage(pet.icon)
                end
            end)
        end
    end

    previewTab:Dropdown({
        Title    = "Pet",
        Values   = petNames,
        Value    = petNames[1],
        Callback = previewPet,
    })

    UI.previewInfo = previewTab:Paragraph({
        Title     = "-",
        Desc      = "pick a pet",
        Image     = "",
        ImageSize = 48,
    })

    local previewSource = ReplicatedStorage:FindFirstChild("AssetModels")
    if previewSource and pets[1] then
        previewSource = previewSource:FindFirstChild(pets[1].id)
    else
        previewSource = nil
    end

    UI.viewport = previewTab:Viewport({
        Title       = "Model",
        Object      = previewSource and previewSource:Clone() or Instance.new("Model"),
        Height      = 220,
        Interactive = true,
    })

    previewTab:Button({
        Title = "Take into backpack",
        Desc  = "hands you the previewed pet as a tool",
        Callback = function()
            if not selectedPet then return end
            local success, err = giveTool(selectedPet)
            WindUI:Notify({
                Title    = success and (selectedPet.name .. " -> backpack") or "tool failed",
                Content  = success and "equip it, then click to place the pet" or tostring(err),
                Duration = 3,
                Icon     = success and "backpack" or "circle-alert",
            })
        end,
    })

    previewTab:Button({
        Title = "Spawn this one",
        Desc  = "drops the previewed pet in front of you",
        Callback = function()
            if not selectedPet then return end
            local success, err = spawnPet(selectedPet)
            WindUI:Notify({
                Title    = success and (selectedPet.name .. " spawned") or "spawn failed",
                Content  = success
                    and (#activeSpawns .. "/" .. CONFIG.MAX_SPAWNS .. " active")
                    or tostring(err),
                Duration = 3,
                Icon     = success and "sparkles" or "circle-alert",
            })
        end,
    })

    --=================================================================
    -- Options tab
    --=================================================================
    optionsTab:Section({ Title = "Spawn behaviour" })

    optionsTab:Toggle({
        Title    = "Give as backpack tool",
        Desc     = "on = card click hands you the pet, click again in-world to place it",
        Value    = CONFIG.GIVE_TOOL,
        Callback = function(v) CONFIG.GIVE_TOOL = v end,
    })

    optionsTab:Slider({
        Title    = "Held pet size (studs)",
        Step     = 1,
        Value    = { Min = 2, Max = 14, Default = CONFIG.HAND_MAX_STUDS },
        Callback = function(v) CONFIG.HAND_MAX_STUDS = tonumber(v) or 4 end,
    })

    optionsTab:Slider({
        Title    = "Scale multiplier",
        Step     = 0.1,
        Value    = { Min = 0.2, Max = 8, Default = CONFIG.SCALE_MULT },
        Callback = function(v) CONFIG.SCALE_MULT = tonumber(v) or 1 end,
    })

    optionsTab:Slider({
        Title    = "Spawn distance",
        Step     = 1,
        Value    = { Min = 4, Max = 60, Default = CONFIG.SPAWN_DISTANCE },
        Callback = function(v) CONFIG.SPAWN_DISTANCE = tonumber(v) or 12 end,
    })

    optionsTab:Slider({
        Title    = "Max active spawns",
        Step     = 1,
        Value    = { Min = 1, Max = 120, Default = CONFIG.MAX_SPAWNS },
        Callback = function(v)
            CONFIG.MAX_SPAWNS = math.floor(tonumber(v) or 40)
            enforceSpawnLimit()
        end,
    })

    optionsTab:Toggle({
        Title    = "Spin",
        Value    = CONFIG.SPIN,
        Callback = function(v) CONFIG.SPIN = v end,
    })

    optionsTab:Toggle({
        Title    = "Float",
        Value    = CONFIG.FLOAT,
        Callback = function(v) CONFIG.FLOAT = v end,
    })

    optionsTab:Toggle({
        Title    = "Place in my plot only",
        Value    = CONFIG.OWN_PLOT_ONLY,
        Callback = function(v) CONFIG.OWN_PLOT_ONLY = v end,
    })

    optionsTab:Toggle({
        Title    = "Sit on the ground",
        Value    = CONFIG.GROUND,
        Callback = function(v) CONFIG.GROUND = v end,
    })

    optionsTab:Toggle({
        Title    = "Play idle animation",
        Value    = CONFIG.ANIMATE,
        Callback = function(v) CONFIG.ANIMATE = v end,
    })

    optionsTab:Toggle({
        Title    = "Name tags",
        Value    = CONFIG.NAMETAG,
        Callback = function(v) CONFIG.NAMETAG = v end,
    })

    optionsTab:Toggle({
        Title    = "Count it on my cash",
        Desc     = "your own money label rises with the popups - visual only, the server never sees it",
        Value    = CONFIG.CASH_VISUAL,
        Callback = function(v)
            CONFIG.CASH_VISUAL = v
            if not v then releaseCashLabel() end
        end,
    })

    optionsTab:Toggle({
        Title    = "Money popups",
        Desc     = "green +$X off every spawn, same as the game's own pets",
        Value    = CONFIG.MONEY_POPUP,
        Callback = function(v) CONFIG.MONEY_POPUP = v end,
    })

    optionsTab:Slider({
        Title    = "Payout interval (s)",
        Step     = 0.5,
        Value    = { Min = 0.5, Max = 10, Default = CONFIG.PAY_INTERVAL },
        Callback = function(v) CONFIG.PAY_INTERVAL = tonumber(v) or 1 end,
    })

    optionsTab:Toggle({
        Title    = "Anchored",
        Desc     = "off = the rig falls on its own joints",
        Value    = CONFIG.ANCHORED,
        Callback = function(v) CONFIG.ANCHORED = v end,
    })

    optionsTab:Divider()
    optionsTab:Section({ Title = "Bulk" })

    optionsTab:Button({
        Title = "Spawn random",
        Callback = function()
            local pet = pets[math.random(1, #pets)]
            spawnPet(pet)
            WindUI:Notify({
                Title    = pet.name,
                Content  = "random spawn",
                Duration = 2,
                Icon     = "dices",
            })
        end,
    })

    optionsTab:Button({
        Title = "Spawn every pet of the selected rarity",
        Desc  = "uses the rarity filter from the Pets tab",
        Callback = function()
            task.spawn(function()
                local count = 0
                for _, pet in ipairs(pets) do
                    local match = filter.rarity == "All" or pet.rarity == filter.rarity
                    if match then
                        if spawnPet(pet) then count += 1 end
                        task.wait(0.05)
                    end
                end
                WindUI:Notify({
                    Title    = "bulk spawn",
                    Content  = count .. " spawned",
                    Duration = 3,
                    Icon     = "sparkles",
                })
            end)
        end,
    })

    optionsTab:Button({
        Title = "Clear all spawns",
        Callback = function()
            local count = #activeSpawns
            clearSpawns()
            WindUI:Notify({
                Title    = "cleared",
                Content  = count .. " removed",
                Duration = 2,
                Icon     = "trash-2",
            })
        end,
    })

    optionsTab:Button({
        Title = "Empty the backpack",
        Desc  = "removes every pet tool this script handed you",
        Callback = function()
            local count = #activeTools
            clearTools()
            WindUI:Notify({
                Title    = "backpack cleared",
                Content  = count .. " tools removed",
                Duration = 2,
                Icon     = "backpack",
            })
        end,
    })

    optionsTab:Divider()

    optionsTab:Button({
        Title = "Unload (End)",
        Desc  = "kills every spawn, the hub and all connections",
        Callback = function()
            getgenv().__VALT_SPAWNER.unload()
        end,
    })

    UI.rebuild()
    if petNames[1] then previewPet(petNames[1]) end
    pcall(function() window:SelectTab(petsTab.Index) end)

    return window
end

--=====================================================================
-- Global API + End keybind
--=====================================================================
getgenv().__VALT_SPAWNER = {
    unload = unload,
    CONFIG = CONFIG,
    PETS   = pets,
    spawn  = spawnPet,
    give   = giveTool,
    tools  = activeTools,
    clear  = clearSpawns,
}

track(UserInputService.InputBegan:Connect(function(input, processed)
    if processed then return end
    if input.KeyCode == Enum.KeyCode.End then
        unload()
    end
end))

--=====================================================================
-- Boot
--=====================================================================
buildUI()

warn("[VALT SPAWNER] loaded - " .. #pets .. " assets indexed. Unload: End.")

Advertisements

Comments

0

Did it work for you? Sign in to share your experience.

Sign in to comment

No comments yet

Tried it? Let others know if it works.