Script
local function dbg(msg) pcall(function() print("[UGC Anim Studio] " .. tostring(msg)) end) end
local plrs = game:GetService("Players")
local as = game:GetService("AssetService")
local aes = game:GetService("AvatarEditorService")
local rs = game:GetService("RunService")
local hs = game:GetService("HttpService")
local cp = game:GetService("ContentProvider")
local uis = game:GetService("UserInputService")
local mps = game:GetService("MarketplaceService")
local lp = plrs.LocalPlayer
local env = (typeof(getgenv) == "function" and getgenv()) or _G
if type(env.UgcAnimStudioCleanup) == "function" then pcall(env.UgcAnimStudioCleanup) end
local connections = {}
local function track(c) connections[#connections + 1] = c; return c end
local toast
local onMetaChanged
local onHistoryChanged
local camera = workspace.CurrentCamera or workspace:WaitForChild("Camera", 10)
local vpSize = camera and camera.ViewportSize or Vector2.new(1280, 720)
local W0, H0 = 700, 450
local baseScale = math.clamp(math.min(vpSize.X / 1920, vpSize.Y / 1080), 0.55, 3) * 1.35
local scale = math.min(baseScale, (vpSize.X * 0.97) / W0, (vpSize.Y * 0.95) / H0)
if scale <= 0 or scale ~= scale then scale = 1 end
local function s(n) return math.round(n * scale) end
local C = {
bg = Color3.fromRGB(38, 38, 40),
panel = Color3.fromRGB(46, 46, 48),
surface = Color3.fromRGB(58, 58, 60),
surfaceHi = Color3.fromRGB(74, 74, 76),
stroke = Color3.fromRGB(96, 96, 98),
accent = Color3.fromRGB(150, 150, 152),
accent2 = Color3.fromRGB(180, 180, 182),
text = Color3.fromRGB(232, 232, 234),
textDim = Color3.fromRGB(178, 178, 180),
textMuted = Color3.fromRGB(130, 130, 132),
overlay = Color3.fromRGB(24, 24, 26),
green = Color3.fromRGB(150, 150, 152),
red = Color3.fromRGB(190, 120, 120),
gold = Color3.fromRGB(210, 190, 140),
orange = Color3.fromRGB(190, 160, 120),
pillBg = Color3.fromRGB(72, 72, 76),
pillTx = Color3.fromRGB(210, 210, 212),
badBg = Color3.fromRGB(90, 60, 60),
badTx = Color3.fromRGB(220, 170, 170),
wearBg = Color3.fromRGB(88, 88, 92),
}
local WHITE = Color3.new(1, 1, 1)
local function new(class, props, parent)
local o = Instance.new(class)
for k, v in pairs(props or {}) do o[k] = v end
if parent then o.Parent = parent end
return o
end
local EASE = {
Linear = function(x) return x end,
Quad = function(x) return 1 - (1 - x) * (1 - x) end,
Quint = function(x) local y = 1 - x; return 1 - y * y * y * y * y end,
Back = function(x)
local c1 = 1.70158
local y = x - 1
return 1 + (c1 + 1) * y * y * y + c1 * y * y
end,
}
local function lerpValue(a, b, e)
local ty = typeof(a)
if ty == "number" then return a + (b - a) * e end
return a:Lerp(b, e)
end
local activeTweens, tweenSlots = {}, setmetatable({}, {__mode = "k"})
local tweenConn
local TweenObj = {}
TweenObj.__index = TweenObj
function TweenObj:Cancel() self.cancelled = true end
TweenObj.Destroy = TweenObj.Cancel
local function releaseTween(rec)
local slots = tweenSlots[rec.obj]
if slots then
for prop in pairs(rec.p) do
if slots[prop] == rec then slots[prop] = nil end
end
end
end
local function tweenStep()
local now = os.clock()
local n = #activeTweens
local i = 1
while i <= n do
local rec = activeTweens[i]
local o = rec.obj
local done = rec.cancelled or o.Parent == nil or next(rec.p) == nil
if not done then
local x = (now - rec.t0) / rec.dur
if rec.loop then
x = x - math.floor(x)
elseif x >= 1 then
x = 1
done = true
end
local e = rec.ease(x)
for prop, pr in pairs(rec.p) do
if done and not rec.loop then
o[prop] = pr[2]
else
o[prop] = lerpValue(pr[1], pr[2], e)
end
end
end
if done then
releaseTween(rec)
activeTweens[i] = activeTweens[n]
activeTweens[n] = nil
n -= 1
else
i += 1
end
end
if n == 0 and tweenConn then
tweenConn:Disconnect()
tweenConn = nil
end
end
local function tw(o, time, props, style, dir, loop)
local slots = tweenSlots[o]
if not slots then slots = {}; tweenSlots[o] = slots end
local rec = setmetatable({
obj = o, p = {}, t0 = os.clock(), dur = math.max(time, 0.001), loop = loop and true or false,
ease = EASE[style and style.Name or "Quad"] or EASE.Quad,
}, TweenObj)
if dir == Enum.EasingDirection.In and rec.loop == false and style and style.Name ~= "Linear" then
local out = rec.ease
rec.ease = function(x) return 1 - out(1 - x) end
end
for prop, goal in pairs(props) do
local old = slots[prop]
if old then old.p[prop] = nil end
local ok, from = pcall(function() return o[prop] end)
if ok and from ~= nil then
rec.p[prop] = {from, goal}
slots[prop] = rec
end
end
if next(rec.p) == nil then return rec end
activeTweens[#activeTweens + 1] = rec
if not tweenConn then tweenConn = rs.RenderStepped:Connect(tweenStep) end
return rec
end
local function tweenShutdown()
if tweenConn then tweenConn:Disconnect(); tweenConn = nil end
table.clear(activeTweens)
end
local function collectFade(root)
local list = {}
local function collect(obj)
local isText = obj:IsA("TextLabel") or obj:IsA("TextButton") or obj:IsA("TextBox")
list[#list + 1] = {obj = obj, bgT = obj.BackgroundTransparency, txtT = isText and obj.TextTransparency or nil}
for _, c in ipairs(obj:GetChildren()) do
if c:IsA("GuiObject") then collect(c) end
end
end
collect(root)
return list
end
local function corner(p, r) return new("UICorner", {CornerRadius = UDim.new(0, r or s(8))}, p) end
local function round(p) return new("UICorner", {CornerRadius = UDim.new(1, 0)}, p) end
local function stroke(p, color, th, tr)
return new("UIStroke", {Color = color or C.stroke, Thickness = th or 1, Transparency = tr or 0, ApplyStrokeMode = Enum.ApplyStrokeMode.Border}, p)
end
local function pad(p, l, r, t, b)
return new("UIPadding", {PaddingLeft = UDim.new(0, l or 0), PaddingRight = UDim.new(0, r or 0), PaddingTop = UDim.new(0, t or 0), PaddingBottom = UDim.new(0, b or 0)}, p)
end
local shimmers = {}
local function applyShimmer(obj, c1, c2, speed)
if shimmers[obj] then return end
local grad = new("UIGradient", {
Name = "ShimmerGrad", Rotation = 25, Offset = Vector2.new(-1, 0),
Color = ColorSequence.new({ColorSequenceKeypoint.new(0, c1), ColorSequenceKeypoint.new(0.5, c2), ColorSequenceKeypoint.new(1, c1)}),
}, obj)
local tween = tw(grad, speed or 1.3, {Offset = Vector2.new(1, 0)}, Enum.EasingStyle.Linear, Enum.EasingDirection.In, true)
shimmers[obj] = {grad, tween}
end
local function stopShimmer(obj)
local d = shimmers[obj]
if d then
pcall(function() d[2]:Cancel() end)
pcall(function() d[1]:Destroy() end)
shimmers[obj] = nil
end
end
local btnState = setmetatable({}, {__mode = "k"})
local function bindHover(b)
b.MouseEnter:Connect(function()
local st = btnState[b]; if not st then return end
st.hover = true
tw(b, 0.12, {BackgroundColor3 = st.bg:Lerp(WHITE, 0.1)})
end)
b.MouseLeave:Connect(function()
local st = btnState[b]; if not st then return end
st.hover = false
tw(b, 0.15, {BackgroundColor3 = st.bg})
end)
b.MouseButton1Down:Connect(function()
local st = btnState[b]; if not st then return end
tw(b, 0.08, {BackgroundColor3 = st.bg:Lerp(Color3.new(0, 0, 0), 0.15)})
end)
b.MouseButton1Up:Connect(function()
local st = btnState[b]; if not st then return end
tw(b, 0.12, {BackgroundColor3 = st.hover and st.bg:Lerp(WHITE, 0.1) or st.bg})
end)
end
local function mkBtn(parent, props, bg, bgT, txt)
props = props or {}
props.AutoButtonColor = false
props.BorderSizePixel = 0
props.Font = props.Font or Enum.Font.GothamBold
props.TextSize = props.TextSize or s(15)
props.BackgroundColor3 = bg
props.BackgroundTransparency = bgT or 0
props.TextColor3 = txt or C.text
local b = new("TextButton", props, parent)
btnState[b] = {bg = bg, hover = false}
bindHover(b)
return b
end
local function setBtn(b, bg, bgT, txt)
local st = btnState[b]; if not st then return end
st.bg = bg
tw(b, 0.15, {
BackgroundColor3 = st.hover and bg:Lerp(WHITE, 0.1) or bg,
BackgroundTransparency = bgT or 0,
TextColor3 = txt or b.TextColor3,
})
end
local function commas(n)
local str = tostring(math.floor(tonumber(n) or 0))
local out = str:reverse():gsub("(%d%d%d)", "%1,"):reverse()
if out:sub(1, 1) == "," then out = out:sub(2) end
return out
end
local function enumItem(enumName, itemName)
local ok, v = pcall(function() return Enum[enumName][itemName] end)
if ok and typeof(v) == "EnumItem" then return v end
return nil
end
local function setParam(p, name, value) return (pcall(function() p[name] = value end)) end
local m = {
["runanimation"] = "run", ["climbanimation"] = "climb", ["jumpanimation"] = "jump", ["fallanimation"] = "fall",
["idleanimation"] = "idle", ["swimidleanimation"] = "swimidle", ["swimanimation"] = "swim", ["walkanimation"] = "walk",
}
local reverseM = {
["run"] = "runanimation", ["climb"] = "climbanimation", ["jump"] = "jumpanimation", ["fall"] = "fallanimation",
["idle"] = "idleanimation", ["swimidle"] = "swimidleanimation", ["swim"] = "swimanimation", ["walk"] = "walkanimation",
}
local shortNames = {
["idleanimation"] = "Idle", ["walkanimation"] = "Walk", ["runanimation"] = "Run", ["jumpanimation"] = "Jump",
["fallanimation"] = "Fall", ["climbanimation"] = "Climb", ["swimidleanimation"] = "S.Idle", ["swimanimation"] = "Swim",
}
local buttonOrder = {"idleanimation", "walkanimation", "runanimation", "jumpanimation", "fallanimation", "climbanimation", "swimidleanimation", "swimanimation"}
local CARD_ORDER = {"idleanimation", "walkanimation", "runanimation", "jumpanimation", "fallanimation", "climbanimation", "swimanimation"}
local DESC_FOLDERS = {
IdleAnimation = {"idle"}, WalkAnimation = {"walk"}, RunAnimation = {"run"}, JumpAnimation = {"jump"},
FallAnimation = {"fall"}, ClimbAnimation = {"climb"}, SwimAnimation = {"swim", "swimidle"},
}
local ASSET_TYPE_IDS = {
[48] = "climbanimation", [50] = "fallanimation", [51] = "idleanimation", [52] = "jumpanimation",
[53] = "runanimation", [54] = "swimanimation", [55] = "walkanimation",
}
local CACHE_FILE_NAME = "animation_bundle_data_cache.json"
local ANIM_CACHE_URL = "https://raw.githubusercontent.com/TribalFootball/TuffTeto/main/animation_bundle_data_cache.json"
local EQUIPPED_FILE = "FeUgcAnim.json"
local SAVED_BUNDLES_FILE = "FeUgcAnim_Bookmarks.json"
local HISTORY_FILE = "FeUgcAnim_History.json"
local HISTORY_MAX = 30
local PRESETS_FILE = "FeUgcAnim_Presets.json"
local SETTINGS_FILE = "FeUgcAnim_Settings.json"
local LAST_PACK_FILE = "FeUgcAnim_LastPack.json"
local function loadJSON(file)
local ok, content = pcall(function()
if isfile and not isfile(file) then return nil end
return readfile(file)
end)
if ok and type(content) == "string" and content ~= "" then
local suc, decoded = pcall(function() return hs:JSONDecode(content) end)
if suc and type(decoded) == "table" then return decoded end
end
return {}
end
local function saveJSON(file, data)
pcall(function() writefile(file, hs:JSONEncode(data)) end)
end
local queued = {}
local function queueSave(file, data)
if queued[file] then return end
queued[file] = true
task.delay(1.5, function()
queued[file] = nil
saveJSON(file, data)
end)
end
local assetCache, inflight, detailsCache, metaCache = {}, {}, {}, {}
local fileCache = loadJSON(CACHE_FILE_NAME)
local equippedAnims = loadJSON(EQUIPPED_FILE)
local savedBookmarks = loadJSON(SAVED_BUNDLES_FILE)
local historyList = loadJSON(HISTORY_FILE)
local presetList = loadJSON(PRESETS_FILE)
local scriptSettings = loadJSON(SETTINGS_FILE)
local lastPackData = loadJSON(LAST_PACK_FILE)
if next(presetList) ~= nil and presetList[1] == nil then presetList = {} end
if next(historyList) ~= nil and historyList[1] == nil then historyList = {} end
if not scriptSettings.globalSpeed then scriptSettings.globalSpeed = 1 end
if not scriptSettings.customSpeeds then scriptSettings.customSpeeds = {} end
if scriptSettings.advanceSpeed == nil then scriptSettings.advanceSpeed = false end
if scriptSettings.restoreLastPack == nil then scriptSettings.restoreLastPack = false end
if scriptSettings.forceAnimation == nil then scriptSettings.forceAnimation = false end
local function saveSettings() saveJSON(SETTINGS_FILE, scriptSettings) end
task.spawn(function()
local ok, body = pcall(function() return game:HttpGet(ANIM_CACHE_URL) end)
if ok and type(body) == "string" then
local ok2, data = pcall(function() return hs:JSONDecode(body) end)
if ok2 and type(data) == "table" then
for k, v in pairs(data) do if not fileCache[k] then fileCache[k] = v end end
queueSave(CACHE_FILE_NAME, fileCache)
end
end
end)
local function normType(t) return (string.lower(tostring(t or "")):gsub("[%s_%-]", "")) end
local function classifyItem(item)
local n = tonumber(item.AssetType)
if n and ASSET_TYPE_IDS[n] then return ASSET_TYPE_IDS[n] end
local lt = normType(item.AssetType)
if lt == "swimanimation" or shortNames[lt] then return lt end
local nm = string.lower(tostring(item.Name or ""))
if nm:find("swim") then return "swimanimation" end
if nm:find("idle") then return "idleanimation" end
if nm:find("walk") then return "walkanimation" end
if nm:find("run") then return "runanimation" end
if nm:find("jump") then return "jumpanimation" end
if nm:find("fall") then return "fallanimation" end
if nm:find("climb") then return "climbanimation" end
return nil
end
local function mapBundleItems(items)
local map = {}
for _, item in ipairs(items or {}) do
local slot = classifyItem(item)
if slot then
local payload = {Id = item.Id, Name = item.Name, AssetType = slot}
if slot == "swimanimation" then
map.swimanimation = payload
map.swimidleanimation = payload
else
map[slot] = payload
end
end
end
return map
end
local function slotToDescProp(assetType)
local lt = normType(assetType)
if lt:find("idle") and not lt:find("swim") then return "IdleAnimation"
elseif lt:find("walk") then return "WalkAnimation"
elseif lt:find("run") then return "RunAnimation"
elseif lt:find("jump") then return "JumpAnimation"
elseif lt:find("fall") then return "FallAnimation"
elseif lt:find("climb") then return "ClimbAnimation"
elseif lt:find("swim") then return "SwimAnimation"
end
return "IdleAnimation"
end
local function fetchAnimations(id, bundleId, assetType)
local t, ser = {}, {}
local descProp = slotToDescProp(assetType)
pcall(function()
local desc = Instance.new("HumanoidDescription")
desc[descProp] = tonumber(id)
local dummy = plrs:CreateHumanoidModelFromDescription(desc, Enum.HumanoidRigType.R15)
local animate = dummy:FindFirstChild("Animate")
if animate then
for _, folderName in ipairs(DESC_FOLDERS[descProp] or {}) do
local folder = animate:FindFirstChild(folderName)
if folder then
for _, child in ipairs(folder:GetChildren()) do
if child:IsA("Animation") and child.AnimationId ~= "" then
local clone = child:Clone()
t[#t + 1] = clone
ser[#ser + 1] = {Name = clone.Name, AnimationId = clone.AnimationId, BundleId = tostring(bundleId)}
end
end
end
end
end
dummy:Destroy()
end)
if #t == 0 then
pcall(function()
local objs = game:GetObjects("rbxassetid://" .. tostring(id))
if objs and #objs > 0 then
local function processItem(item)
if item:IsA("Animation") then
t[#t + 1] = item:Clone()
ser[#ser + 1] = {Name = item.Name, AnimationId = item.AnimationId, BundleId = tostring(bundleId)}
end
end
processItem(objs[1])
for _, c in ipairs(objs[1]:GetDescendants()) do processItem(c) end
end
end)
end
return t, ser
end
local function get(id, bundleId, assetType)
local sid = tostring(id)
if assetCache[sid] then return assetCache[sid] end
local cached = fileCache[sid]
if type(cached) == "table" and #cached > 0 then
local rebuilt = {}
for _, d in ipairs(cached) do
if d.AnimationId and d.AnimationId ~= "" then
local a = Instance.new("Animation")
a.Name = d.Name or "Anim"
a.AnimationId = d.AnimationId
rebuilt[#rebuilt + 1] = a
end
end
if #rebuilt > 0 then assetCache[sid] = rebuilt; return rebuilt end
end
if inflight[sid] then
local t0 = os.clock()
while inflight[sid] and os.clock() - t0 < 20 do task.wait(0.05) end
return assetCache[sid] or {}
end
inflight[sid] = true
local ok, t, ser = pcall(fetchAnimations, id, bundleId, assetType)
inflight[sid] = nil
if not ok or type(t) ~= "table" then return {} end
if #t > 0 then
assetCache[sid] = t
fileCache[sid] = ser
queueSave(CACHE_FILE_NAME, fileCache)
end
return t
end
local function clearAssetCache(id)
local sid = tostring(id)
assetCache[sid], fileCache[sid] = nil, nil
queueSave(CACHE_FILE_NAME, fileCache)
end
local function getSpecificTrack(fetchedTracks, targetType)
if #fetchedTracks == 0 then return nil end
if targetType == "swimidleanimation" then
for _, tr in ipairs(fetchedTracks) do
if string.lower(tr.Name):find("idle") then return tr end
end
return fetchedTracks[2] or fetchedTracks[1]
elseif targetType == "swimanimation" then
for _, tr in ipairs(fetchedTracks) do
if string.lower(tr.Name) == "swim" then return tr end
end
end
return fetchedTracks[1]
end
local function getTracksFor(payload, slot)
if payload.Direct then
if not payload.Tracks then
local out = {}
for _, d in ipairs(payload.Direct) do
if d.AnimationId and d.AnimationId ~= "" then
local a = Instance.new("Animation")
a.Name = d.Name or "Anim"
a.AnimationId = d.AnimationId
out[#out + 1] = a
end
end
payload.Tracks = out
end
return payload.Tracks
end
local list = get(payload.Id, nil, payload.AssetType)
if #list == 0 then return {} end
if slot == "idleanimation" then return list end
local spec = getSpecificTrack(list, slot)
return spec and {spec} or {}
end
local function preloadAnimations(list)
if #list > 0 then pcall(function() cp:PreloadAsync(list) end) end
end
local function findStoredEntry(key)
local e = savedBookmarks[key]
if type(e) == "table" and e.Items then return e end
for _, h in ipairs(historyList) do
if tostring(h.Id) == key and h.Items then return h end
end
return nil
end
local function getDetails(id)
local key = tostring(id)
if detailsCache[key] then return detailsCache[key] end
local stored = findStoredEntry(key)
if stored then
local res = {Items = stored.Items}
detailsCache[key] = res
return res
end
local ok, res = pcall(function() return as:GetBundleDetailsAsync(id) end)
if ok and type(res) == "table" and res.Items then
detailsCache[key] = res
return res
end
return nil
end
local function normalizeAnimationId(value)
local raw = tostring(value or "")
local digits = raw:match("(%d+)")
return digits or raw
end
local function getTargetSpeed(slot)
if scriptSettings.advanceSpeed and slot then
return tonumber(scriptSettings.customSpeeds[slot]) or 1
end
return tonumber(scriptSettings.globalSpeed) or 1
end
local function applySpeed(track, slot)
if not track then return end
local speed = getTargetSpeed(slot)
local currentSpeed
pcall(function() currentSpeed = track.Speed end)
if type(currentSpeed) ~= "number" or math.abs(currentSpeed - speed) > 0.001 then
pcall(function() track:AdjustSpeed(speed) end)
end
end
local function getTrackSlot(tr)
local animation = tr and tr.Animation
if not animation then return nil end
local character = lp.Character
local animate = character and character:FindFirstChild("Animate")
local node = animation
while node and node ~= animate do
local slot = reverseM[string.lower(node.Name)]
if slot then return slot end
node = node.Parent
end
local targetId = normalizeAnimationId(animation.AnimationId)
if animate and targetId ~= "" then
for folderName, slot in pairs(reverseM) do
local folder = animate:FindFirstChild(folderName)
if folder then
for _, item in ipairs(folder:GetDescendants()) do
if item:IsA("Animation") and normalizeAnimationId(item.AnimationId) == targetId then
return slot
end
end
end
end
end
for sName, list in pairs(equippedAnims) do
for _, anim in ipairs(list) do
if anim.Name == animation.Name or normalizeAnimationId(anim.AnimationId) == targetId then
return sName
end
end
end
return nil
end
local function applyTrackSpeed(tr)
local animation = tr and tr.Animation
if not animation then return end
local pName = animation.Parent and string.lower(animation.Parent.Name) or ""
local animationName = string.lower(animation.Name or "")
local isTool = pName == "tool" or animationName:find("tool", 1, true) ~= nil
if scriptSettings.forceAnimation and isTool then
tr:Stop(0)
return
end
applySpeed(tr, getTrackSlot(tr))
end
local function applyCurrentCharacterSpeeds()
local character = lp.Character
local human = character and character:FindFirstChildOfClass("Humanoid")
local animator = human and human:FindFirstChildOfClass("Animator")
if not animator then return end
for _, tr in ipairs(animator:GetPlayingAnimationTracks()) do
applyTrackSpeed(tr)
end
end
local hookedAnimators = setmetatable({}, {__mode = "k"})
local function hookAnimator(character)
if not character then return end
task.spawn(function()
local human = character:FindFirstChildOfClass("Humanoid") or character:WaitForChild("Humanoid", 10)
if not human or character ~= lp.Character then return end
local animator = human:FindFirstChildOfClass("Animator") or human:WaitForChild("Animator", 5)
if not animator then
animator = Instance.new("Animator")
animator.Parent = human
end
if hookedAnimators[animator] then return end
hookedAnimators[animator] = true
track(animator.AnimationPlayed:Connect(function(animationTrack)
task.defer(function()
if animationTrack and animationTrack.IsPlaying then applyTrackSpeed(animationTrack) end
end)
end))
for _, tr in ipairs(animator:GetPlayingAnimationTracks()) do
applyTrackSpeed(tr)
end
end)
end
track(lp.CharacterAdded:Connect(hookAnimator))
if lp.Character then hookAnimator(lp.Character) end
track(rs.Heartbeat:Connect(applyCurrentCharacterSpeeds))
local function equipMap(character, slotMap, persist)
if not character then return 0 end
local animate = character:FindFirstChild("Animate") or character:WaitForChild("Animate", 5)
local human = character:FindFirstChildOfClass("Humanoid")
if not animate or not human then return 0 end
if human.RigType == Enum.HumanoidRigType.R6 then
if toast then toast("R6 detected. Resetting to default.", "bad") end
pcall(function() human:ApplyDescription(human:GetAppliedDescription()) end)
return 0
end
for _, tr in ipairs(human:GetPlayingAnimationTracks()) do
pcall(function() tr:AdjustWeight(0, 0); tr:Stop(0) end)
end
animate.Disabled = true
local count = 0
for slot, anims in pairs(slotMap) do
local folderName = m[slot]
local folder = folderName and animate:FindFirstChild(folderName)
if folder and #anims > 0 then
for _, o in ipairs(folder:GetChildren()) do
if o:IsA("Animation") then o:Destroy() end
end
local saved = {}
for _, a in ipairs(anims) do
local na = Instance.new("Animation")
na.Name = a.Name
na.AnimationId = a.AnimationId
na.Parent = folder
saved[#saved + 1] = {Name = a.Name, AnimationId = a.AnimationId}
end
if persist then equippedAnims[slot] = saved end
count = count + 1
end
end
if persist then saveJSON(EQUIPPED_FILE, equippedAnims) end
task.wait(0.05)
animate.Disabled = false
return count
end
local function applyBundleItemsToCharacter(items)
local char = lp.Character
if not char then return 0 end
local slotMap, toLoad = {}, {}
for _, slot in ipairs(buttonOrder) do
local payload = items[slot]
if payload then
local list = getTracksFor(payload, slot)
if #list > 0 then
slotMap[slot] = list
for _, a in ipairs(list) do toLoad[#toLoad + 1] = a end
end
end
end
preloadAnimations(toLoad)
return equipMap(char, slotMap, true)
end
local capturing = {}
local function captureBundleData(id)
local key = tostring(id)
if capturing[key] then return end
capturing[key] = true
task.spawn(function()
local ok = pcall(function()
local details = getDetails(id)
if not (details and details.Items) then return end
local items = {}
for _, it in ipairs(details.Items) do
items[#items + 1] = {Id = it.Id, Name = it.Name, AssetType = it.AssetType, Type = it.Type}
end
local anims, seen = {}, {}
for slot, payload in pairs(mapBundleItems(details.Items)) do
local pid = tostring(payload.Id)
if not seen[pid] then
seen[pid] = true
get(payload.Id, id, payload.AssetType)
end
local list = fileCache[pid]
if type(list) == "table" and #list > 0 then
local copy = {}
for _, a in ipairs(list) do copy[#copy + 1] = {Name = a.Name, AnimationId = a.AnimationId} end
anims[slot] = {Id = payload.Id, List = copy}
end
end
local function store(e)
if type(e) == "table" then e.Items, e.Anims = items, anims end
end
store(savedBookmarks[key])
for _, h in ipairs(historyList) do
if tostring(h.Id) == key then store(h) end
end
saveJSON(SAVED_BUNDLES_FILE, savedBookmarks)
saveJSON(HISTORY_FILE, historyList)
end)
capturing[key] = nil
end)
end
local function recordHistory(id, name)
if not id then return end
local key = tostring(id)
local oldItems, oldAnims
for i = #historyList, 1, -1 do
if tostring(historyList[i].Id) == key then
oldItems, oldAnims = historyList[i].Items, historyList[i].Anims
table.remove(historyList, i)
end
end
local sv = savedBookmarks[key]
if not oldItems and type(sv) == "table" then oldItems, oldAnims = sv.Items, sv.Anims end
local mt = metaCache[key] or {}
table.insert(historyList, 1, {
Id = id, Name = name or "Unknown", Time = os.time(), Kind = mt.kind, Amount = mt.amount, Creator = mt.creator,
Items = oldItems, Anims = oldAnims,
})
while #historyList > HISTORY_MAX do table.remove(historyList) end
saveJSON(HISTORY_FILE, historyList)
if not oldAnims then captureBundleData(id) end
if onHistoryChanged then onHistoryChanged() end
end
local function wearBundle(bundleId, bundleName, onDone)
task.spawn(function()
local details = getDetails(bundleId)
if details and lp.Character then
local n = applyBundleItemsToCharacter(mapBundleItems(details.Items))
if n > 0 then
recordHistory(bundleId, bundleName)
lastPackData = {Id = bundleId, Name = bundleName}
saveJSON(LAST_PACK_FILE, lastPackData)
end
if toast then toast(n > 0 and ("Equipped " .. n .. " animations") or "Nothing could be equipped", n > 0 and "ok" or "bad") end
elseif toast then
toast("Couldn't load that bundle", "bad")
end
if onDone then onDone() end
end)
end
local function initAutoEquip(char)
if not char then return end
task.spawn(function()
local human = char:WaitForChild("Humanoid", 10)
local animate = char:WaitForChild("Animate", 10)
if not human or not animate then return end
if human.RigType == Enum.HumanoidRigType.R6 then
if toast then toast("R6 detected. Custom animations are not supported.", "bad") end
return
end
local animator = human:WaitForChild("Animator", 5)
if not animator then
animator = Instance.new("Animator")
animator.Parent = human
end
local idleFolder = animate:FindFirstChild("idle")
if idleFolder then
local idleAnimation = idleFolder:FindFirstChild("Animation1") or idleFolder:FindFirstChildWhichIsA("Animation")
if not idleAnimation then
idleFolder:WaitForChild("Animation1", 5)
end
end
task.wait(0.75)
if char ~= lp.Character or human.Health <= 0 then return end
if next(equippedAnims) ~= nil then
equipMap(char, equippedAnims, false)
end
end)
end
if lp.Character then initAutoEquip(lp.Character) end
track(lp.CharacterAdded:Connect(initAutoEquip))
local badgeRefs, fetchingMeta = {}, {}
local function priceFromInfo(info)
local price = tonumber(info.PriceInRobux)
if price == 0 then return "free", 0 end
if price then return "paid", price end
if info.IsForSale == false then return "off", nil end
if info.IsForSale == true and info.IsLimited ~= true and info.IsLimitedUnique ~= true then return "free", 0 end
return nil, nil
end
local function seedMeta(entry)
if type(entry) ~= "table" or not entry.Id then return nil end
local key = tostring(entry.Id)
local mt = metaCache[key] or {}
if mt.kind == nil then
local rawStatus = entry.PriceStatus or entry.ItemStatus or ""
local status = string.lower(tostring(rawStatus))
local price = tonumber(entry.Price)
if price == nil then price = tonumber(entry.LowestPrice) end
if status:find("free") or price == 0 then
mt.kind, mt.amount = "free", 0
elseif status:find("off") or status:find("reseller") then
mt.kind = "off"
elseif price then
mt.kind, mt.amount = "paid", price
end
end
if entry.CreatorName and not mt.creator then mt.creator = tostring(entry.CreatorName) end
metaCache[key] = mt
return mt
end
local function ensureMeta(id, needCreator)
local key = tostring(id)
local mt = metaCache[key]
if mt and (mt.fetched or (mt.kind and (not needCreator or mt.creator))) then return end
if fetchingMeta[key] then return end
fetchingMeta[key] = true
task.spawn(function()
local ok, info = pcall(function() return mps:GetProductInfo(tonumber(id), Enum.InfoType.Bundle) end)
fetchingMeta[key] = nil
local mm = metaCache[key] or {}
mm.fetched = true
if ok and type(info) == "table" then
if mm.kind == nil or mm.kind == "unknown" then
local k, a = priceFromInfo(info)
mm.kind, mm.amount = k or "unknown", a
end
if not mm.creator and type(info.Creator) == "table" and info.Creator.Name then
mm.creator = tostring(info.Creator.Name)
end
elseif mm.kind == nil then
mm.kind = "unknown"
end
metaCache[key] = mm
if onMetaChanged then onMetaChanged(key) end
end)
end
local function applyBadge(label, meta)
local kind = meta and meta.kind
local text, bg, fg
if kind == "paid" then text, bg, fg = "R$ " .. commas(meta.amount), C.green, Color3.fromRGB(20, 20, 22)
elseif kind == "free" then text, bg, fg = "FREE", C.accent2, Color3.fromRGB(20, 20, 22)
elseif kind == "off" then text, bg, fg = "OFFSALE", C.surfaceHi, C.textDim
elseif kind == "unknown" then text, bg, fg = "N/A", C.surfaceHi, C.textDim
else text, bg, fg = "…", C.surfaceHi, C.textMuted end
label.Text = text
tw(label, 0.2, {BackgroundColor3 = bg, TextColor3 = fg})
end
local function bindBadge(label, id)
local key = tostring(id)
badgeRefs[key] = badgeRefs[key] or {}
table.insert(badgeRefs[key], label)
applyBadge(label, metaCache[key])
end
local function mkBadge(parent, props)
props = props or {}
props.BackgroundColor3 = C.surfaceHi
props.AutomaticSize = Enum.AutomaticSize.X
props.Size = UDim2.fromOffset(0, s(22))
props.Font = Enum.Font.GothamBlack
props.TextSize = s(13)
props.TextColor3 = C.textMuted
props.Text = "…"
props.BorderSizePixel = 0
local b = new("TextLabel", props, parent)
round(b); pad(b, s(10), s(10))
return b
end
local KEY_ALIASES = {
cat = "category", cats = "category",
min = "price_min", max = "price_max",
pricemin = "price_min", pricemax = "price_max",
minprice = "price_min", maxprice = "price_max",
}
local VALID_KEYS = {sort = true, price_min = true, price_max = true, price = true, creator = true, sale = true, category = true, sales = true, period = true, limit = true, is = true}
local SORT_ALIASES = {
mostfavorited = "MostFavorited", pricelowtohigh = "PriceLowToHigh", pricehightolow = "PriceHighToLow", recentlycreated = "RecentlyCreated",
rel = "Relevance", relevance = "Relevance", relevant = "Relevance",
new = "RecentlyCreated", newest = "RecentlyCreated", recent = "RecentlyCreated", latest = "RecentlyCreated",
fav = "MostFavorited", favorite = "MostFavorited", favorited = "MostFavorited", popular = "MostFavorited", top = "MostFavorited",
best = "Bestselling", bestselling = "Bestselling", sales = "Bestselling", seller = "Bestselling",
low = "PriceLowToHigh", asc = "PriceLowToHigh", cheap = "PriceLowToHigh", cheapest = "PriceLowToHigh",
high = "PriceHighToLow", desc = "PriceHighToLow", expensive = "PriceHighToLow",
}
local SAVED_SORT = {
new = "new", newest = "new", recent = "new", latest = "new", old = "old", oldest = "old",
name = "name", az = "name", alpha = "name", recentlycreated = "new", pricelowtohigh = "low", pricehightolow = "high", mostfavorited = "fav",
low = "low", asc = "low", cheap = "low", cheapest = "low", high = "high", desc = "high", expensive = "high",
fav = "fav", favorite = "fav", favorites = "fav",
}
local CAT_ALIASES = {
none = "None", all = "None", featured = "Featured", collectibles = "Collectibles", collectible = "Collectibles",
community = "CommunityCreations", communitycreations = "CommunityCreations", ugc = "CommunityCreations", premium = "Premium", recommended = "Recommended",
}
local SALES_ALIASES = {all = "All", collectibles = "Collectibles", collectible = "Collectibles", premium = "Premium"}
local PERIOD_ALIASES = {
["12h"] = "Past12Hours", day = "PastDay", ["1d"] = "PastDay", ["24h"] = "PastDay", ["3d"] = "Past3Days",
week = "PastWeek", ["7d"] = "PastWeek", month = "PastMonth", ["30d"] = "PastMonth", all = "AllTime", alltime = "AllTime",
}
local LIMITS = {10, 28, 30, 60, 120}
local function parseQuery(text)
text = text or ""
local tags, words = {}, {}
local function addTag(k, v)
k = string.lower(k)
k = KEY_ALIASES[k] or k
if not VALID_KEYS[k] then return false end
for i = #tags, 1, -1 do
if tags[i].key == k then table.remove(tags, i) end
end
tags[#tags + 1] = {key = k, val = v}
return true
end
local rest = text:gsub('([%a_]+):"([^"]*)"', function(k, v)
if addTag(k, v) then return " " end
return nil
end)
for word in rest:gmatch("%S+") do
local k, v = word:match("^([%a_]+):(.+)$")
if k and addTag(k, v) then
else
words[#words + 1] = word
end
end
return {keyword = table.concat(words, " "), tags = tags}
end
local function buildQuery(q)
local parts = {}
if q.keyword ~= "" then parts[#parts + 1] = q.keyword end
for _, tg in ipairs(q.tags) do
local v = tg.val
if v:find("%s") then v = '"' .. v .. '"' end
parts[#parts + 1] = tg.key .. ":" .. v
end
return table.concat(parts, " ")
end
local function interpretTag(tab, key, val)
key = KEY_ALIASES[key] or key
local v = (string.lower(val):gsub("_", ""))
local saved = (tab ~= "Discover")
if tab == "Presets" then
if key == "sort" then
local x = ({new = "new", newest = "new", recent = "new", old = "old", oldest = "old", name = "name", az = "name", alpha = "name"})[v]
return x ~= nil, x
end
return false
end
if key == "sort" then
local x = (saved and SAVED_SORT or SORT_ALIASES)[v]
return x ~= nil, x
elseif key == "price_min" or key == "price_max" then
local n = tonumber(v)
if n and n >= 0 then return true, math.floor(n) end
return false
elseif key == "price" then
if v == "free" then return true, {min = 0, max = 0} end
local a, b = v:match("^(%d+)%-(%d+)$")
if a then
a, b = tonumber(a), tonumber(b)
if a > b then a, b = b, a end
return true, {min = a, max = b}
end
local n = v:match("^(%d+)$")
if n then n = tonumber(n); return true, {min = n, max = n} end
return false
elseif key == "creator" then
return val ~= "", val
elseif key == "is" then
if tab == "Saved" and (v == "fav" or v == "favorite" or v == "favorites" or v == "starred") then return true, "fav" end
if saved and (v == "free" or v == "paid" or v == "offsale" or v == "off") then return true, (v == "off" and "offsale" or v) end
return false
elseif saved then
return false
elseif key == "sale" then
if v == "on" or v == "only" then return true, "on" end
if v == "all" or v == "any" then return true, "all" end
return false
elseif key == "category" then
local x = CAT_ALIASES[v]; return x ~= nil, x
elseif key == "sales" then
local x = SALES_ALIASES[v]; return x ~= nil, x
elseif key == "period" then
local x = PERIOD_ALIASES[v]; return x ~= nil, x
elseif key == "limit" then
local n = tonumber(v)
if not n then return false end
local best = LIMITS[1]
for _, l in ipairs(LIMITS) do
if math.abs(l - n) < math.abs(best - n) then best = l end
end
return true, best
end
return false
end
local function compileQuery(tab, q)
local spec = {keyword = q.keyword, issues = {}}
for _, tg in ipairs(q.tags) do
local ok, v = interpretTag(tab, tg.key, tg.val)
if not ok then
spec.issues[#spec.issues + 1] = tg.key .. ":" .. tg.val
elseif tg.key == "sort" then spec.sort = v
elseif tg.key == "price_min" then spec.min = v
elseif tg.key == "price_max" then spec.max = v
elseif tg.key == "price" then spec.min, spec.max = v.min, v.max
elseif tg.key == "creator" then spec.creator = v
elseif tg.key == "sale" then spec.saleOnly = (v == "on")
elseif tg.key == "category" then spec.cat = v
elseif tg.key == "sales" then spec.sales = v
elseif tg.key == "period" then spec.period = v
elseif tg.key == "limit" then spec.limit = v
elseif tg.key == "is" then spec.is = v
end
end
return spec
end
local function buildParams(spec)
local p = CatalogSearchParams.new()
setParam(p, "SearchKeyword", spec.keyword or "")
setParam(p, "BundleTypes", {Enum.BundleType.Animations})
local priceFiltered = spec.min ~= nil or spec.max ~= nil
setParam(p, "IncludeOffSale", (not spec.saleOnly) and not priceFiltered)
setParam(p, "Limit", spec.limit or 120)
local freeOnly = spec.max ~= nil and spec.max == 0
local sortName = spec.sort or (freeOnly and "PriceLowToHigh") or (((spec.keyword or "") == "") and "RecentlyCreated" or "Relevance")
local e = enumItem("CatalogSortType", sortName)
if e then setParam(p, "SortType", e) end
if spec.min and spec.min > 0 then setParam(p, "MinPrice", spec.min) end
if spec.max and spec.max > 0 then setParam(p, "MaxPrice", spec.max) end
if spec.creator then setParam(p, "CreatorName", spec.creator) end
if spec.cat then
local ce = enumItem("CatalogCategoryFilter", spec.cat)
if ce then setParam(p, "CategoryFilter", ce) end
end
local se = enumItem("SalesTypeFilter", spec.sales or "All")
if se then setParam(p, "SalesTypeFilter", se) end
if spec.period then
local pe = enumItem("CatalogSortAggregation", spec.period)
if pe then setParam(p, "SortAggregation", pe) end
end
return p
end
local priceTried = {}
local function resolveUnknownPrices(items)
local queue = {}
for _, entry in ipairs(items) do
if type(entry) == "table" and entry.Id then
local key = tostring(entry.Id)
local mt = metaCache[key]
if not mt and entry.Kind then
mt = {kind = entry.Kind, amount = entry.Amount, creator = entry.Creator}
metaCache[key] = mt
end
if not (mt and mt.kind) or mt.kind == "unknown" then mt = seedMeta(entry) end
if (not (mt and mt.kind) or mt.kind == "unknown") and not priceTried[key] then
queue[#queue + 1] = key
end
end
end
if #queue == 0 then return end
local nextI, running = 1, 0
local function worker()
while true do
local key = queue[nextI]
if not key then break end
nextI += 1
local info
for attempt = 1, 2 do
local ok, res = pcall(function() return mps:GetProductInfo(tonumber(key), Enum.InfoType.Bundle) end)
if ok and type(res) == "table" then info = res; break end
task.wait(0.4)
end
if info then
priceTried[key] = true
local mm = metaCache[key] or {}
local k, a = priceFromInfo(info)
mm.kind, mm.amount = k or "unknown", a
if not mm.creator and type(info.Creator) == "table" and info.Creator.Name then
mm.creator = tostring(info.Creator.Name)
end
metaCache[key] = mm
end
end
running -= 1
end
for _ = 1, math.min(12, #queue) do
running += 1
task.spawn(worker)
end
local t0 = os.clock()
while running > 0 and os.clock() - t0 < 15 do task.wait(0.05) end
end
local function passesClientFilter(entry, spec)
if type(entry) ~= "table" or not entry.Id then return false end
local mt = seedMeta(entry)
local wantPrice = spec and (spec.min ~= nil or spec.max ~= nil)
if wantPrice then
local kind = mt and mt.kind
local amt
if kind == "free" then amt = 0
elseif kind == "paid" then amt = mt.amount end
if amt == nil then return false end
if spec.min and amt < spec.min then return false end
if spec.max and amt > spec.max then return false end
end
return true
end
local function savedAmount(v)
local mt = metaCache[tostring(v.Id)]
local kind = (mt and mt.kind) or v.Kind
local amount = (mt and mt.amount) or v.Amount
if kind == "free" then return 0 end
if kind == "paid" then return amount end
return nil
end
local function extraFilter(v, spec)
if spec.is == "fav" and not v.Fav then return false end
if spec.is == "free" or spec.is == "paid" or spec.is == "offsale" then
local mt = metaCache[tostring(v.Id)]
local kind = (mt and mt.kind) or v.Kind
local want = spec.is == "offsale" and "off" or spec.is
if kind ~= want then return false end
end
return true
end
local function buildSavedList(spec)
local list = {}
local kw = string.lower(spec.keyword or "")
local cr = spec.creator and string.lower(spec.creator) or nil
for key, v in pairs(savedBookmarks) do
if type(v) == "table" and v.Id then
if not metaCache[key] and v.Kind then
metaCache[key] = {kind = v.Kind, amount = v.Amount, creator = v.Creator}
end
local ok = true
if kw ~= "" and not string.find(string.lower(v.Name or ""), kw, 1, true) then ok = false end
if ok and cr then
local mt = metaCache[key]
local c = string.lower((mt and mt.creator) or v.Creator or "")
if not string.find(c, cr, 1, true) then ok = false end
end
if ok and not extraFilter(v, spec) then ok = false end
if ok and (spec.min ~= nil or spec.max ~= nil) then
local a = savedAmount(v)
if a == nil then ok = false end
if ok and spec.min and a < spec.min then ok = false end
if ok and spec.max and a > spec.max then ok = false end
end
if ok then list[#list + 1] = v end
end
end
local function cmpTime(a, b)
local ta, tb = a.Time or 0, b.Time or 0
if ta ~= tb then return ta > tb end
return tostring(a.Id) < tostring(b.Id)
end
local mode = spec.sort or "fav"
table.sort(list, function(a, b)
if mode == "new" then return cmpTime(a, b)
elseif mode == "old" then
local ta, tb = a.Time or 0, b.Time or 0
if ta ~= tb then return ta < tb end
return tostring(a.Id) < tostring(b.Id)
elseif mode == "name" then
local na, nb = string.lower(a.Name or ""), string.lower(b.Name or "")
if na ~= nb then return na < nb end
return cmpTime(a, b)
elseif mode == "low" or mode == "high" then
local x, y = savedAmount(a), savedAmount(b)
if x == y then return cmpTime(a, b) end
if x == nil then return false end
if y == nil then return true end
if mode == "low" then return x < y end
return x > y
end
local fa, fb = a.Fav and true or false, b.Fav and true or false
if fa ~= fb then return fa end
return cmpTime(a, b)
end)
return list
end
local function buildHistoryList(spec)
local list = {}
local kw = string.lower(spec.keyword or "")
local cr = spec.creator and string.lower(spec.creator) or nil
for _, v in ipairs(historyList) do
local key = tostring(v.Id)
if not metaCache[key] and v.Kind then
metaCache[key] = {kind = v.Kind, amount = v.Amount, creator = v.Creator}
end
local ok = true
if kw ~= "" and not string.find(string.lower(v.Name or ""), kw, 1, true) then ok = false end
if ok and cr then
local mt = metaCache[key]
local c = string.lower((mt and mt.creator) or v.Creator or "")
if not string.find(c, cr, 1, true) then ok = false end
end
if ok and (spec.min ~= nil or spec.max ~= nil) then
local a = savedAmount(v)
if a == nil then ok = false end
if ok and spec.min and a < spec.min then ok = false end
if ok and spec.max and a > spec.max then ok = false end
end
if ok and not extraFilter(v, spec) then ok = false end
if ok then list[#list + 1] = v end
end
if spec.sort == "old" then
local rev = {}
for i = #list, 1, -1 do rev[#rev + 1] = list[i] end
list = rev
elseif spec.sort == "name" then
table.sort(list, function(a, b) return string.lower(a.Name or "") < string.lower(b.Name or "") end)
elseif spec.sort == "low" or spec.sort == "high" then
local asc = spec.sort == "low"
table.sort(list, function(a, b)
local pa, pb = savedAmount(a), savedAmount(b)
if pa == nil and pb == nil then return false end
if pa == nil then return false end
if pb == nil then return true end
if pa == pb then return false end
if asc then return pa < pb else return pa > pb end
end)
end
return list
end
local uiOk, uiErr = pcall(function()
local PAD, TOPH = s(12), s(50)
local W, H = s(W0), s(H0)
local BODY_H = H - TOPH - PAD
local LW, GAP = s(420), s(12)
local RW = W - PAD * 2 - LW - GAP
local CHIP_Y, CHIP_H, TAG_Y, TAG_H, GRID_Y, FOOT_H = s(42), s(28), s(76), s(44), s(126), s(34)
local GRID_H = BODY_H - GRID_Y - FOOT_H - s(4)
local COLS, ROWS = 3, 1
local PER_PAGE = 3
local CPAD = s(8)
local CW = math.floor((LW - CPAD * (COLS + 1)) / COLS)
local GRID_AVAIL_H = GRID_H - CPAD * (ROWS + 1)
local CH = math.min(math.floor(GRID_AVAIL_H / ROWS), s(200))
local AH = s(28)
local playerGui = lp:WaitForChild("PlayerGui", 10) or lp:FindFirstChildOfClass("PlayerGui")
local parentCandidates = {}
if playerGui then parentCandidates[#parentCandidates + 1] = playerGui end
pcall(function()
if typeof(gethui) == "function" then
local h = gethui()
if h then parentCandidates[#parentCandidates + 1] = h end
end
end)
pcall(function() parentCandidates[#parentCandidates + 1] = game:GetService("CoreGui") end)
pcall(function()
if playerGui and playerGui:FindFirstChild("StudioAnimStudio") then playerGui.StudioAnimStudio:Destroy() end
local cg = game:GetService("CoreGui")
if cg and cg:FindFirstChild("StudioAnimStudio") then cg.StudioAnimStudio:Destroy() end
end)
local g = Instance.new("ScreenGui")
g.Name = "StudioAnimStudio"
g.ResetOnSpawn = false
g.IgnoreGuiInset = true
g.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
g.DisplayOrder = 999
local parented = false
for _, p in ipairs(parentCandidates) do
local okP = pcall(function() g.Parent = p end)
if okP and g.Parent == p then parented = true; break end
end
if not parented then
pcall(function() g.Parent = lp:WaitForChild("PlayerGui") end)
end
local mf = new("Frame", {
Name = "Main",
Size = UDim2.fromOffset(W, H),
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
BackgroundColor3 = C.bg,
BackgroundTransparency = 0.15,
BorderSizePixel = 0,
ClipsDescendants = true,
Active = true,
Visible = true,
}, g)
corner(mf, s(16))
stroke(mf, C.stroke, 1, 0.35)
local topLine = new("Frame", {Size = UDim2.new(1, 0, 0, s(2)), BackgroundColor3 = C.accent2, BackgroundTransparency = 0.3, BorderSizePixel = 0, ZIndex = 3}, mf)
local top = new("Frame", {Name = "Top", Size = UDim2.new(1, 0, 0, TOPH), BackgroundTransparency = 1, Active = true}, mf)
local logo = new("Frame", {Size = UDim2.fromOffset(s(30), s(30)), Position = UDim2.new(0, PAD, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), BackgroundColor3 = C.surfaceHi, BackgroundTransparency = 0.1, BorderSizePixel = 0}, top)
corner(logo, s(9))
new("TextLabel", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Text = "A", Font = Enum.Font.GothamBlack, TextSize = s(18), TextColor3 = C.text}, logo)
new("TextLabel", {Position = UDim2.fromOffset(PAD + s(42), s(8)), Size = UDim2.fromOffset(s(170), s(20)), BackgroundTransparency = 1, Text = "Animation Studio", Font = Enum.Font.GothamBlack, TextSize = s(18), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left}, top)
new("TextLabel", {Position = UDim2.fromOffset(PAD + s(42), s(28)), Size = UDim2.fromOffset(s(170), s(16)), BackgroundTransparency = 1, Text = "FE UGC bundles", Font = Enum.Font.Gotham, TextSize = s(13), TextColor3 = C.textMuted, TextXAlignment = Enum.TextXAlignment.Left}, top)
local seg = new("Frame", {Position = UDim2.fromOffset(PAD + s(212), math.floor((TOPH - s(34)) / 2)), Size = UDim2.fromOffset(s(300), s(34)), BackgroundColor3 = C.surface, BackgroundTransparency = 0.2, BorderSizePixel = 0}, top)
corner(seg, s(10))
local tabIndicator = new("Frame", {Position = UDim2.new(0, s(2), 0, s(2)), Size = UDim2.new(1 / 4, -s(3), 1, -s(4)), BackgroundColor3 = C.surfaceHi, BackgroundTransparency = 0.05, BorderSizePixel = 0, ZIndex = 1}, seg)
corner(tabIndicator, s(8))
local tabDiscover = mkBtn(seg, {Text = "Discover", Size = UDim2.fromScale(1 / 4, 1), ZIndex = 2, TextSize = s(13)}, C.surface, 1, C.text)
local tabSaved = mkBtn(seg, {Text = "Saved", Size = UDim2.fromScale(1 / 4, 1), Position = UDim2.fromScale(1 / 4, 0), ZIndex = 2, TextSize = s(13)}, C.surface, 1, C.textMuted)
local tabRecent = mkBtn(seg, {Text = "Recent", Size = UDim2.fromScale(1 / 4, 1), Position = UDim2.fromScale(2 / 4, 0), ZIndex = 2, TextSize = s(13)}, C.surface, 1, C.textMuted)
local tabPresets = mkBtn(seg, {Text = "Presets", Size = UDim2.fromScale(1 / 4, 1), Position = UDim2.fromScale(3 / 4, 0), ZIndex = 2, TextSize = s(13)}, C.surface, 1, C.textMuted)
local closeBtn = mkBtn(top, {Text = "×", Size = UDim2.fromOffset(s(30), s(30)), Position = UDim2.new(1, -PAD - s(30), 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), TextSize = s(20)}, C.surface, 0.2, C.textDim)
corner(closeBtn, s(8))
local minBtn = mkBtn(top, {Text = "–", Size = UDim2.fromOffset(s(30), s(30)), Position = UDim2.new(1, -PAD - s(66), 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), TextSize = s(18)}, C.surface, 0.2, C.textDim)
corner(minBtn, s(8))
local left = new("Frame", {Position = UDim2.fromOffset(PAD, TOPH), Size = UDim2.fromOffset(LW, BODY_H), BackgroundTransparency = 1}, mf)
local helpW, searchW, gap4 = s(38), s(86), s(6)
local sbW = LW - helpW - searchW - gap4 * 2
local sbBox = new("Frame", {Size = UDim2.fromOffset(sbW, s(38)), BackgroundColor3 = C.surface, BackgroundTransparency = 0.25, BorderSizePixel = 0}, left)
corner(sbBox, s(10))
stroke(sbBox, C.stroke, 1, 0.55)
local sb = new("TextBox", {
Size = UDim2.new(1, -s(44), 1, 0), Position = UDim2.fromOffset(s(14), 0), BackgroundTransparency = 1, Text = "",
PlaceholderText = "Search… try sort:price_high_to_low price_max:500",
ClearTextOnFocus = false, Font = Enum.Font.Gotham, TextSize = s(15),
TextColor3 = C.text, PlaceholderColor3 = C.textMuted, TextXAlignment = Enum.TextXAlignment.Left, ClipsDescendants = true,
}, sbBox)
local clearBtn = mkBtn(sbBox, {Text = "×", Size = UDim2.fromOffset(s(26), s(26)), Position = UDim2.new(1, -s(30), 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), TextSize = s(17), Visible = false}, C.surfaceHi, 0.35, C.textDim)
round(clearBtn)
local searchBtn = mkBtn(left, {Text = "Search", Size = UDim2.fromOffset(searchW, s(38)), Position = UDim2.fromOffset(sbW + gap4, 0), TextSize = s(15)}, C.surfaceHi, 0.15, C.text)
corner(searchBtn, s(10))
local helpBtn = mkBtn(left, {Text = "?", Size = UDim2.fromOffset(helpW, s(38)), Position = UDim2.fromOffset(sbW + gap4 * 2 + searchW, 0), TextSize = s(16)}, C.surface, 0.25, C.textDim)
corner(helpBtn, s(10))
local settingsBtn = mkBtn(top, {Text = "⚙", Size = UDim2.fromOffset(s(30), s(30)), Position = UDim2.new(1, -PAD - s(102), 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), TextSize = s(18)}, C.surface, 0.2, C.textDim)
corner(settingsBtn, s(8))
local function mkRow(y, h)
local row = new("ScrollingFrame", {
Position = UDim2.fromOffset(0, y), Size = UDim2.fromOffset(LW, h), BackgroundTransparency = 1, BorderSizePixel = 0,
ScrollBarThickness = 0, CanvasSize = UDim2.new(0, 0, 0, 0), AutomaticCanvasSize = Enum.AutomaticSize.X,
ScrollingDirection = Enum.ScrollingDirection.X, ZIndex = 2,
}, left)
new("UIListLayout", {FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, s(6)), SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Center}, row)
return row
end
local chipsRow = mkRow(CHIP_Y, CHIP_H)
local tagsRow = mkRow(TAG_Y, TAG_H)
local tagHint = new("TextLabel", {
Name = "TagHint",
Position = UDim2.fromOffset(0, TAG_Y),
Size = UDim2.fromOffset(LW, TAG_H),
BackgroundTransparency = 1,
BorderSizePixel = 0,
Text = "No filters · press ? to see all search tags",
Font = Enum.Font.GothamBold,
TextScaled = true,
TextColor3 = C.textDim,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
TextWrapped = true,
ZIndex = 6,
}, left)
new("UITextSizeConstraint", {MaxTextSize = s(28), MinTextSize = s(14)}, tagHint)
local gridHolder = new("Frame", {Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundColor3 = C.panel, BackgroundTransparency = 0.55, BorderSizePixel = 0, ClipsDescendants = true}, left)
corner(gridHolder, s(12)); stroke(gridHolder, C.stroke, 1, 0.7)
new("UIGridLayout", {
CellSize = UDim2.fromOffset(CW, CH),
CellPadding = UDim2.fromOffset(CPAD, CPAD),
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirectionMaxCells = COLS,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
}, gridHolder)
local gridMsg = new("TextLabel", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Text = "", Font = Enum.Font.GothamSemibold, TextSize = s(15), TextColor3 = C.textMuted, Visible = false, ZIndex = 2}, left)
gridMsg.Position = UDim2.fromOffset(0, GRID_Y)
gridMsg.Size = UDim2.fromOffset(LW, GRID_H)
local helpPanel = new("Frame", {Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundColor3 = C.overlay, BackgroundTransparency = 1, BorderSizePixel = 0, Visible = false, ClipsDescendants = true, ZIndex = 20}, left)
corner(helpPanel, s(12))
local helpScroll = new("ScrollingFrame", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = s(3), ScrollBarImageColor3 = C.surfaceHi, CanvasSize = UDim2.new(0, 0, 0, 0), AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 21}, helpPanel)
local HELP_TEXT = table.concat({
'<b>Search tags</b> <font color="#9AA1B8">type, press Enter</font>',
'',
'<font color="#B0B0B2">sort:</font> relevance newest mostfavorited bestselling price_low_to_high price_high_to_low',
'<font color="#B0B0B2">price_min:</font> 50 <font color="#B0B0B2">price_max:</font> 500 <font color="#B0B0B2">price:</font> 100-500 or free',
'<font color="#B0B0B2">creator:</font> Roblox or creator:"Group Name"',
'<font color="#B0B0B2">sale:</font> on (hide off-sale) all',
'<font color="#B0B0B2">category:</font> featured collectibles community premium recommended',
'<font color="#B0B0B2">sales:</font> collectibles premium all',
'<font color="#B0B0B2">period:</font> day week month alltime',
'<font color="#B0B0B2">limit:</font> 10 28 30 60 120',
'',
'<i>Underscores work as spaces in tag values:</i> sort:price_high_to_low',
'',
'<b>Saved tab</b>',
'<font color="#B0B0B2">sort:</font> newest oldest name price_low_to_high price_high_to_low favorites',
'<font color="#B0B0B2">is:</font> favorite free paid offsale',
'<font color="#B0B0B2">Recent tab:</font> same filters except is:favorite',
'',
'<b>Presets tab</b>',
'<font color="#B0B0B2">sort:</font> newest oldest name (search filters by name)',
'',
'<font color="#B0B0B2">Tab</font> autocomplete the highlighted suggestion',
'Example: <i>zombie sort:price_high_to_low price_max:300</i>',
}, "\n")
local helpText = new("TextLabel", {
Position = UDim2.fromOffset(s(14), s(12)), Size = UDim2.new(1, -s(28), 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1,
RichText = true, Text = HELP_TEXT, TextWrapped = true, Font = Enum.Font.Gotham, TextSize = s(15), TextColor3 = C.text, TextTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, ZIndex = 22,
}, helpScroll)
local settingsPanel = new("Frame", {Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundColor3 = C.overlay, BackgroundTransparency = 1, BorderSizePixel = 0, Visible = false, ClipsDescendants = true, ZIndex = 20}, left)
corner(settingsPanel, s(12))
stroke(settingsPanel, C.stroke, 1, 0.2)
local setScroll = new("ScrollingFrame", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = s(3), ScrollBarImageColor3 = C.surfaceHi, CanvasSize = UDim2.new(0, 0, 0, 0), AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 21}, settingsPanel)
pad(setScroll, s(14), s(14), s(10), s(10))
local function addSettingLabel(text, y)
local lbl = new("TextLabel", {Position = UDim2.fromOffset(0, y), Size = UDim2.new(1, 0, 0, s(20)), BackgroundTransparency = 1, Text = text, Font = Enum.Font.GothamBold, TextSize = s(15), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 22}, setScroll)
return lbl
end
local function createSlider(parent, label, minVal, maxVal, currentVal, yPos, callback)
local row = new("Frame", {Position = UDim2.fromOffset(0, yPos), Size = UDim2.new(1, 0, 0, s(40)), BackgroundTransparency = 1, ZIndex = 22}, parent)
if label ~= "" then
new("TextLabel", {Size = UDim2.new(1, 0, 0, s(20)), BackgroundTransparency = 1, Text = label, Font = Enum.Font.Gotham, TextSize = s(14), TextColor3 = C.textDim, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 22}, row)
end
local bg = new("Frame", {Position = UDim2.fromOffset(0, label ~= "" and s(22) or s(5)), Size = UDim2.new(0.7, 0, 0, s(10)), BackgroundColor3 = C.surfaceHi, ZIndex = 22}, row)
round(bg)
local pct = (currentVal - minVal) / (maxVal - minVal)
local fill = new("Frame", {Size = UDim2.new(pct, 0, 1, 0), BackgroundColor3 = C.accent2, ZIndex = 23}, bg)
round(fill)
local knob = new("Frame", {Size = UDim2.fromOffset(s(16), s(16)), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(pct, 0, 0.5, 0), BackgroundColor3 = WHITE, ZIndex = 24}, bg)
round(knob)
local valLbl = new("TextLabel", {Position = UDim2.new(0.72, 0, 0, label ~= "" and s(17) or s(0)), Size = UDim2.new(0.28, 0, 0, s(20)), BackgroundTransparency = 1, Text = string.format("x%.2f", currentVal), Font = Enum.Font.GothamBold, TextSize = s(13), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 22}, row)
local dragging = false
knob.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then dragging = true end
end)
track(uis.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then dragging = false end
end))
track(uis.InputChanged:Connect(function(input)
if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
local posX = math.clamp(input.Position.X - bg.AbsolutePosition.X, 0, bg.AbsoluteSize.X)
local p = posX / bg.AbsoluteSize.X
local val = minVal + p * (maxVal - minVal)
fill.Size = UDim2.new(p, 0, 1, 0)
knob.Position = UDim2.new(p, 0, 0.5, 0)
valLbl.Text = string.format("x%.2f", val)
callback(val)
end
end))
return row
end
addSettingLabel("Restore Last Pack on Launch", s(10))
local restoreToggle = mkBtn(setScroll, {Text = scriptSettings.restoreLastPack and "ON" or "OFF", Size = UDim2.fromOffset(s(50), s(25)), Position = UDim2.fromOffset(0, s(40)), TextSize = s(14), ZIndex = 22}, scriptSettings.restoreLastPack and C.accent2 or C.surface, 0, scriptSettings.restoreLastPack and Color3.new(0,0,0) or C.textDim)
corner(restoreToggle, s(6))
restoreToggle.MouseButton1Click:Connect(function()
scriptSettings.restoreLastPack = not scriptSettings.restoreLastPack
restoreToggle.Text = scriptSettings.restoreLastPack and "ON" or "OFF"
setBtn(restoreToggle, scriptSettings.restoreLastPack and C.accent2 or C.surface, 0, scriptSettings.restoreLastPack and Color3.new(0,0,0) or C.textDim)
saveSettings()
end)
addSettingLabel("Force Animation (Ignore Tools)", s(85))
local forceToggle = mkBtn(setScroll, {Text = scriptSettings.forceAnimation and "ON" or "OFF", Size = UDim2.fromOffset(s(50), s(25)), Position = UDim2.fromOffset(0, s(115)), TextSize = s(14), ZIndex = 22}, scriptSettings.forceAnimation and C.accent2 or C.surface, 0, scriptSettings.forceAnimation and Color3.new(0,0,0) or C.textDim)
corner(forceToggle, s(6))
forceToggle.MouseButton1Click:Connect(function()
scriptSettings.forceAnimation = not scriptSettings.forceAnimation
forceToggle.Text = scriptSettings.forceAnimation and "ON" or "OFF"
setBtn(forceToggle, scriptSettings.forceAnimation and C.accent2 or C.surface, 0, scriptSettings.forceAnimation and Color3.new(0,0,0) or C.textDim)
saveSettings()
end)
addSettingLabel("Global Animation Speed", s(160))
createSlider(setScroll, "", 0.25, 5, scriptSettings.globalSpeed, s(180), function(val)
scriptSettings.globalSpeed = val
saveSettings()
applyCurrentCharacterSpeeds()
end)
addSettingLabel("Advance Speed Animation", s(230))
local advToggle = mkBtn(setScroll, {Text = scriptSettings.advanceSpeed and "ON" or "OFF", Size = UDim2.fromOffset(s(50), s(25)), Position = UDim2.fromOffset(0, s(255)), TextSize = s(14), ZIndex = 22}, scriptSettings.advanceSpeed and C.accent2 or C.surface, 0, scriptSettings.advanceSpeed and Color3.new(0,0,0) or C.textDim)
corner(advToggle, s(6))
local customSpeedRows = {}
local customSpeedsY = s(290)
local function createCustomSliders()
for _, row in ipairs(customSpeedRows) do row:Destroy() end
table.clear(customSpeedRows)
customSpeedsY = s(290)
if scriptSettings.advanceSpeed then
for _, slot in ipairs(buttonOrder) do
local currentVal = scriptSettings.customSpeeds[slot] or 1
local row = createSlider(setScroll, shortNames[slot] .. " Speed", 0.25, 5, currentVal, customSpeedsY, function(val)
scriptSettings.customSpeeds[slot] = val
saveSettings()
applyCurrentCharacterSpeeds()
end)
customSpeedRows[#customSpeedRows + 1] = row
customSpeedsY += s(45)
end
end
end
createCustomSliders()
advToggle.MouseButton1Click:Connect(function()
scriptSettings.advanceSpeed = not scriptSettings.advanceSpeed
advToggle.Text = scriptSettings.advanceSpeed and "ON" or "OFF"
setBtn(advToggle, scriptSettings.advanceSpeed and C.accent2 or C.surface, 0, scriptSettings.advanceSpeed and Color3.new(0,0,0) or C.textDim)
saveSettings()
createCustomSliders()
applyCurrentCharacterSpeeds()
end)
local footer = new("Frame", {Position = UDim2.fromOffset(0, BODY_H - FOOT_H), Size = UDim2.fromOffset(LW, FOOT_H), BackgroundTransparency = 1}, left)
local prevBtn = mkBtn(footer, {Text = "← Back", Size = UDim2.fromOffset(s(90), FOOT_H), TextSize = s(14)}, C.surfaceHi, 0.6, C.textMuted)
corner(prevBtn, s(8))
local nextBtn = mkBtn(footer, {Text = "Next →", Size = UDim2.fromOffset(s(90), FOOT_H), Position = UDim2.new(1, -s(90), 0, 0), TextSize = s(14)}, C.surfaceHi, 0.6, C.textMuted)
corner(nextBtn, s(8))
local pageLbl = new("TextLabel", {Position = UDim2.fromOffset(s(94), 0), Size = UDim2.new(1, -s(188), 1, 0), BackgroundTransparency = 1, Text = "Loading…", Font = Enum.Font.GothamSemibold, TextSize = s(14), TextColor3 = C.textDim, TextXAlignment = Enum.TextXAlignment.Center}, footer)
local rightP = new("Frame", {Position = UDim2.fromOffset(PAD + LW + GAP, TOPH), Size = UDim2.fromOffset(RW, BODY_H), BackgroundColor3 = C.panel, BackgroundTransparency = 0.5, BorderSizePixel = 0}, mf)
corner(rightP, s(14)); stroke(rightP, C.stroke, 1, 0.6)
local IP = s(10)
local innerW = RW - IP * 2
local VPH = s(140)
local masterViewport = new("ViewportFrame", {
Position = UDim2.fromOffset(IP, IP), Size = UDim2.fromOffset(innerW, VPH), BackgroundColor3 = C.overlay, BackgroundTransparency = 0.15, BorderSizePixel = 0,
Ambient = Color3.fromRGB(160, 160, 165), LightColor = WHITE, LightDirection = Vector3.new(-1, -1.5, -1), ClipsDescendants = true,
}, rightP)
corner(masterViewport, s(12)); stroke(masterViewport, C.stroke, 1, 0.5)
local hintLabel = new("TextLabel", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Text = "Select a bundle\nto preview it here", Font = Enum.Font.GothamSemibold, TextSize = s(15), TextColor3 = C.textMuted, ZIndex = 3}, masterViewport)
local previewShimmer = new("Frame", {Size = UDim2.fromScale(1, 1), BackgroundColor3 = C.surfaceHi, BackgroundTransparency = 0.5, BorderSizePixel = 0, Visible = false, ZIndex = 5}, masterViewport)
corner(previewShimmer, s(12)); applyShimmer(previewShimmer, C.overlay, C.surface)
local masterBadge = mkBadge(masterViewport, {Position = UDim2.fromOffset(s(10), s(10)), ZIndex = 6, Visible = false})
local statusChip = new("TextLabel", {
Position = UDim2.fromOffset(s(10), VPH - s(34)), Size = UDim2.fromOffset(0, s(24)), AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = C.overlay, BackgroundTransparency = 0.3, BorderSizePixel = 0, Text = "NO BUNDLE", Font = Enum.Font.GothamBold, TextSize = s(13), TextColor3 = C.textMuted, ZIndex = 6,
}, masterViewport)
round(statusChip); pad(statusChip, s(12), s(12))
local yInfo = IP + VPH + s(6)
local nameLabel = new("TextLabel", {Position = UDim2.fromOffset(IP, yInfo), Size = UDim2.fromOffset(innerW - s(42), s(22)), BackgroundTransparency = 1, Text = "No bundle selected", Font = Enum.Font.GothamBold, TextSize = s(17), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd}, rightP)
local creatorLabel = new("TextLabel", {Position = UDim2.fromOffset(IP, yInfo + s(24)), Size = UDim2.fromOffset(innerW - s(42), s(17)), BackgroundTransparency = 1, Text = "Pick one from the list", Font = Enum.Font.Gotham, TextSize = s(13), TextColor3 = C.textDim, TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd}, rightP)
local starBtn = mkBtn(rightP, {Text = "☆", Size = UDim2.fromOffset(s(38), s(38)), Position = UDim2.fromOffset(IP + innerW - s(38), yInfo), TextSize = s(22), Visible = false}, C.surface, 0.2, C.textDim)
corner(starBtn, s(9))
local ySlots = yInfo + s(44) + s(4)
local slotGrid = new("Frame", {Position = UDim2.fromOffset(IP, ySlots), Size = UDim2.fromOffset(innerW, s(60)), BackgroundTransparency = 1}, rightP)
local cellW = math.floor((innerW - s(6) * 3) / 4)
new("UIGridLayout", {CellSize = UDim2.fromOffset(cellW, s(28)), CellPadding = UDim2.fromOffset(s(6), s(6)), SortOrder = Enum.SortOrder.LayoutOrder}, slotGrid)
local slotButtons = {}
for i, slot in ipairs(buttonOrder) do
local b = mkBtn(slotGrid, {Text = shortNames[slot], LayoutOrder = i, TextSize = s(14)}, C.surface, 0.5, C.textMuted)
corner(b, s(8))
slotButtons[slot] = b
end
local yAct = ySlots + s(60) + s(8)
local halfW = math.floor((innerW - s(10)) / 2)
local wearSelBtn = mkBtn(rightP, {Text = "Wear Selected", Size = UDim2.fromOffset(halfW, s(34)), Position = UDim2.fromOffset(IP, yAct), TextSize = s(14)}, C.surfaceHi, 0.55, C.textMuted)
corner(wearSelBtn, s(9))
local wearAllBtn = mkBtn(rightP, {Text = "Wear All", Size = UDim2.fromOffset(halfW, s(34)), Position = UDim2.fromOffset(IP + halfW + s(10), yAct), TextSize = s(14)}, C.surfaceHi, 0.55, C.textMuted)
corner(wearAllBtn, s(9))
local resetBtn = mkBtn(rightP, {Text = "RESET TO DEFAULT", Size = UDim2.fromOffset(innerW, s(34)), Position = UDim2.fromOffset(IP, yAct + s(34) + s(8)), TextSize = s(14), Font = Enum.Font.GothamBold}, Color3.fromRGB(160, 80, 80), 0.1, WHITE)
corner(resetBtn, s(8))
local toastLbl = new("TextLabel", {
AnchorPoint = Vector2.new(0.5, 1), Position = UDim2.new(0.5, 0, 1, -s(18)), Size = UDim2.fromOffset(0, s(34)), AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = C.panel, BackgroundTransparency = 1, BorderSizePixel = 0, Text = "", Font = Enum.Font.GothamSemibold, TextSize = s(15),
TextColor3 = C.text, TextTransparency = 1, Visible = false, ZIndex = 100,
}, mf)
round(toastLbl); pad(toastLbl, s(18), s(18))
local toastStroke = stroke(toastLbl, C.accent2, 1, 1)
local pill = mkBtn(g, {Text = "UGC", Size = UDim2.fromOffset(s(58), s(58)), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(1, -s(50), 0, s(76)), TextSize = s(17), Font = Enum.Font.GothamBlack, Visible = false, Active = true}, C.surfaceHi, 0.1, C.text)
round(pill); stroke(pill, C.accent2, 1, 0.5)
local currentTab = "Discover"
local searchResults, savedTabList = {}, {}
local catalogCursor
local currentPageIndex = 1
local activeSpec
local activeBundle
local currentBundleItems = {}
local selectedSlot = "idleanimation"
local searchGen, masterGen, inspectGen = 0, 0, 0
local masterTrack, masterRig, masterAnimator
local cards, skeletons = {}, {}
local chipButtons, tagPills = {}, {}
local pagingBusy, wearBusy = false, false
local inspectBundle, playMaster, runSearch, refreshTags, drawGridPage, refreshSavedView
local toastGen = 0
toast = function(text, kind)
toastGen = toastGen + 1
local myGen = toastGen
local col = (kind == "bad" and C.red) or (kind == "ok" and C.accent2) or C.accent2
toastLbl.Text = text
toastStroke.Color = col
toastLbl.Visible = true
toastLbl.Position = UDim2.new(0.5, 0, 1, -s(2))
toastLbl.BackgroundTransparency, toastLbl.TextTransparency, toastStroke.Transparency = 1, 1, 1
tw(toastLbl, 0.28, {Position = UDim2.new(0.5, 0, 1, -s(18)), BackgroundTransparency = 0.15, TextTransparency = 0}, Enum.EasingStyle.Back)
tw(toastStroke, 0.28, {Transparency = 0.3})
task.delay(2.4, function()
if myGen ~= toastGen then return end
tw(toastLbl, 0.3, {BackgroundTransparency = 1, TextTransparency = 1})
tw(toastStroke, 0.3, {Transparency = 1})
task.delay(0.32, function()
if myGen == toastGen then toastLbl.Visible = false end
end)
end)
end
local spinning = {}
local spinAngle = 0
local spinConn
spinConn = rs.RenderStepped:Connect(function(dt)
if not g.Parent then spinConn:Disconnect(); return end
spinAngle = (spinAngle + math.rad(28) * dt) % (math.pi * 2)
local cf = CFrame.Angles(0, spinAngle, 0)
for model in pairs(spinning) do
if model.Parent then model:PivotTo(cf) else spinning[model] = nil end
end
end)
track(spinConn)
local function buildRig(vpFrame)
local wm = vpFrame:FindFirstChildOfClass("WorldModel")
if wm then wm:ClearAllChildren() else wm = new("WorldModel", {}, vpFrame) end
local char = lp.Character or lp.CharacterAdded:Wait()
local wasArch = char.Archivable
char.Archivable = true
local clone = char:Clone()
char.Archivable = wasArch
if not clone then return nil end
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("LuaSourceContainer") or d:IsA("Tool") or d:IsA("Sound") then d:Destroy() end
end
local root = clone:FindFirstChild("HumanoidRootPart")
local human = clone:FindFirstChildOfClass("Humanoid")
if not root or not human then clone:Destroy(); return nil end
root.Anchored = true
pcall(function()
human.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
human.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOff
end)
local animator = human:FindFirstChildOfClass("Animator") or new("Animator", {}, human)
clone.Parent = wm
clone:PivotTo(CFrame.new(0, 0, 0))
local cam = vpFrame.CurrentCamera
if not cam then
cam = new("Camera", {}, vpFrame)
vpFrame.CurrentCamera = cam
end
cam.FieldOfView = 50
cam.CFrame = CFrame.lookAt(Vector3.new(0, 0.6, 8), Vector3.new(0, -0.2, 0))
spinning[clone] = true
return clone, animator
end
local function tryPlayTrack(animator, animation, looped)
local ok, pt = pcall(function()
local tr = animator:LoadAnimation(animation)
tr.Looped = looped
tr:Play()
return tr
end)
if not ok or not pt then return false, nil end
task.wait(0.15)
if not pt.IsPlaying then
pcall(function() pt:Stop() end)
return false, nil
end
return true, pt
end
local function setPreviewState(state, slot, n)
local label = (slot and shortNames[slot]) and string.upper(shortNames[slot]) or ""
if state == "empty" then
statusChip.Text, statusChip.TextColor3 = "NO BUNDLE", C.textMuted
previewShimmer.Visible = false
hintLabel.Visible = true
elseif state == "loading" then
statusChip.Text, statusChip.TextColor3 = label .. " · LOADING", C.textDim
previewShimmer.Visible = true
elseif state == "retry" then
statusChip.Text, statusChip.TextColor3 = label .. " · RETRY " .. tostring(n or 1), C.orange
previewShimmer.Visible = true
elseif state == "playing" then
statusChip.Text, statusChip.TextColor3 = "• " .. label, C.accent2
previewShimmer.Visible = false
elseif state == "missing" then
statusChip.Text, statusChip.TextColor3 = "NO ANIMATIONS", C.orange
previewShimmer.Visible = false
else
statusChip.Text, statusChip.TextColor3 = label .. " · FAILED", C.red
previewShimmer.Visible = false
end
end
local function refreshSlotButtons()
for slot, b in pairs(slotButtons) do
local has = currentBundleItems[slot] ~= nil
if has and slot == selectedSlot then setBtn(b, C.surfaceHi, 0, C.text)
elseif has then setBtn(b, C.surface, 0.2, C.text)
else setBtn(b, C.surface, 0.5, C.textMuted) end
end
end
local function setActionsEnabled(on)
if on then
setBtn(wearSelBtn, C.surfaceHi, 0.15, C.text)
setBtn(wearAllBtn, C.surfaceHi, 0.15, C.text)
else
setBtn(wearSelBtn, C.surfaceHi, 0.55, C.textMuted)
setBtn(wearAllBtn, C.surfaceHi, 0.55, C.textMuted)
end
end
local function refreshActiveInfo()
if not activeBundle then
nameLabel.Text = "No bundle selected"
creatorLabel.Text = "Pick one from the list"
masterBadge.Visible = false
starBtn.Visible = false
return
end
if activeBundle.IsPreset then
nameLabel.Text = activeBundle.Name
creatorLabel.Text = "Preset · " .. tostring(activeBundle.Count or 0) .. ((activeBundle.Count or 0) == 1 and " slot" or " slots")
masterBadge.Visible = false
starBtn.Visible = false
return
end
local key = tostring(activeBundle.Id)
local meta = metaCache[key]
nameLabel.Text = activeBundle.Name
local parts = {}
if meta and meta.creator then parts[#parts + 1] = "by " .. meta.creator end
parts[#parts + 1] = "ID " .. key
creatorLabel.Text = table.concat(parts, " · ")
masterBadge.Visible = true
applyBadge(masterBadge, meta)
starBtn.Visible = true
local saved = savedBookmarks[key] ~= nil
starBtn.Text = saved and "★" or "☆"
starBtn.TextColor3 = saved and C.gold or C.textDim
end
onMetaChanged = function(key)
local refs = badgeRefs[key]
if refs then
for i = #refs, 1, -1 do
local lbl = refs[i]
if lbl.Parent then applyBadge(lbl, metaCache[key]) else table.remove(refs, i) end
end
end
if activeBundle and tostring(activeBundle.Id) == key then refreshActiveInfo() end
local bm = savedBookmarks[key]
local mt = metaCache[key]
if bm and mt then
bm.Kind, bm.Amount, bm.Creator = mt.kind, mt.amount, mt.creator
queueSave(SAVED_BUNDLES_FILE, savedBookmarks)
end
if mt then
local touched = false
for _, h in ipairs(historyList) do
if tostring(h.Id) == key then
h.Kind, h.Amount, h.Creator = mt.kind, mt.amount, mt.creator
touched = true
end
end
if touched then queueSave(HISTORY_FILE, historyList) end
end
end
local function updateSelection()
for _, c in ipairs(cards) do
if c.box.Parent then c.restore() end
end
end
local function runMasterPlayback(myGen, payload, slot, tracks)
local refetched = false
local idx, fails = 1, 0
while myGen == masterGen do
local tr = tracks[idx]
local ok, played = false, nil
if tr then ok, played = tryPlayTrack(masterAnimator, tr, #tracks == 1) end
if myGen ~= masterGen then
if played then pcall(function() played:Stop(0) end) end
return nil
end
if ok then
fails = 0
masterTrack = played
applySpeed(masterTrack, slot)
setPreviewState("playing", slot)
if #tracks == 1 then return true end
played.Stopped:Wait()
idx = idx % #tracks + 1
else
fails = fails + 1
if fails >= 2 * math.max(#tracks, 1) then
if refetched then return false end
refetched, fails = true, 0
clearAssetCache(payload.Id)
task.wait(0.4)
if myGen ~= masterGen then return nil end
tracks = getTracksFor(payload, slot)
idx = 1
if #tracks == 0 then return false end
else
idx = idx % math.max(#tracks, 1) + 1
task.wait(0.3)
end
end
end
return nil
end
playMaster = function(slot)
local payload = currentBundleItems[slot]
if not payload then return end
masterGen = masterGen + 1
local myGen = masterGen
if masterTrack then pcall(function() masterTrack:Stop(0.1) end); masterTrack = nil end
setPreviewState("loading", slot)
task.spawn(function()
if not masterAnimator or not masterRig or not masterRig.Parent then
masterRig, masterAnimator = buildRig(masterViewport)
end
if myGen ~= masterGen then return end
if not masterAnimator then setPreviewState("failed", slot); return end
local tracks = {}
for attempt = 0, 4 do
tracks = getTracksFor(payload, slot)
if #tracks > 0 then break end
if myGen ~= masterGen then return end
setPreviewState("retry", slot, attempt + 1)
clearAssetCache(payload.Id)
task.wait(math.min(0.5 + attempt * 0.3, 2.5))
if myGen ~= masterGen then return end
end
if myGen ~= masterGen then return end
if #tracks == 0 then setPreviewState("failed", slot); return end
local result = runMasterPlayback(myGen, payload, slot, tracks)
if result == false and myGen == masterGen then setPreviewState("failed", slot) end
end)
end
inspectBundle = function(entry)
activeBundle = {Id = entry.Id, Name = entry.Name or "Unknown"}
inspectGen = inspectGen + 1
local myInspect = inspectGen
masterGen = masterGen + 1
if masterTrack then pcall(function() masterTrack:Stop(0.1) end); masterTrack = nil end
table.clear(currentBundleItems)
hintLabel.Visible = false
seedMeta(entry)
ensureMeta(entry.Id, true)
refreshActiveInfo()
refreshSlotButtons()
setActionsEnabled(false)
setPreviewState("loading", selectedSlot)
updateSelection()
task.spawn(function()
local details = getDetails(entry.Id)
if myInspect ~= inspectGen then return end
if not details then setPreviewState("failed", selectedSlot); return end
for k, v in pairs(mapBundleItems(details.Items)) do currentBundleItems[k] = v end
local slot = currentBundleItems[selectedSlot] and selectedSlot or nil
if not slot then
for _, name in ipairs(buttonOrder) do
if currentBundleItems[name] then slot = name; break end
end
end
if not slot then
refreshSlotButtons()
setPreviewState("missing", selectedSlot)
return
end
selectedSlot = slot
refreshSlotButtons()
setActionsEnabled(true)
masterRig, masterAnimator = nil, nil
playMaster(slot)
end)
end
for _, slot in ipairs(buttonOrder) do
slotButtons[slot].MouseButton1Click:Connect(function()
if not activeBundle or not currentBundleItems[slot] then return end
selectedSlot = slot
refreshSlotButtons()
playMaster(slot)
end)
end
wearSelBtn.MouseButton1Click:Connect(function()
if not activeBundle or wearBusy then return end
local payload = currentBundleItems[selectedSlot]
if not payload then return end
if not lp.Character then toast("No character found", "bad"); return end
wearBusy = true
wearSelBtn.Text = "Equipping…"
local slot = selectedSlot
task.spawn(function()
local list = getTracksFor(payload, slot)
if #list > 0 then
preloadAnimations(list)
local n = equipMap(lp.Character, {[slot] = list}, true)
if n > 0 and activeBundle and not activeBundle.IsPreset then
recordHistory(activeBundle.Id, activeBundle.Name)
lastPackData = {Id = activeBundle.Id, Name = activeBundle.Name}
saveJSON(LAST_PACK_FILE, lastPackData)
end
toast(n > 0 and ("Equipped " .. shortNames[slot]) or "Couldn't equip that slot", n > 0 and "ok" or "bad")
else
toast("Animation failed to load", "bad")
end
wearSelBtn.Text = "Wear Selected"
wearBusy = false
end)
end)
wearAllBtn.MouseButton1Click:Connect(function()
if not activeBundle or wearBusy or next(currentBundleItems) == nil then return end
if not lp.Character then toast("No character found", "bad"); return end
wearBusy = true
wearAllBtn.Text = "Equipping…"
task.spawn(function()
local n = applyBundleItemsToCharacter(currentBundleItems)
if n > 0 and activeBundle and not activeBundle.IsPreset then
recordHistory(activeBundle.Id, activeBundle.Name)
lastPackData = {Id = activeBundle.Id, Name = activeBundle.Name}
saveJSON(LAST_PACK_FILE, lastPackData)
end
toast(n > 0 and ("Equipped " .. n .. " animations") or "Nothing could be equipped", n > 0 and "ok" or "bad")
wearAllBtn.Text = "Wear All"
wearBusy = false
end)
end)
local resetBusy = false
resetBtn.MouseButton1Click:Connect(function()
if resetBusy then return end
resetBusy = true
resetBtn.Text = "RESETTING..."
task.spawn(function()
local function finishReset()
if resetBtn and resetBtn.Parent then resetBtn.Text = "RESET TO DEFAULT" end
resetBusy = false
end
for k in pairs(equippedAnims) do equippedAnims[k] = nil end
saveJSON(EQUIPPED_FILE, equippedAnims)
lastPackData = {}
saveJSON(LAST_PACK_FILE, lastPackData)
local char = lp.Character
local human = char and char:FindFirstChildOfClass("Humanoid")
if not char or not human then
toast("No character found", "bad")
finishReset()
return
end
local okDesc, desc = pcall(function()
return plrs:GetHumanoidDescriptionFromUserId(lp.UserId)
end)
if not okDesc or not desc then
okDesc, desc = pcall(function() return human:GetAppliedDescription() end)
end
if not okDesc or not desc then
toast("Couldn't retrieve default avatar animations", "bad")
finishReset()
return
end
pcall(function() human:ApplyDescription(desc) end)
local animate = char:FindFirstChild("Animate") or char:WaitForChild("Animate", 5)
if not animate then
pcall(function() desc:Destroy() end)
toast("Animate script not found", "bad")
finishReset()
return
end
for _, tr in ipairs(human:GetPlayingAnimationTracks()) do
pcall(function() tr:Stop(0) end)
end
pcall(function() animate.Disabled = true end)
local defaults = {
idle = {"507766666", "507766951"},
walk = {"507777826"},
run = {"507767714"},
jump = {"507765000"},
fall = {"507767968"},
climb = {"507765644"},
swim = {"507784897"},
swimidle = {"507785072"},
}
local descriptionProperties = {
idle = "IdleAnimation", walk = "WalkAnimation", run = "RunAnimation",
jump = "JumpAnimation", fall = "FallAnimation", climb = "ClimbAnimation",
swim = "SwimAnimation", swimidle = "SwimAnimation",
}
local restored = 0
for folderName, fallbackIds in pairs(defaults) do
local folder = animate:FindFirstChild(folderName)
if folder then
local ids = fallbackIds
local property = descriptionProperties[folderName]
if property and desc then
local okId, rawId = pcall(function() return desc[property] end)
local assetId = okId and tonumber(rawId) or nil
if assetId and assetId > 0 then ids = {tostring(math.floor(assetId))} end
end
local animations = {}
for _, child in ipairs(folder:GetChildren()) do
if child:IsA("Animation") then table.insert(animations, child) end
end
if #animations == 0 then
local animation = Instance.new("Animation")
animation.Name = folderName == "idle" and "Animation1" or (folderName == "walk" and "WalkAnim" or folderName .. "Anim")
animation.Parent = folder
animations[1] = animation
end
for index, animation in ipairs(animations) do
local id = ids[math.min(index, #ids)]
animation.AnimationId = "rbxassetid://" .. id
if folderName == "idle" and index == 1 then animation.Name = "Animation1" end
if folderName == "idle" and index == 2 then animation.Name = "Animation2" end
end
restored += 1
end
end
task.wait(0.1)
pcall(function() animate.Disabled = false end)
pcall(function() desc:Destroy() end)
if restored > 0 then
toast("Animations reset to default", "ok")
else
toast("Could not restore default animation IDs", "bad")
end
finishReset()
end)
end)
local function toggleBookmark(id, name)
local key = tostring(id)
local nowSaved
if savedBookmarks[key] then
savedBookmarks[key] = nil
nowSaved = false
else
local mt = metaCache[key] or {}
savedBookmarks[key] = {Id = id, Name = name, Fav = false, Time = tick(), Kind = mt.kind, Amount = mt.amount, Creator = mt.creator}
nowSaved = true
ensureMeta(id, true)
captureBundleData(id)
end
saveJSON(SAVED_BUNDLES_FILE, savedBookmarks)
for _, c in ipairs(cards) do
if c.key == key then c.refreshSave() end
end
if activeBundle and tostring(activeBundle.Id) == key then refreshActiveInfo() end
if currentTab == "Saved" and not nowSaved and refreshSavedView then refreshSavedView() end
toast(nowSaved and "Saved to your list" or "Removed from saved", nowSaved and "ok" or nil)
return nowSaved
end
starBtn.MouseButton1Click:Connect(function()
if activeBundle then toggleBookmark(activeBundle.Id, activeBundle.Name) end
end)
local function clearSkeletons()
for _, f in ipairs(skeletons) do stopShimmer(f); f:Destroy() end
table.clear(skeletons)
end
local function clearGrid()
for _, c in ipairs(cards) do
if c.skeleton then stopShimmer(c.skeleton) end
c.box:Destroy()
end
table.clear(cards)
end
local function showSkeletons()
clearGrid()
clearSkeletons()
gridMsg.Visible = false
for i = 1, PER_PAGE do
local f = new("Frame", {BackgroundColor3 = C.surface, BackgroundTransparency = 0.35, BorderSizePixel = 0, Size = UDim2.fromOffset(CW, CH), LayoutOrder = i}, gridHolder)
corner(f, s(12))
applyShimmer(f, C.panel, C.surfaceHi)
skeletons[#skeletons + 1] = f
end
end
local function showGridMessage(msg)
clearGrid()
clearSkeletons()
gridMsg.Text = msg
gridMsg.Visible = true
end
local function updatePager(total)
local pages = math.max(1, math.ceil(total / PER_PAGE))
local more = (currentTab == "Discover" and catalogCursor ~= nil and not catalogCursor.IsFinished) and true or false
pageLbl.Text = "Page " .. currentPageIndex .. " / " .. pages .. (more and "+" or "") .. " · " .. total .. (more and "+" or "") .. " results"
local canPrev = currentPageIndex > 1
local canNext = currentPageIndex < pages or more
setBtn(prevBtn, C.surfaceHi, canPrev and 0.3 or 0.7, canPrev and C.text or C.textMuted)
setBtn(nextBtn, C.surfaceHi, canNext and 0.3 or 0.7, canNext and C.text or C.textMuted)
end
local cardOrder = 0
local function buildCard(entry, idx)
local id = entry.Id
local key = tostring(id)
seedMeta(entry)
ensureMeta(id, false)
cardOrder += 1
local box = new("ViewportFrame", {
Name = "Card", BackgroundColor3 = C.panel, BackgroundTransparency = 0.25, BorderSizePixel = 0, ClipsDescendants = true,
Size = UDim2.fromOffset(CW, CH),
LayoutOrder = cardOrder,
Ambient = Color3.fromRGB(160, 160, 165), LightColor = WHITE, LightDirection = Vector3.new(-1, -1.5, -1),
}, gridHolder)
corner(box, s(12))
local boxStroke = stroke(box, C.stroke, 1, 0.5)
local boxScale = new("UIScale", {Scale = 0.86}, box)
tw(boxScale, 0.3 + idx * 0.05, {Scale = 1}, Enum.EasingStyle.Back)
local skeleton = new("Frame", {Size = UDim2.fromScale(1, 1), BackgroundColor3 = C.surface, BackgroundTransparency = 0.15, BorderSizePixel = 0, ZIndex = 2}, box)
corner(skeleton, s(12)); applyShimmer(skeleton, C.panel, C.surfaceHi)
local cpad = s(6)
new("TextLabel", {
Position = UDim2.fromOffset(cpad + s(2), CH - AH - cpad - s(22)), Size = UDim2.fromOffset(CW - cpad * 2 - s(4), s(20)), BackgroundTransparency = 1,
Text = entry.Name or "Unknown", Font = Enum.Font.GothamBold, TextSize = s(15), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 4,
}, box)
local badge = mkBadge(box, {Position = UDim2.fromOffset(cpad, cpad), ZIndex = 5})
bindBadge(badge, id)
local hit = new("TextButton", {Size = UDim2.new(1, 0, 1, -(AH + s(12))), BackgroundTransparency = 1, Text = "", ZIndex = 5}, box)
local cardActions = new("Frame", {Position = UDim2.fromOffset(cpad, CH - AH - cpad), Size = UDim2.fromOffset(CW - cpad * 2, AH), BackgroundTransparency = 1, ZIndex = 6}, box)
local saveBtn = mkBtn(cardActions, {Text = "+ Save", Size = UDim2.new(0.5, -s(3), 1, 0), TextSize = s(12), ZIndex = 6}, C.surfaceHi, 0.25, C.textDim)
corner(saveBtn, s(6))
local wearBtn = mkBtn(cardActions, {Text = "▶ Wear", Size = UDim2.new(0.5, -s(3), 1, 0), Position = UDim2.new(0.5, s(3), 0, 0), TextSize = s(12), ZIndex = 6}, C.surfaceHi, 0.15, C.text)
corner(wearBtn, s(6))
local function refreshSave()
local saved = savedBookmarks[key] ~= nil
saveBtn.Text = saved and "✓ Saved" or "+ Save"
saveBtn.TextColor3 = saved and C.gold or C.textDim
end
refreshSave()
if currentTab == "Saved" then
local favBtn = mkBtn(box, {Text = (entry.Fav and "★" or "☆"), Size = UDim2.fromOffset(s(28), s(28)), Position = UDim2.new(1, -s(32), 0, s(4)), TextSize = s(18), ZIndex = 7}, C.overlay, 0.4, entry.Fav and C.gold or C.textDim)
round(favBtn)
favBtn.MouseButton1Click:Connect(function()
local bm = savedBookmarks[key]
if bm then
bm.Fav = not bm.Fav
saveJSON(SAVED_BUNDLES_FILE, savedBookmarks)
favBtn.Text = bm.Fav and "★" or "☆"
favBtn.TextColor3 = bm.Fav and C.gold or C.textDim
end
end)
end
local record = {key = key, box = box, skeleton = skeleton, refreshSave = refreshSave}
record.restore = function()
local selected = activeBundle ~= nil and tostring(activeBundle.Id) == key
boxStroke.Color = selected and C.accent2 or C.stroke
boxStroke.Thickness = selected and 2 or 1
boxStroke.Transparency = selected and 0 or 0.5
end
cards[#cards + 1] = record
record.restore()
hit.MouseEnter:Connect(function()
tw(box, 0.12, {BackgroundColor3 = C.surface})
boxStroke.Color = C.accent2
boxStroke.Transparency = 0
end)
hit.MouseLeave:Connect(function()
tw(box, 0.15, {BackgroundColor3 = C.panel})
record.restore()
end)
hit.MouseButton1Click:Connect(function() inspectBundle(entry) end)
saveBtn.MouseButton1Click:Connect(function() toggleBookmark(id, entry.Name) end)
wearBtn.MouseButton1Click:Connect(function()
wearBtn.Text = "…"
wearBundle(id, entry.Name, function()
if wearBtn.Parent then wearBtn.Text = "▶ Wear" end
end)
end)
task.spawn(function()
task.wait(0.14 * idx)
if not box.Parent then return end
local function dropSkeleton()
if skeleton.Parent then
tw(skeleton, 0.25, {BackgroundTransparency = 1})
task.delay(0.3, function()
stopShimmer(skeleton)
skeleton:Destroy()
end)
end
end
local details = getDetails(id)
if not box.Parent then return end
if not details then dropSkeleton(); return end
local map = mapBundleItems(details.Items)
local usable = {}
for _, slot in ipairs(CARD_ORDER) do
if map[slot] and #usable < 4 then usable[#usable + 1] = {slot = slot, payload = map[slot]} end
end
if #usable == 0 then dropSkeleton(); return end
local rig, animator = buildRig(box)
if not rig or not box.Parent then dropSkeleton(); return end
local i, current = 1, nil
while box.Parent do
local u = usable[i]
local list = getTracksFor(u.payload, u.slot)
if #list > 0 then
if current then pcall(function() current:Stop(0.1) end); current = nil end
local ok, played = tryPlayTrack(animator, list[1], #list == 1)
if ok then
current = played
applySpeed(current, u.slot)
dropSkeleton()
end
end
task.wait(2.6)
i = i % #usable + 1
end
if current then pcall(function() current:Stop(0) end) end
end)
end
drawGridPage = function(dataList)
clearGrid()
clearSkeletons()
gridMsg.Visible = false
if not dataList or #dataList == 0 then
showGridMessage(currentTab == "Saved" and "No saved bundles match" or (currentTab == "Recent" and "No recently used bundles yet" or "No results"))
updatePager(0)
return
end
local start = (currentPageIndex - 1) * PER_PAGE + 1
local ending = math.min(currentPageIndex * PER_PAGE, #dataList)
if start > #dataList then
currentPageIndex = 1
start = 1
ending = math.min(PER_PAGE, #dataList)
end
local idx = 1
for i = start, ending do
local entry = dataList[i]
if not entry then break end
buildCard(entry, idx)
idx += 1
end
updatePager(#dataList)
end
refreshTags = function(q, spec)
for _, p in ipairs(tagPills) do p:Destroy() end
table.clear(tagPills)
local issueSet = {}
for _, issue in ipairs(spec.issues or {}) do issueSet[issue] = true end
local any = false
for _, tg in ipairs(q.tags) do
any = true
local txt = tg.key .. ":" .. tg.val
local bad = issueSet[txt] == true
local pillBtn = mkBtn(tagsRow, {
Text = txt, AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, TAG_H),
TextSize = s(14), LayoutOrder = #tagPills + 1,
}, bad and C.badBg or C.pillBg, 0.25, bad and C.badTx or C.pillTx)
corner(pillBtn, math.floor(TAG_H / 2))
pad(pillBtn, s(15), s(15))
table.insert(tagPills, pillBtn)
pillBtn.MouseButton1Click:Connect(function()
local newTags = {}
for _, t in ipairs(q.tags) do
if not (t.key == tg.key and t.val == tg.val) then newTags[#newTags + 1] = t end
end
local parts = {}
if q.keyword ~= "" then parts[#parts + 1] = q.keyword end
for _, t in ipairs(newTags) do
local v = t.val
if v:find("%s") then v = '"' .. v .. '"' end
parts[#parts + 1] = t.key .. ":" .. v
end
local newText = table.concat(parts, " ")
sb.Text = newText
if runSearch then runSearch(newText) end
end)
end
tagHint.Visible = not any
for tabName, set in pairs(chipButtons) do
for tag, b in pairs(set) do
local shown = (tabName == currentTab)
b.Visible = shown
if shown then
local active = false
for _, t in ipairs(q.tags) do
if (t.key .. ":" .. t.val) == tag then active = true; break end
end
if active then setBtn(b, C.surfaceHi, 0.1, C.text)
else setBtn(b, C.surface, 0.35, C.textDim) end
end
end
end
end
local CHIP_SETS = {
Discover = {
{label = "Free", tag = "price:free"},
{label = "Newest", tag = "sort:newest"},
{label = "Popular", tag = "sort:mostfavorited"},
{label = "Cheapest", tag = "sort:pricelowtohigh"},
{label = "Featured", tag = "category:featured"},
{label = "Sale", tag = "sale:on"},
{label = "Best sellers", tag = "sort:bestselling"},
{label = "Priciest", tag = "sort:pricehightolow"},
{label = "Under 100", tag = "price:0-100"},
{label = "Under 500", tag = "price:0-500"},
{label = "100–500", tag = "price:100-500"},
{label = "Premium", tag = "category:premium"},
{label = "Collectibles", tag = "category:collectibles"},
{label = "Community", tag = "category:community"},
{label = "Recommended", tag = "category:recommended"},
{label = "This week", tag = "period:week"},
{label = "This month", tag = "period:month"},
},
Saved = {
{label = "Favorites", tag = "is:favorite"},
{label = "Newest", tag = "sort:newest"},
{label = "Oldest", tag = "sort:oldest"},
{label = "A–Z", tag = "sort:name"},
{label = "Free", tag = "is:free"},
{label = "Cheapest", tag = "sort:pricelowtohigh"},
{label = "Offsale", tag = "is:offsale"},
{label = "Paid", tag = "is:paid"},
{label = "Priciest", tag = "sort:pricehightolow"},
},
Presets = {
{label = "Newest", tag = "sort:newest"},
{label = "Oldest", tag = "sort:oldest"},
{label = "A–Z", tag = "sort:name"},
},
Recent = {
{label = "Newest", tag = "sort:newest"},
{label = "Oldest", tag = "sort:oldest"},
{label = "A–Z", tag = "sort:name"},
{label = "Free", tag = "is:free"},
{label = "Paid", tag = "is:paid"},
{label = "Cheapest", tag = "sort:pricelowtohigh"},
{label = "Priciest", tag = "sort:pricehightolow"},
{label = "Offsale", tag = "is:offsale"},
},
}
for tabName, set in pairs(CHIP_SETS) do
chipButtons[tabName] = {}
for i, c in ipairs(set) do
local b = mkBtn(chipsRow, {
Text = c.label, AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, CHIP_H),
TextSize = s(13), LayoutOrder = i, Visible = (tabName == "Discover"),
}, C.surface, 0.35, C.textDim)
corner(b, math.floor(CHIP_H / 2))
pad(b, s(14), s(14))
chipButtons[tabName][c.tag] = b
b.MouseButton1Click:Connect(function()
local q = parseQuery(sb.Text)
local found = false
for j = #q.tags, 1, -1 do
local tg = q.tags[j]
if (tg.key .. ":" .. tg.val) == c.tag then table.remove(q.tags, j); found = true; break end
end
if not found then
local k, v = c.tag:match("^(%a+):(.+)$")
if k then
for j = #q.tags, 1, -1 do
if q.tags[j].key == k then table.remove(q.tags, j) end
end
q.tags[#q.tags + 1] = {key = k, val = v}
end
end
local newText = buildQuery(q)
sb.Text = newText
if runSearch then runSearch(newText) end
end)
end
end
local applySuggestionRef
local TAG_VALUES = {
sort = {"relevance", "newest", "mostfavorited", "bestselling", "price_low_to_high", "price_high_to_low"},
price_min = {"50", "100", "500", "1000"},
price_max = {"100", "300", "500", "1000"},
price = {"free", "0-100", "100-500"},
creator = {"Roblox"},
sale = {"on", "all"},
category = {"featured", "collectibles", "community", "premium", "recommended"},
sales = {"collectibles", "premium", "all"},
period = {"day", "week", "month", "alltime"},
limit = {"10", "28", "30", "60", "120"},
is = {"favorite", "free", "paid", "offsale"},
}
local SORT_SAVED_VALUES = {"newest", "oldest", "name", "price_low_to_high", "price_high_to_low", "favorites"}
local SORT_RECENT_VALUES = {"newest", "oldest", "name"}
local KEYS_DISCOVER = {"sort", "price", "price_min", "price_max", "creator", "sale", "category", "sales", "period", "limit"}
local KEYS_SAVED = {"sort", "price", "price_min", "price_max", "creator", "is"}
local KEYS_PRESETS = {"sort"}
local KEYS_RECENT = {"sort", "price", "price_min", "price_max", "creator", "is"}
local suggestStrip = new("Frame", {
Name = "Suggest", Position = UDim2.fromOffset(PAD, TOPH - s(34)), Size = UDim2.fromOffset(LW, s(30)),
BackgroundColor3 = C.overlay, BackgroundTransparency = 0.08, BorderSizePixel = 0, Visible = false, ZIndex = 60,
}, mf)
corner(suggestStrip, s(10)); stroke(suggestStrip, C.stroke, 1, 0.4)
local suggestRow = new("ScrollingFrame", {
Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 0,
CanvasSize = UDim2.new(0, 0, 0, 0), AutomaticCanvasSize = Enum.AutomaticSize.X,
ScrollingDirection = Enum.ScrollingDirection.X, ZIndex = 61,
}, suggestStrip)
new("UIListLayout", {FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, s(5)), SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Center}, suggestRow)
pad(suggestRow, s(5), s(5), 0, 0)
local currentSuggestions = {}
local function getSuggestions()
local text = sb.Text
local token = text:match("(%S*)$") or ""
local before = text:sub(1, #text - #token)
local lower = token:lower()
local out = {}
local key, valPrefix = lower:match("^([%a_]+):(.*)$")
local origKey = key
if key then key = KEY_ALIASES[key] or key end
if key and VALID_KEYS[key] then
local vals
if key == "sort" then
vals = (currentTab == "Discover" and TAG_VALUES.sort) or (currentTab == "Saved" and SORT_SAVED_VALUES) or SORT_RECENT_VALUES
elseif key == "is" and currentTab == "Recent" then
vals = {"free", "paid", "offsale"}
else
vals = TAG_VALUES[key]
end
local normPrefix = ((valPrefix or ""):gsub("_", ""))
for _, v in ipairs(vals or {}) do
local normVal = (v:lower():gsub("_", ""))
if normVal:find(normPrefix, 1, true) == 1 and normVal ~= normPrefix then
out[#out + 1] = {label = origKey .. ":" .. v, apply = before .. origKey .. ":" .. v .. " ", run = true}
end
end
return out
end
local used = {}
for _, tg in ipairs(parseQuery(text).tags) do used[tg.key] = true end
local keys = (currentTab == "Discover" and KEYS_DISCOVER) or (currentTab == "Saved" and KEYS_SAVED) or (currentTab == "Presets" and KEYS_PRESETS) or KEYS_RECENT
for _, k in ipairs(keys) do
local ak = KEY_ALIASES[k] or k
if not used[ak] and (lower == "" or k:find(lower, 1, true) == 1) and (k .. ":") ~= lower then
out[#out + 1] = {label = k .. ":", apply = before .. k .. ":", run = false}
end
end
if #lower >= 2 then
local seen, count = {}, 0
local function consider(name)
if count >= 4 or type(name) ~= "string" then return end
local ln = name:lower()
if seen[ln] or ln == lower or not ln:find(lower, 1, true) then return end
seen[ln] = true
count += 1
out[#out + 1] = {label = name, apply = before .. name .. " ", run = true}
end
if currentTab == "Presets" then
for _, v in ipairs(presetList) do consider(v.Name) end
else
for _, v in ipairs(historyList) do consider(v.Name) end
for _, v in pairs(savedBookmarks) do if type(v) == "table" then consider(v.Name) end end
for _, v in ipairs(searchResults) do consider(v.Name) end
end
end
while #out > 8 do table.remove(out) end
return out
end
local function refreshSuggestions()
for _, c in ipairs(suggestRow:GetChildren()) do
if c:IsA("GuiObject") then c:Destroy() end
end
if sb.Text == "" then suggestStrip.Visible = false; return end
currentSuggestions = getSuggestions()
if #currentSuggestions == 0 then suggestStrip.Visible = false; return end
new("TextLabel", {
Text = "Tab ⇥", LayoutOrder = 0, AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, s(22)),
BackgroundTransparency = 1, Font = Enum.Font.GothamBold, TextSize = s(12), TextColor3 = C.textMuted, ZIndex = 62,
}, suggestRow)
for i, sg in ipairs(currentSuggestions) do
local b = mkBtn(suggestRow, {
Text = sg.label, AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, s(22)),
TextSize = s(13), LayoutOrder = i, ZIndex = 62,
}, i == 1 and C.surfaceHi or C.surface, i == 1 and 0 or 0.2, i == 1 and C.text or C.textDim)
corner(b, s(11)); pad(b, s(10), s(10))
b.MouseButton1Down:Connect(function() applySuggestionRef(sg) end)
end
suggestStrip.Visible = true
end
applySuggestionRef = function(sg)
sb.Text = sg.apply
if sg.run and runSearch then runSearch(sg.apply) end
refreshSuggestions()
task.delay(0.05, function()
pcall(function()
sb:CaptureFocus()
sb.CursorPosition = #sb.Text + 1
end)
end)
end
sb:GetPropertyChangedSignal("Text"):Connect(refreshSuggestions)
track(uis.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.Tab and suggestStrip.Visible and currentSuggestions[1] then
applySuggestionRef(currentSuggestions[1])
end
end))
local renderPresets, presetPanel, presetEmpty, saveCurrentBtn, hideConfirm, confirmOverlay
do
local ROW_H = s(54)
local focusNameId
local pendingDelete
presetPanel = new("ScrollingFrame", {
Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundColor3 = C.panel, BackgroundTransparency = 0.55,
BorderSizePixel = 0, ScrollBarThickness = s(3), ScrollBarImageColor3 = C.surfaceHi, CanvasSize = UDim2.new(0, 0, 0, 0),
AutomaticCanvasSize = Enum.AutomaticSize.Y, ScrollingDirection = Enum.ScrollingDirection.Y, Visible = false, ClipsDescendants = true, ZIndex = 2,
}, left)
corner(presetPanel, s(12)); stroke(presetPanel, C.stroke, 1, 0.7)
new("UIListLayout", {Padding = UDim.new(0, s(6)), SortOrder = Enum.SortOrder.LayoutOrder, HorizontalAlignment = Enum.HorizontalAlignment.Center}, presetPanel)
pad(presetPanel, s(8), s(8), s(8), s(8))
presetEmpty = new("TextLabel", {
Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundTransparency = 1, Text = "",
Font = Enum.Font.GothamSemibold, TextSize = s(15), TextColor3 = C.textMuted, TextWrapped = true, Visible = false, ZIndex = 4,
}, left)
saveCurrentBtn = mkBtn(chipsRow, {
Text = "+ Save current", AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, CHIP_H),
TextSize = s(13), LayoutOrder = 0, Visible = false,
}, C.surfaceHi, 0.15, C.text)
corner(saveCurrentBtn, math.floor(CHIP_H / 2)); pad(saveCurrentBtn, s(14), s(14))
confirmOverlay = new("TextButton", {
Size = UDim2.fromScale(1, 1), BackgroundColor3 = C.overlay, BackgroundTransparency = 0.35, BorderSizePixel = 0, Text = "",
AutoButtonColor = false, Visible = false, Active = true, ZIndex = 90,
}, mf)
local DW, DH = s(300), s(152)
local dlg = new("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(DW, DH),
BackgroundColor3 = C.panel, BorderSizePixel = 0, Active = true, ZIndex = 91,
}, confirmOverlay)
corner(dlg, s(14)); stroke(dlg, C.stroke, 1, 0.2)
local dlgScale = new("UIScale", {Scale = 1}, dlg)
new("TextLabel", {
Position = UDim2.fromOffset(s(16), s(14)), Size = UDim2.fromOffset(DW - s(32), s(22)), BackgroundTransparency = 1,
Text = "Delete preset?", Font = Enum.Font.GothamBlack, TextSize = s(18), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 92,
}, dlg)
local confirmMsg = new("TextLabel", {
Position = UDim2.fromOffset(s(16), s(42)), Size = UDim2.fromOffset(DW - s(32), s(50)), BackgroundTransparency = 1,
Text = "", Font = Enum.Font.Gotham, TextSize = s(14), TextColor3 = C.textDim, TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, ZIndex = 92,
}, dlg)
local btnW = math.floor((DW - s(32) - s(10)) / 2)
local cancelBtn = mkBtn(dlg, {Text = "Cancel", Size = UDim2.fromOffset(btnW, s(36)), Position = UDim2.fromOffset(s(16), DH - s(16) - s(36)), TextSize = s(14), ZIndex = 92}, C.surfaceHi, 0.1, C.text)
corner(cancelBtn, s(9))
local deleteBtn = mkBtn(dlg, {Text = "Delete", Size = UDim2.fromOffset(btnW, s(36)), Position = UDim2.fromOffset(s(16) + btnW + s(10), DH - s(16) - s(36)), TextSize = s(14), ZIndex = 92}, Color3.fromRGB(160, 84, 84), 0, WHITE)
corner(deleteBtn, s(9))
hideConfirm = function()
pendingDelete = nil
confirmOverlay.Visible = false
end
local function showConfirm(p)
pendingDelete = p
confirmMsg.Text = 'Do you want to delete this preset?\n"' .. tostring(p.Name) .. '"'
confirmOverlay.BackgroundTransparency = 1
confirmOverlay.Visible = true
dlgScale.Scale = 0.9
tw(confirmOverlay, 0.15, {BackgroundTransparency = 0.35})
tw(dlgScale, 0.22, {Scale = 1}, Enum.EasingStyle.Back)
end
cancelBtn.MouseButton1Click:Connect(hideConfirm)
confirmOverlay.MouseButton1Click:Connect(hideConfirm)
deleteBtn.MouseButton1Click:Connect(function()
local p = pendingDelete
hideConfirm()
if not p then return end
for i = #presetList, 1, -1 do
if presetList[i] == p then table.remove(presetList, i) end
end
saveJSON(PRESETS_FILE, presetList)
renderPresets(activeSpec or {})
toast("Preset deleted")
end)
local function snapshotEquipped()
local anims, any = {}, false
for slot, list in pairs(equippedAnims) do
if type(list) == "table" and #list > 0 then
local copy = {}
for _, a in ipairs(list) do copy[#copy + 1] = {Name = a.Name, AnimationId = a.AnimationId} end
anims[slot] = copy
any = true
end
end
return anims, any
end
local function nextPresetName()
local maxN = 0
for _, p in ipairs(presetList) do
local n = tonumber(string.match(p.Name or "", "^[Pp]reset (%d+)$"))
if n and n > maxN then maxN = n end
end
return "Preset " .. (maxN + 1)
end
local function summary(p)
local names, n = {}, 0
for _, slot in ipairs(buttonOrder) do
local l = p.Anims and p.Anims[slot]
if type(l) == "table" and #l > 0 then
n += 1
names[#names + 1] = shortNames[slot]
end
end
return n .. (n == 1 and " slot" or " slots") .. " · " .. table.concat(names, " · ")
end
local function inspectPreset(p)
local count = 0
for _ in pairs(p.Anims or {}) do count += 1 end
activeBundle = {Id = "preset:" .. tostring(p.Id), Name = p.Name, IsPreset = true, Count = count}
inspectGen = inspectGen + 1
masterGen = masterGen + 1
if masterTrack then pcall(function() masterTrack:Stop(0.1) end); masterTrack = nil end
table.clear(currentBundleItems)
hintLabel.Visible = false
for slot, list in pairs(p.Anims or {}) do
if type(list) == "table" and #list > 0 then
currentBundleItems[slot] = {Id = "preset:" .. tostring(p.Id) .. "/" .. slot, Name = p.Name, AssetType = slot, Direct = list}
end
end
refreshActiveInfo()
updateSelection()
local slot = currentBundleItems[selectedSlot] and selectedSlot or nil
if not slot then
for _, name in ipairs(buttonOrder) do
if currentBundleItems[name] then slot = name; break end
end
end
if not slot then
refreshSlotButtons()
setActionsEnabled(false)
setPreviewState("missing", selectedSlot)
return
end
selectedSlot = slot
refreshSlotButtons()
setActionsEnabled(true)
masterRig, masterAnimator = nil, nil
playMaster(slot)
end
local function buildRow(p, order)
local row = new("Frame", {
Size = UDim2.new(1, 0, 0, ROW_H), BackgroundColor3 = C.surface, BackgroundTransparency = 0.3, BorderSizePixel = 0, LayoutOrder = order,
}, presetPanel)
corner(row, s(10)); stroke(row, C.stroke, 1, 0.6)
local rowHit = new("TextButton", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Text = "", AutoButtonColor = false, ZIndex = 2}, row)
local prevW, wearW, renW, delW, g4 = s(56), s(44), s(60), s(52), s(4)
local blockW = prevW + wearW + renW + delW + g4 * 3 + s(8)
local yb = math.floor((ROW_H - s(30)) / 2)
local nameBox = new("TextBox", {
Position = UDim2.fromOffset(s(8), s(6)), Size = UDim2.new(1, -(blockW + s(14)), 0, s(22)), BackgroundColor3 = C.overlay, BackgroundTransparency = 1,
BorderSizePixel = 0, Text = p.Name, ClearTextOnFocus = false, Font = Enum.Font.GothamBold, TextSize = s(15), TextColor3 = C.text,
TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd, ClipsDescendants = true, ZIndex = 3,
}, row)
corner(nameBox, s(6)); pad(nameBox, s(4), s(4))
new("TextLabel", {
Position = UDim2.fromOffset(s(12), s(30)), Size = UDim2.new(1, -(blockW + s(20)), 0, s(16)), BackgroundTransparency = 1,
Text = summary(p), Font = Enum.Font.Gotham, TextSize = s(12), TextColor3 = C.textDim, TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 3,
}, row)
nameBox.Focused:Connect(function()
tw(nameBox, 0.12, {BackgroundTransparency = 0.25})
end)
nameBox.FocusLost:Connect(function()
tw(nameBox, 0.12, {BackgroundTransparency = 1})
local newName = nameBox.Text
newName = newName:gsub("^%s+", "")
newName = newName:gsub("%s+$", "")
newName = newName:sub(1, 24)
if newName == "" then
nameBox.Text = p.Name
elseif newName ~= p.Name then
p.Name = newName
nameBox.Text = newName
if activeBundle and activeBundle.IsPreset and activeBundle.Id == "preset:" .. tostring(p.Id) then
activeBundle.Name = newName
refreshActiveInfo()
end
saveJSON(PRESETS_FILE, presetList)
toast("Renamed to " .. newName, "ok")
else
nameBox.Text = newName
end
end)
local delBtn = mkBtn(row, {Text = "Delete", Size = UDim2.fromOffset(delW, s(30)), AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -s(8), 0, yb), TextSize = s(12), ZIndex = 3}, C.badBg, 0.1, C.badTx)
corner(delBtn, s(8))
local renBtn = mkBtn(row, {Text = "Rename", Size = UDim2.fromOffset(renW, s(30)), AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -(s(8) + delW + g4), 0, yb), TextSize = s(12), ZIndex = 3}, C.surfaceHi, 0.2, C.text)
corner(renBtn, s(8))
local wearBtn = mkBtn(row, {Text = "Wear", Size = UDim2.fromOffset(wearW, s(30)), AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -(s(8) + delW + renW + g4 * 2), 0, yb), TextSize = s(12), ZIndex = 3}, C.surfaceHi, 0.1, C.text)
corner(wearBtn, s(8))
local previewBtn = mkBtn(row, {Text = "Preview", Size = UDim2.fromOffset(prevW, s(30)), AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -(s(8) + delW + renW + wearW + g4 * 3), 0, yb), TextSize = s(12), ZIndex = 3}, C.surfaceHi, 0.1, C.text)
corner(previewBtn, s(8))
previewBtn.MouseButton1Click:Connect(function() inspectPreset(p) end)
rowHit.MouseButton1Click:Connect(function() inspectPreset(p) end)
renBtn.MouseButton1Click:Connect(function()
task.delay(0.03, function()
pcall(function()
nameBox:CaptureFocus()
nameBox.SelectionStart = 1
nameBox.CursorPosition = #nameBox.Text + 1
end)
end)
end)
delBtn.MouseButton1Click:Connect(function() showConfirm(p) end)
local busy = false
wearBtn.MouseButton1Click:Connect(function()
if busy then return end
if not lp.Character then toast("No character found", "bad"); return end
busy = true
wearBtn.Text = "…"
task.spawn(function()
local n = equipMap(lp.Character, p.Anims or {}, true)
toast(n > 0 and ("Equipped " .. p.Name) or "Couldn't equip that preset", n > 0 and "ok" or "bad")
if wearBtn.Parent then wearBtn.Text = "Wear" end
busy = false
end)
end)
if focusNameId == p.Id then
focusNameId = nil
task.delay(0.12, function()
pcall(function()
nameBox:CaptureFocus()
nameBox.SelectionStart = 1
nameBox.CursorPosition = #nameBox.Text + 1
end)
end)
end
end
renderPresets = function(spec)
spec = spec or {}
for _, c in ipairs(presetPanel:GetChildren()) do
if c:IsA("Frame") then c:Destroy() end
end
local kw = string.lower(spec.keyword or "")
local list = {}
for _, p in ipairs(presetList) do
if kw == "" or string.find(string.lower(p.Name or ""), kw, 1, true) then list[#list + 1] = p end
end
local mode = spec.sort or "new"
if mode == "name" then
table.sort(list, function(a, b) return string.lower(a.Name or "") < string.lower(b.Name or "") end)
elseif mode ~= "old" then
local rev = {}
for i = #list, 1, -1 do rev[#rev + 1] = list[i] end
list = rev
end
for i, p in ipairs(list) do buildRow(p, i) end
pageLbl.Text = #list .. (#list == 1 and " preset" or " presets")
if #presetList == 0 then
presetEmpty.Text = "No presets yet\nWear some animations, then tap\n“+ Save current”"
presetEmpty.Visible = true
elseif #list == 0 then
presetEmpty.Text = "No presets match"
presetEmpty.Visible = true
else
presetEmpty.Visible = false
end
end
saveCurrentBtn.MouseButton1Click:Connect(function()
local anims, any = snapshotEquipped()
if not any then toast("Nothing equipped yet. Wear an animation first", "bad"); return end
local p = {Id = tostring(os.time()) .. tostring(math.random(1000, 9999)), Name = nextPresetName(), Anims = anims, Time = os.time()}
presetList[#presetList + 1] = p
saveJSON(PRESETS_FILE, presetList)
focusNameId = p.Id
sb.Text = ""
if runSearch then runSearch("") end
toast("Saved as " .. p.Name .. ". Type to rename", "ok")
end)
end
refreshSavedView = function()
if currentTab ~= "Saved" then return end
if runSearch then runSearch(sb.Text) end
end
runSearch = function(text)
searchGen += 1
local myGen = searchGen
local q = parseQuery(text or "")
local spec = compileQuery(currentTab, q)
activeSpec = spec
currentPageIndex = 1
refreshTags(q, spec)
local isPresets = (currentTab == "Presets")
gridHolder.Visible = not isPresets
presetPanel.Visible = isPresets
saveCurrentBtn.Visible = isPresets
prevBtn.Visible = not isPresets
nextBtn.Visible = not isPresets
if isPresets then
clearGrid()
clearSkeletons()
gridMsg.Visible = false
renderPresets(spec)
return
end
presetEmpty.Visible = false
if currentTab ~= "Discover" then
local function buildAndDraw()
savedTabList = (currentTab == "Saved") and buildSavedList(spec) or buildHistoryList(spec)
drawGridPage(savedTabList)
end
local needPrice = spec.min ~= nil or spec.max ~= nil or spec.is == "free" or spec.is == "paid" or spec.is == "offsale" or spec.sort == "low" or spec.sort == "high"
if needPrice then
local src = {}
if currentTab == "Saved" then
for _, v in pairs(savedBookmarks) do if type(v) == "table" and v.Id then src[#src + 1] = v end end
else
for _, v in ipairs(historyList) do src[#src + 1] = v end
end
task.spawn(function()
resolveUnknownPrices(src)
if myGen ~= searchGen then return end
buildAndDraw()
end)
else
buildAndDraw()
end
return
end
searchResults = {}
catalogCursor = nil
showSkeletons()
pageLbl.Text = "Searching…"
task.spawn(function()
local params = buildParams(spec)
local ok, pages = pcall(function() return aes:SearchCatalog(params) end)
if myGen ~= searchGen then return end
if not ok or not pages then
showGridMessage("Search failed — try again")
pageLbl.Text = "Error"
return
end
catalogCursor = pages
local filtered = {}
local fetched = 0
local wantPrice = spec.min ~= nil or spec.max ~= nil
while true do
local page = catalogCursor:GetCurrentPage()
fetched += 1
if wantPrice then resolveUnknownPrices(page) end
local sawOverMax = false
for _, entry in ipairs(page) do
if passesClientFilter(entry, spec) then
filtered[#filtered + 1] = entry
elseif wantPrice and spec.max == 0 and spec.sort == nil then
local mt = seedMeta(entry)
if mt and mt.kind == "paid" and (mt.amount or 0) > 0 then sawOverMax = true end
end
end
if myGen ~= searchGen then return end
if #filtered > 0 or not wantPrice or sawOverMax or fetched >= 6 or catalogCursor.IsFinished then break end
local okAdv = pcall(function() catalogCursor:AdvanceToNextPageAsync() end)
if not okAdv or myGen ~= searchGen then break end
end
if myGen ~= searchGen then return end
searchResults = filtered
if #filtered == 0 then
showGridMessage("No bundles matched your search")
updatePager(0)
else
drawGridPage(searchResults)
end
end)
end
local function setTab(tab)
if currentTab == tab then return end
currentTab = tab
local order = {Discover = 0, Saved = 1, Recent = 2, Presets = 3}
local btns = {Discover = tabDiscover, Saved = tabSaved, Recent = tabRecent, Presets = tabPresets}
for name, b in pairs(btns) do
setBtn(b, C.surface, 1, name == tab and C.text or C.textMuted)
end
tw(tabIndicator, 0.2, {Position = UDim2.new(order[tab] / 4, s(order[tab] == 0 and 2 or 1), 0, s(2))})
runSearch(sb.Text)
refreshSuggestions()
end
tabDiscover.MouseButton1Click:Connect(function() setTab("Discover") end)
tabSaved.MouseButton1Click:Connect(function() setTab("Saved") end)
tabRecent.MouseButton1Click:Connect(function() setTab("Recent") end)
tabPresets.MouseButton1Click:Connect(function() setTab("Presets") end)
onHistoryChanged = function()
if currentTab == "Recent" and runSearch then runSearch(sb.Text) end
end
searchBtn.MouseButton1Click:Connect(function() runSearch(sb.Text) end)
sb.FocusLost:Connect(function(enter)
if not enter then return end
if suggestStrip.Visible and currentSuggestions[1] then
local cur = (sb.Text:gsub("%s+$", ""))
local lastToken = cur:match("(%S*)$") or ""
if lastToken:find(":", 1, true) then
local sg = currentSuggestions[1]
local ap = (sg.apply:gsub("%s+$", ""))
if ap ~= cur and ap:sub(1, #cur) == cur then
sb.Text = sg.apply
if sg.run then runSearch(sg.apply) end
return
end
end
end
runSearch(sb.Text)
end)
sb:GetPropertyChangedSignal("Text"):Connect(function()
clearBtn.Visible = sb.Text ~= ""
end)
clearBtn.MouseButton1Click:Connect(function()
sb.Text = ""
runSearch("")
end)
prevBtn.MouseButton1Click:Connect(function()
if pagingBusy or currentPageIndex <= 1 then return end
currentPageIndex -= 1
drawGridPage(currentTab ~= "Discover" and savedTabList or searchResults)
end)
nextBtn.MouseButton1Click:Connect(function()
if pagingBusy then return end
local list = currentTab ~= "Discover" and savedTabList or searchResults
local pages = math.max(1, math.ceil(#list / PER_PAGE))
local more = currentTab == "Discover" and catalogCursor ~= nil and not catalogCursor.IsFinished
if currentPageIndex < pages then
currentPageIndex += 1
drawGridPage(list)
elseif more then
pagingBusy = true
pageLbl.Text = "Loading…"
task.spawn(function()
local ok = pcall(function() catalogCursor:AdvanceToNextPageAsync() end)
pagingBusy = false
if not ok then toast("Couldn't load more results", "bad"); return end
local spec = activeSpec or {}
local before = #searchResults
local tries = 0
while true do
local page = catalogCursor:GetCurrentPage()
if spec.min ~= nil or spec.max ~= nil then resolveUnknownPrices(page) end
for _, e in ipairs(page) do
if passesClientFilter(e, spec) then searchResults[#searchResults + 1] = e end
end
tries += 1
if #searchResults > before or catalogCursor.IsFinished or tries >= 5 then break end
if not pcall(function() catalogCursor:AdvanceToNextPageAsync() end) then break end
end
if #searchResults == before then
toast("No more matching results", "bad")
pageLbl.Text = "Page " .. currentPageIndex
drawGridPage(searchResults)
return
end
currentPageIndex += 1
drawGridPage(searchResults)
end)
end
end)
local helpOpen = false
helpBtn.MouseButton1Click:Connect(function()
helpOpen = not helpOpen
settingsPanel.Visible = false
helpPanel.Visible = true
if helpOpen then
tw(helpPanel, 0.2, {BackgroundTransparency = 0.15})
tw(helpText, 0.22, {TextTransparency = 0})
setBtn(helpBtn, C.surfaceHi, 0.1, C.text)
setBtn(settingsBtn, C.surface, 0.2, C.textDim)
else
tw(helpPanel, 0.18, {BackgroundTransparency = 1})
tw(helpText, 0.18, {TextTransparency = 1})
task.delay(0.2, function()
if not helpOpen then helpPanel.Visible = false end
end)
setBtn(helpBtn, C.surface, 0.25, C.textDim)
end
end)
pad(helpScroll, s(14), s(14), s(10), s(10))
local settingsOpen = false
settingsBtn.MouseButton1Click:Connect(function()
settingsOpen = not settingsOpen
helpPanel.Visible = false
settingsPanel.Visible = true
if settingsOpen then
tw(settingsPanel, 0.2, {BackgroundTransparency = 0.15})
setBtn(settingsBtn, C.surfaceHi, 0.1, C.text)
setBtn(helpBtn, C.surface, 0.25, C.textDim)
helpOpen = false
else
tw(settingsPanel, 0.18, {BackgroundTransparency = 1})
task.delay(0.2, function()
if not settingsOpen then settingsPanel.Visible = false end
end)
setBtn(settingsBtn, C.surface, 0.2, C.textDim)
end
end)
local introTargets = nil
local function hideToPill()
introTargets = collectFade(mf)
for _, tg in ipairs(introTargets) do
local props
if tg.obj.BackgroundTransparency < 1 then props = {BackgroundTransparency = 1} end
if tg.txtT and tg.obj.TextTransparency < 1 then
props = props or {}
props.TextTransparency = 1
end
if props then tw(tg.obj, 0.2, props) end
end
task.delay(0.24, function()
mf.Visible = false
pill.Visible = true
pill.Size = UDim2.fromOffset(0, 0)
tw(pill, 0.22, {Size = UDim2.fromOffset(s(58), s(58))}, Enum.EasingStyle.Back)
end)
end
local function showFromPill()
pill.Visible = false
mf.Visible = true
local targets = introTargets or collectFade(mf)
for _, tg in ipairs(targets) do
if tg.obj.Parent then
local props
if tg.obj.BackgroundTransparency ~= tg.bgT then props = {BackgroundTransparency = tg.bgT} end
if tg.txtT and tg.obj.TextTransparency ~= tg.txtT then
props = props or {}
props.TextTransparency = tg.txtT
end
if props then tw(tg.obj, 0.28, props) end
end
end
end
closeBtn.MouseButton1Click:Connect(hideToPill)
minBtn.MouseButton1Click:Connect(hideToPill)
pill.MouseButton1Click:Connect(showFromPill)
local dragging, dragStart, startPos
top.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
dragging = true
dragStart = input.Position
startPos = mf.Position
end
end)
track(uis.InputChanged:Connect(function(input)
if not dragging then return end
if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then
local delta = input.Position - dragStart
mf.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y)
end
end))
track(uis.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
dragging = false
end
end))
track(uis.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then return end
if input.KeyCode == Enum.KeyCode.RightShift then
if mf.Visible then hideToPill() else showFromPill() end
elseif input.KeyCode == Enum.KeyCode.Escape and confirmOverlay.Visible then
hideConfirm()
elseif input.KeyCode == Enum.KeyCode.Escape and helpOpen then
helpOpen = false
helpPanel.Visible = false
helpText.TextTransparency = 1
helpPanel.BackgroundTransparency = 1
setBtn(helpBtn, C.surface, 0.25, C.textDim)
elseif input.KeyCode == Enum.KeyCode.Escape and settingsOpen then
settingsOpen = false
settingsPanel.Visible = false
settingsPanel.BackgroundTransparency = 1
setBtn(settingsBtn, C.surface, 0.2, C.textDim)
end
end))
env.UgcAnimStudioCleanup = function()
for _, c in ipairs(connections) do pcall(function() c:Disconnect() end) end
table.clear(connections)
for obj in pairs(spinning) do spinning[obj] = nil end
for obj, d in pairs(shimmers) do
pcall(function() d[2]:Cancel() end)
pcall(function() d[1]:Destroy() end)
shimmers[obj] = nil
end
if masterTrack then pcall(function() masterTrack:Stop(0) end) end
tweenShutdown()
pcall(function() g:Destroy() end)
end
runSearch("")
task.spawn(function()
task.wait(1)
if scriptSettings.restoreLastPack and lastPackData.Id then
wearBundle(lastPackData.Id, lastPackData.Name)
end
end)
env.UgcAnimStudio = {gui = g, main = mf}
end)
if not uiOk then
warn("[UGC Anim Studio] BUILD ERROR: " .. tostring(uiErr))
dbg("BUILD ERROR: " .. tostring(uiErr))
else
dbg("ui built successfully")
pcall(function()
local mf2 = env.UgcAnimStudio and env.UgcAnimStudio.main
if not mf2 then return end
local us = Instance.new("UIScale")
us.Scale = 0.86
us.Parent = mf2
local origPos = mf2.Position
mf2.Position = origPos + UDim2.fromOffset(0, 26)
local targets = collectFade(mf2)
for _, tg in ipairs(targets) do
if tg.bgT < 1 then tg.obj.BackgroundTransparency = 1 end
if tg.txtT and tg.txtT < 1 then tg.obj.TextTransparency = 1 end
end
tw(mf2, 0.55, {Position = origPos}, Enum.EasingStyle.Quint)
tw(us, 0.7, {Scale = 1}, Enum.EasingStyle.Back)
for _, tg in ipairs(targets) do
local props
if tg.bgT < 1 then props = {BackgroundTransparency = tg.bgT} end
if tg.txtT and tg.txtT < 1 then
props = props or {}
props.TextTransparency = tg.txtT
end
if props then tw(tg.obj, 0.55, props, Enum.EasingStyle.Quint) end
end
end)
endFeatures
- Fe animation script 😛
- Features:
- R6 Detection
- Advance Animation speed
- Bla bla bla