
KeylessUniversal

--[[
╔══════════════════════════════════════════════════════╗
║ FNF ANIMATION INJECTOR v6.1 ║
║ Arrow HUD • Dynamic Face • Anim Maker • Leg Lock ║
║ + Idle BPM Speed Control ║
║ + Live Preview • Favorites • Playlist • Themes ║
║ + Hold-to-Loop + Weighted Alt Animations (per bind) ║
╚══════════════════════════════════════════════════════╝
• Z/X/C/V → play directional anims
• Arrow HUD reacts on screen when you hit keys
• Face decal swaps per direction (if your character has one)
• Animation Sequence Maker (chain anims with delays)
• Optional Legs-on-Ground lock (no walk / no jump)
• Idle animation speed scales with BPM
• Live character viewport preview
• Equalizer / waveform / beat-sync visualizers
• Favorites, recents, presets, playlist manager
• Pink / Cyan / Purple / RGB theme selector
• NEW: per-direction HOLD-LOOP toggle — hold the key to keep the
animation looping instead of snap-freezing on the pose
• NEW: per-direction weighted ALT ANIMATIONS — each key press can
roll into an alt animation instead of the primary one, at
whatever % chance you configure
]]
-- ============================================================
-- SERVICES & GLOBALS
-- ============================================================
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer:WaitForChild("PlayerGui")
-- ============================================================
-- DEFAULT BINDINGS
-- ============================================================
local DEFAULT_BINDINGS = {
{ dir = "LEFT", key = "Z", id = "243827693" },
{ dir = "DOWN", key = "X", id = "182724289" },
{ dir = "UP", key = "C", id = "183412246" },
{ dir = "RIGHT", key = "V", id = "243656287" },
}
local CurrentBindings = {}
for _, v in ipairs(DEFAULT_BINDINGS) do
-- holdLoop: false = original "snap to pose and freeze" behavior,
-- true = animation actually loops for as long as the key is held
-- alts: { {id = "123", chance = 25}, ... } — weighted alt anim IDs,
-- rolled against on every key press; uncovered % falls back to `id`
table.insert(CurrentBindings, { dir = v.dir, key = v.key, id = v.id, speedMult = 1, holdLoop = false, alts = {} })
end
-- Placeholder quick-preset library (edit IDs freely; these are just slots)
local PRESETS = {
{ name = "Default Set", ids = { LEFT="204062532", DOWN="204292303", UP="183412246", RIGHT="218504594" } },
{ name = "Preset Slot A",ids = { LEFT="0", DOWN="0", UP="0", RIGHT="0" } },
{ name = "Preset Slot B",ids = { LEFT="0", DOWN="0", UP="0", RIGHT="0" } },
}
-- ============================================================
-- STATE
-- ============================================================
local animTracks = {}
local idleTrack = nil
local connections = {}
local animator = nil
local character = nil
local rebindingRow = nil
local guiVisible = true
local idleLooping = false
-- RP Tug-of-War State
local rpModeEnabled = false
local currentTarget = nil
local rpMoveStep = 1.5 -- Base speed/distance per note
local rpMinDistance = 3.5 -- How close before stopping
local rpPushbackStep = 1.0 -- How far you get pushed back when the target hits a note
local targetAnimConn = nil -- Stores the listener for the target's animations
-- Legs on ground
local legsLocked = false
local savedWalkSpeed = 16
local savedJumpPower = 50
-- Face system
local faceDecal = nil
local defaultFaceId = nil
local faceIds = { LEFT = "", DOWN = "", UP = "", RIGHT = "" }
-- Sequence maker
local sequence = {}
local seqPlaying = false
local seqTrack = nil
local seqRowFrames = {}
-- Arrow HUD handles
local arrowHudFrames = {} -- [dir] = { bg, lbl }
-- Idle BPM control
local idleBPM = 100 -- the tempo you want the idle to play at
local BASE_BPM = 58 -- the tempo the idle animation was originally made/synced for
-- Status / playback tracking (drives the status panel + visualizers)
local StatusState = {
currentAnimName = "—",
currentAnimId = "",
speedMult = 1,
bpm = idleBPM,
playing = false,
}
-- Favorites / recents / playlist
-- Seeded with Roblox's own default emote IDs — these are built into every
-- avatar by Roblox itself, so they're free to use and always available.
local favorites = {
{ name = "Default: Dance", id = "507771019" },
{ name = "Default: Dance2", id = "507776043" },
{ name = "Default: Dance3", id = "507777268" },
{ name = "Default: Laugh", id = "507770818" },
{ name = "Default: Cheer", id = "507770677" },
{ name = "Default: Point", id = "507770453" },
{ name = "Default: Sit", id = "2506281703" },
}
local recents = {} -- { id, id, ... } most-recent-first, capped
local RECENTS_CAP = 8
local playlist = {} -- { {name=, id=, delay=} }
local playlistIndex = 1
local playlistPlaying = false
-- Preview viewport
local previewClone = nil
local previewAnimator = nil
local previewCam = nil
local previewAngle = 0
-- Theme
local THEMES = {
Pink = { accent = Color3.fromRGB(255, 80, 180), accent2 = Color3.fromRGB(0, 200, 255) },
Cyan = { accent = Color3.fromRGB(0, 200, 255), accent2 = Color3.fromRGB(255, 80, 180) },
Purple = { accent = Color3.fromRGB(165, 85, 255), accent2 = Color3.fromRGB(255, 80, 180) },
RGB = { accent = Color3.fromRGB(255, 0, 0), accent2 = Color3.fromRGB(0, 255, 255) },
}
local currentThemeName = "Pink"
local themedStrokes = {} -- UIStroke objects to recolor with accent
local themedBgs = {} -- Frame/TextButton objects to recolor with accent
local themedTexts = {} -- TextLabel objects to recolor with accent
local rgbCycleOn = false
-- ============================================================
-- VINYL RECORD WIDGET
-- ============================================================
local vinylImage = nil -- ImageLabel, set your decal via vinylImage.Image or the Settings box
local VINYL_DECAL_ID = "" -- paste an asset id here, or set it live from the FX tab
local vinylRotation = 0
local vinylBaseRPM = 33.3 -- classic vinyl RPM at speedMult == 1 / idle BPM baseline
-- ============================================================
-- CRT MONITOR MODE
-- ============================================================
local crtEnabled = false
local crtOverlay = nil -- Frame that holds scanlines + tint, toggled visible
local crtFlickerConn = nil
-- ============================================================
-- SPECTROGRAM (MIC) VISUALIZER
-- ============================================================
-- Uses Roblox's newer Audio API (AudioDeviceInput + AudioAnalyzer). This is
-- still a fairly recent, evolving part of the engine, needs the experience
-- to have voice/microphone permission granted by the player, and isn't
-- guaranteed to exist on every client — everything below is wrapped in
-- pcall and falls back to an animation-synced "fake" spectrogram if real
-- mic analysis isn't available.
local micDeviceInput = nil
local micAnalyzer = nil
local micWire = nil
local micAvailable = false
local specBars = {}
local SPEC_BAR_COUNT = 20
-- ============================================================
-- ANIMATION BPM DETECTOR
-- ============================================================
-- Two detection modes:
-- 1) TAP MODE: every directional key press timestamp is recorded; the
-- rolling average gap between presses is converted straight to BPM.
-- 2) IDLE-LENGTH MODE: BPM is derived from the currently loaded idle
-- animation's length and a user-supplied "beats per loop" count.
local bpmDetectTaps = {} -- recent os.clock() timestamps of direction presses
local BPM_TAP_WINDOW = 8 -- how many taps to average over
local detectedBPM = nil
local beatsPerIdleLoop = 4
-- ============================================================
-- SHARED MUSIC PLAYER (no VC — plays an uploaded audio asset for
-- every player in the server via a server-replicated Sound, instead
-- of routing anything through voice chat)
-- ============================================================
-- Requires a small companion SERVER script (provided separately) that
-- creates the "FNFMusicRemote" RemoteEvent and the actual Sound
-- instance — a LocalScript alone cannot make a sound audible to other
-- players, only the server can do that.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")
local musicRemote = nil
local MUSIC_SOUND_NAME = "FNFMusicPlayer"
local musicStatusLbl = nil -- assigned once the FX tab is built
local musicRequestTick = 0
-- ============================================================
-- CONSTANTS
-- ============================================================
local PRIORITY = Enum.AnimationPriority.Action
local FRAME_TARGET = 10
local BLEND_IN = 0
local BLEND_OUT = 0.05
-- FNF arrow colours
local ARROW_COLORS = {
LEFT = { idle = Color3.fromRGB(150, 50, 130), hit = Color3.fromRGB(255, 110, 230) },
DOWN = { idle = Color3.fromRGB(0, 130, 170), hit = Color3.fromRGB(0, 240, 255) },
UP = { idle = Color3.fromRGB(15, 130, 10), hit = Color3.fromRGB(20, 250, 5) },
RIGHT = { idle = Color3.fromRGB(160, 35, 35), hit = Color3.fromRGB(255, 90, 90) },
}
local ARROW_SYMBOLS = { LEFT = "◄", DOWN = "▼", UP = "▲", RIGHT = "►" }
-- Arrow HUD layout
local ARROW_SIZE = 64
local ARROW_HIT_SIZE = 74
local ARROW_GAP = 14
local ARROW_ORDER = { "LEFT", "DOWN", "UP", "RIGHT" }
-- ============================================================
-- LOGGING
-- ============================================================
local function log(m) print("[FNF] "..tostring(m)) end
-- ============================================================
-- ANIMATOR HELPERS
-- ============================================================
local function getAnimator(char)
local hum = char:FindFirstChildOfClass("Humanoid")
if not hum then return nil end
local a = hum:FindFirstChildOfClass("Animator")
if not a then a = Instance.new("Animator"); a.Parent = hum end
return a
end
local function destroyTrack(key)
local t = animTracks[key]
if t then pcall(function() t:Stop(0) end) pcall(function() t:Destroy() end) animTracks[key] = nil end
end
local function destroyIdleTrack()
if idleTrack then pcall(function() idleTrack:Stop(0) end) pcall(function() idleTrack:Destroy() end) idleTrack = nil end
idleLooping = false
end
local function destroySeqTrack()
if seqTrack then pcall(function() seqTrack:Stop(0) end) pcall(function() seqTrack:Destroy() end) seqTrack = nil end
end
local function cleanupAll()
idleLooping = false; seqPlaying = false
destroyIdleTrack(); destroySeqTrack()
for k in pairs(animTracks) do destroyTrack(k) end
for _, c in ipairs(connections) do pcall(function() c:Disconnect() end) end
connections = {}; animator = nil; character = nil
faceDecal = nil; defaultFaceId = nil
end
-- ============================================================
-- NOTIFICATION TOASTS (forward-declared, container built in GUI section)
-- ============================================================
local notifyContainer = nil
local function notify(text, kind)
if not notifyContainer then log("[toast] "..text); return end
kind = kind or "info"
local colors = {
info = Color3.fromRGB(0, 200, 255),
success = Color3.fromRGB(80, 220, 120),
warn = Color3.fromRGB(255, 180, 60),
error = Color3.fromRGB(255, 80, 80),
}
local toast = Instance.new("Frame")
toast.Size = UDim2.new(1, 0, 0, 40)
toast.BackgroundColor3 = Color3.fromRGB(20, 20, 30)
toast.BackgroundTransparency = 0.05
toast.BorderSizePixel = 0
toast.ClipsDescendants = true
toast.Parent = notifyContainer
local corner = Instance.new("UICorner"); corner.CornerRadius = UDim.new(0,8); corner.Parent = toast
local stroke = Instance.new("UIStroke"); stroke.Color = colors[kind] or colors.info; stroke.Thickness = 1.5; stroke.Parent = toast
local bar = Instance.new("Frame")
bar.Size = UDim2.new(0, 4, 1, 0)
bar.BackgroundColor3 = colors[kind] or colors.info
bar.BorderSizePixel = 0
bar.Parent = toast
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.new(1, -16, 1, 0); lbl.Position = UDim2.new(0, 12, 0, 0)
lbl.BackgroundTransparency = 1; lbl.Text = text
lbl.TextColor3 = Color3.fromRGB(230,230,240); lbl.Font = Enum.Font.GothamBold
lbl.TextSize = 12; lbl.TextXAlignment = Enum.TextXAlignment.Left
lbl.TextWrapped = true; lbl.Parent = toast
toast.Position = UDim2.new(1, 20, 0, 0)
TweenService:Create(toast, TweenInfo.new(0.25, Enum.EasingStyle.Quint, Enum.EasingDirection.Out), { Position = UDim2.new(0,0,0,0) }):Play()
task.delay(2.6, function()
if not toast or not toast.Parent then return end
local t = TweenService:Create(toast, TweenInfo.new(0.25, Enum.EasingStyle.Quint, Enum.EasingDirection.In), { BackgroundTransparency = 1 })
TweenService:Create(lbl, TweenInfo.new(0.2), { TextTransparency = 1 }):Play()
TweenService:Create(stroke, TweenInfo.new(0.2), { Transparency = 1 }):Play()
t:Play()
t.Completed:Connect(function() if toast then toast:Destroy() end end)
end)
end
-- ============================================================
-- THEME SYSTEM
-- ============================================================
local function regStroke(obj) table.insert(themedStrokes, obj); obj.Color = THEMES[currentThemeName].accent end
local function regBg(obj) table.insert(themedBgs, obj); obj.BackgroundColor3 = THEMES[currentThemeName].accent end
local function regText(obj) table.insert(themedTexts, obj); obj.TextColor3 = THEMES[currentThemeName].accent end
local function applyThemeColors(accent)
for _, o in ipairs(themedStrokes) do pcall(function() o.Color = accent end) end
for _, o in ipairs(themedBgs) do pcall(function() o.BackgroundColor3 = accent end) end
for _, o in ipairs(themedTexts) do pcall(function() o.TextColor3 = accent end) end
end
local function setTheme(name)
if not THEMES[name] then return end
currentThemeName = name
rgbCycleOn = (name == "RGB")
if not rgbCycleOn then
applyThemeColors(THEMES[name].accent)
end
notify("Theme set to "..name, "info")
end
-- RGB cycling loop (only active while theme == RGB)
task.spawn(function()
local hue = 0
while true do
task.wait(0.05)
if rgbCycleOn then
hue = (hue + 0.006) % 1
applyThemeColors(Color3.fromHSV(hue, 0.85, 1))
end
end
end)
-- ============================================================
-- FACE SYSTEM
-- ============================================================
local function initFace(char)
local head = char:FindFirstChild("Head")
if not head then return end
local d = head:FindFirstChild("face")
if not d or d.ClassName ~= "Decal" then return end
faceDecal = d
defaultFaceId = d.Texture
log("Face found: "..defaultFaceId)
end
local function swapFace(dir)
if not faceDecal then return end
local id = faceIds[dir]
if id and id ~= "" then
faceDecal.Texture = "rbxassetid://"..id
end
end
local function restoreFace()
if faceDecal and defaultFaceId then
faceDecal.Texture = defaultFaceId
end
end
-- ============================================================
-- LEGS ON GROUND
-- ============================================================
local function applyLegsLock(locked)
if not character then return end
local hum = character:FindFirstChildOfClass("Humanoid")
if not hum then return end
if locked then
savedWalkSpeed = hum.WalkSpeed
savedJumpPower = hum.JumpPower
hum.WalkSpeed = 0
hum.JumpPower = 0
else
hum.WalkSpeed = savedWalkSpeed
hum.JumpPower = savedJumpPower
end
end
-- ============================================================
-- RECENTS
-- ============================================================
local function pushRecent(id, label)
if not id or id == "" then return end
for i = #recents, 1, -1 do
if recents[i].id == id then table.remove(recents, i) end
end
table.insert(recents, 1, { id = id, label = label or id })
while #recents > RECENTS_CAP do table.remove(recents) end
if refreshRecentsUI then refreshRecentsUI() end
end
-- ============================================================
-- LIVE PREVIEW HELPERS (forward declared, viewport built in GUI section)
-- ============================================================
local function updatePreviewAnimation(assetId, label)
StatusState.currentAnimName = label or assetId
StatusState.currentAnimId = assetId
if refreshStatusUI then refreshStatusUI() end
if not previewAnimator or not assetId or assetId == "" then return end
local ok, result = pcall(function()
local anim = Instance.new("Animation")
anim.AnimationId = "rbxassetid://"..assetId
local t = previewAnimator:LoadAnimation(anim)
t.Priority = Enum.AnimationPriority.Action
return t
end)
if ok and result then
for _, t in ipairs(previewAnimator:GetPlayingAnimationTracks()) do t:Stop(0) end
result:Play(0.05)
end
end
-- ============================================================
-- RP TUG-OF-WAR STATE & CONFIG
-- ============================================================
local rpModeEnabled = true
local currentTarget = nil
-- Forward Movement (YOUR Note Hits)
local rpMoveStep = 1.5 -- Distance you slide forward per note
local rpMinDistance = 3.5 -- Closest you can get to target
-- Backward Movement (TARGET'S Note Hits / Health Drain)
local rpRetreatStep = 1.2 -- Distance you get pulled back when target hits a note
local rpMaxDistance = 25.0 -- Maximum pushback distance limit
-- Event Connections
local targetAnimConn = nil -- Stores AnimationPlayed listener
local targetCharAddedConn = nil -- Stores target respawn listener
-- ============================================================
-- TUG-OF-WAR LOGIC
-- ============================================================
-- Disconnects active listeners to prevent duplicate triggers or memory leaks
local function stopWatchingTargetAnims()
if targetAnimConn then
targetAnimConn:Disconnect()
targetAnimConn = nil
end
if targetCharAddedConn then
targetCharAddedConn:Disconnect()
targetCharAddedConn = nil
end
end
-- Pushes YOUR character BACKWARD when the TARGET plays an animation
local function triggerRetreat()
if not rpModeEnabled or not currentTarget then return end
if not character or not currentTarget.Character then return end
local myRoot = character:FindFirstChild("HumanoidRootPart")
local targetRoot = currentTarget.Character:FindFirstChild("HumanoidRootPart")
if not myRoot or not targetRoot then return end
-- Vector pointing directly AWAY from the target
local awayVector = (myRoot.Position - targetRoot.Position)
local distance = awayVector.Magnitude
if distance <= 0.001 then return end -- Avoid divide-by-zero if overlapping
-- Push backward only if within max distance
if distance < rpMaxDistance then
local actualStep = math.min(rpRetreatStep, rpMaxDistance - distance)
myRoot.CFrame = myRoot.CFrame + (awayVector.Unit * actualStep)
end
end
-- Hooks into the target's Animator to detect any incoming notes/anims
local function startWatchingTargetAnims()
stopWatchingTargetAnims()
if not currentTarget then return end
local function bindAnimator(targetChar)
if not targetChar then return end
local hum = targetChar:WaitForChild("Humanoid", 5)
if not hum then return end
local anr = hum:WaitForChild("Animator", 5) or hum:FindFirstChildOfClass("Animator")
if not anr then return end
-- Triggers retreat every time target plays ANY animation track
targetAnimConn = anr.AnimationPlayed:Connect(function(track)
if rpModeEnabled then
triggerRetreat()
end
end)
end
-- Bind immediately if character exists
if currentTarget.Character then
bindAnimator(currentTarget.Character)
end
-- Re-bind automatically if target dies/respawns
targetCharAddedConn = currentTarget.CharacterAdded:Connect(function(newChar)
stopWatchingTargetAnims()
bindAnimator(newChar)
end)
end
-- Target Selection Function
local function setTargetPlayer(partialName)
if not partialName or partialName == "" then
currentTarget = nil
stopWatchingTargetAnims()
if notify then notify("RP Target cleared.", "info") end
return nil
end
local lowerName = partialName:lower()
for _, player in ipairs(Players:GetPlayers()) do
if player ~= LocalPlayer and player.Name:lower():sub(1, #lowerName) == lowerName then
currentTarget = player
startWatchingTargetAnims()
if notify then notify("Target locked: " .. player.Name, "success") end
return player
end
end
if notify then notify("Player not found.", "warn") end
return nil
end
-- Pushes YOUR character FORWARD when YOU hit a note key
local function triggerRPMovement()
if not rpModeEnabled or not currentTarget then return end
if not character or not currentTarget.Character then return end
local myRoot = character:FindFirstChild("HumanoidRootPart")
local targetRoot = currentTarget.Character:FindFirstChild("HumanoidRootPart")
if not myRoot or not targetRoot then return end
local direction = (targetRoot.Position - myRoot.Position)
local distance = direction.Magnitude
-- Slide forward towards target
if distance > rpMinDistance then
local actualStep = math.min(rpMoveStep, distance - rpMinDistance)
myRoot.CFrame = myRoot.CFrame + (direction.Unit * actualStep)
end
end
-- ============================================================
-- DIRECTIONAL ANIMATION PLAYBACK
-- ============================================================
-- holdLoop: false (default) reproduces the original behavior — quickly
-- play to the target frame, then freeze there until the key is released.
-- holdLoop: true instead keeps the animation genuinely looping at
-- speedMult for as long as the key stays down, and lets stopAnim's
-- normal blend-out handle the release.
local function playAnim(key, assetId, speedMult, holdLoop)
if not animator then return end
speedMult = speedMult or 1
if speedMult <= 0 then speedMult = 0.01 end
local wantLooped = holdLoop and true or false
local track = animTracks[key]
if not track or not track.Animation
or track.Animation.AnimationId ~= "rbxassetid://"..assetId
or track.Looped ~= wantLooped then
destroyTrack(key)
local ok, result = pcall(function()
local anim = Instance.new("Animation")
anim.AnimationId = "rbxassetid://"..assetId
local t = animator:LoadAnimation(anim)
t.Priority = PRIORITY; t.Looped = wantLooped
return t
end)
if not ok then warn("[FNF] Load fail: "..assetId); return end
track = result; animTracks[key] = track
end
if wantLooped then
-- HOLD-TO-SUSTAIN MODE: just keep it looping at speedMult; release
-- is handled by stopAnim()'s normal blend-out below.
track:Play(BLEND_IN)
track:AdjustSpeed(speedMult)
else
-- ORIGINAL "SNAP TO POSE" MODE: quickly play to the target frame
-- then freeze there until release.
local baseReach = FRAME_TARGET / 60
local reach = baseReach / speedMult
track:Play(BLEND_IN)
track:AdjustSpeed(track.Length > 0 and (track.Length / reach) or 10)
task.delay(reach, function()
if track and track.IsPlaying then
track:AdjustSpeed(0)
track.TimePosition = math.max(0, track.Length - 0.01)
end
end)
end
StatusState.currentAnimName = key
StatusState.currentAnimId = assetId
StatusState.speedMult = speedMult
StatusState.playing = true
if refreshStatusUI then refreshStatusUI() end
updatePreviewAnimation(assetId, key)
pushRecent(assetId, key)
end
local function stopAnim(key)
local t = animTracks[key]
if t and t.IsPlaying then t:Stop(BLEND_OUT) end
StatusState.playing = false
if refreshStatusUI then refreshStatusUI() end
end
-- Rolls which animation id to actually use for a given binding's press.
-- Alt slots are checked in order, each claiming its configured % slice of
-- a 0-100 roll; anything left uncovered (including "no alts configured
-- at all") falls back to the binding's primary animation id.
local function rollBindingAnimId(row)
if row.alts and #row.alts > 0 then
local roll = math.random() * 100
local acc = 0
for _, alt in ipairs(row.alts) do
acc += alt.chance
if roll <= acc and alt.id ~= "" then
return alt.id
end
end
end
return row.id
end
-- ============================================================
-- ARROW HUD REACTIONS
-- ============================================================
local function hitArrow(dir)
local f = arrowHudFrames[dir]
if not f then return end
TweenService:Create(f.bg, TweenInfo.new(0.05, Enum.EasingStyle.Linear), {
BackgroundColor3 = ARROW_COLORS[dir].hit,
Size = UDim2.new(0, ARROW_HIT_SIZE, 0, ARROW_HIT_SIZE),
}):Play()
if f.stroke then
TweenService:Create(f.stroke, TweenInfo.new(0.05, Enum.EasingStyle.Linear), { Thickness = 4 }):Play()
end
if f.glow then
f.glow.Visible = true
TweenService:Create(f.glow, TweenInfo.new(0.05), { BackgroundTransparency = 0.35 }):Play()
end
end
local function releaseArrow(dir)
local f = arrowHudFrames[dir]
if not f then return end
TweenService:Create(f.bg, TweenInfo.new(0.1, Enum.EasingStyle.Linear), {
BackgroundColor3 = ARROW_COLORS[dir].idle,
Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE),
}):Play()
if f.stroke then
TweenService:Create(f.stroke, TweenInfo.new(0.1, Enum.EasingStyle.Linear), { Thickness = 2 }):Play()
end
if f.glow then
local tw = TweenService:Create(f.glow, TweenInfo.new(0.15), { BackgroundTransparency = 1 })
tw:Play()
tw.Completed:Connect(function() if f.glow then f.glow.Visible = false end end)
end
end
-- ============================================================
-- AFTERIMAGE HELPER
-- ============================================================
local function spawnAfterimage(char)
if not char then return end
-- Briefly allow cloning in case the game disabled it
local oldArch = char.Archivable
char.Archivable = true
local ok, clone = pcall(function() return char:Clone() end)
char.Archivable = oldArch
if not ok or not clone then return end
-- Strip out logic and configure parts for the ghost effect
for _, v in ipairs(clone:GetDescendants()) do
if v:IsA("Script") or v:IsA("LocalScript") then
v:Destroy()
elseif v:IsA("BasePart") then
v.CanCollide = false
v.Anchored = true
v.Massless = true
v.Material = Enum.Material.ForceField -- Gives a slick, ghostly energy vibe
-- Fade out parts smoothly
TweenService:Create(v, TweenInfo.new(0.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Transparency = 1}):Play()
elseif v:IsA("Decal") or v:IsA("Texture") or v:IsA("SurfaceAppearance") then
TweenService:Create(v, TweenInfo.new(0.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Transparency = 1}):Play()
end
end
-- Make sure the clone stays frozen exactly where it spawned
local root = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
if root then
root.Anchored = true
end
clone.Parent = workspace
-- Delete the clone entirely once the fade tween is done
task.delay(0.45, function()
if clone then clone:Destroy() end
end)
end
-- ============================================================
-- IDLE ANIMATION
-- ============================================================
local function idleSpeedFromBPM()
if not BASE_BPM or BASE_BPM <= 0 then return 1 end
return idleBPM / BASE_BPM
end
local function loadIdleTrack(assetId, shouldLoop)
destroyIdleTrack()
if not animator then return nil end
local ok, result = pcall(function()
local anim = Instance.new("Animation")
anim.AnimationId = "rbxassetid://"..assetId
local t = animator:LoadAnimation(anim)
t.Priority = Enum.AnimationPriority.Idle; t.Looped = shouldLoop
return t
end)
if not ok then return nil end
return result
end
local function playIdleOnce(assetId, cb)
local t = loadIdleTrack(assetId, false)
if not t then return end
idleTrack = t; t:Play(0.1)
t:AdjustSpeed(idleSpeedFromBPM())
updatePreviewAnimation(assetId, "IDLE (once)")
t.Stopped:Connect(function() if cb then cb() end end)
end
local function playIdleLoop(assetId)
local t = loadIdleTrack(assetId, true)
if not t then return end
idleTrack = t; idleLooping = true; t:Play(0.1)
t:AdjustSpeed(idleSpeedFromBPM())
updatePreviewAnimation(assetId, "IDLE (loop)")
StatusState.playing = true
if refreshStatusUI then refreshStatusUI() end
end
local function stopIdle()
idleLooping = false
if idleTrack and idleTrack.IsPlaying then idleTrack:Stop(0.2) end
StatusState.playing = false
if refreshStatusUI then refreshStatusUI() end
end
local function setIdleBPM(bpm)
if bpm and bpm > 0 then idleBPM = bpm end
StatusState.bpm = idleBPM
if idleTrack and idleTrack.IsPlaying then
idleTrack:AdjustSpeed(idleSpeedFromBPM())
end
if refreshStatusUI then refreshStatusUI() end
end
local function setIdleBaseBPM(bpm)
if bpm and bpm > 0 then BASE_BPM = bpm end
if idleTrack and idleTrack.IsPlaying then
idleTrack:AdjustSpeed(idleSpeedFromBPM())
end
end
-- ============================================================
-- ANIMATION BPM DETECTOR
-- ============================================================
-- Tap mode: call this every time a directional anim is triggered by the
-- player. Keeps a rolling window of press timestamps and converts the
-- average gap between them into BPM.
local function registerBpmTap()
local now = os.clock()
table.insert(bpmDetectTaps, now)
while #bpmDetectTaps > BPM_TAP_WINDOW do table.remove(bpmDetectTaps, 1) end
if #bpmDetectTaps < 3 then return nil end
local gaps = {}
for i = 2, #bpmDetectTaps do
local gap = bpmDetectTaps[i] - bpmDetectTaps[i-1]
if gap > 0.08 and gap < 3 then table.insert(gaps, gap) end
end
if #gaps == 0 then return nil end
-- discard outliers more than 2x away from the median gap, then average
table.sort(gaps)
local median = gaps[math.ceil(#gaps/2)]
local sum, count = 0, 0
for _, g in ipairs(gaps) do
if g < median * 2 and g > median * 0.5 then sum = sum + g; count = count + 1 end
end
if count == 0 then return nil end
local avgGap = sum / count
detectedBPM = math.floor((60 / avgGap) + 0.5)
return detectedBPM
end
-- Idle-length mode: BPM = (beats per loop * 60) / animation length in seconds.
-- Reads whatever idle track is currently loaded (call after playIdleLoop/Once).
local function detectBpmFromIdleLength(beatsPerLoop)
beatsPerLoop = beatsPerLoop or beatsPerIdleLoop
if not idleTrack or not idleTrack.Length or idleTrack.Length <= 0 then return nil end
-- Length reflects the un-adjusted animation, AdjustSpeed doesn't change it,
-- so this is the authored duration regardless of current playback speed.
local bpm = (beatsPerLoop * 60) / idleTrack.Length
detectedBPM = math.floor(bpm + 0.5)
return detectedBPM
end
-- ============================================================
-- SHARED MUSIC PLAYER (no VC)
-- ============================================================
-- Waits for the companion server script's RemoteEvent. If that server
-- script hasn't been added to the place yet, this just times out and
-- the music section reports "server script missing" instead of erroring.
local function setupMusicRemote()
local ok, remote = pcall(function()
return ReplicatedStorage:WaitForChild("FNFMusicRemote", 8)
end)
if ok and remote then
musicRemote = remote
end
return musicRemote ~= nil
end
local function setMusicStatus(text, color)
if musicStatusLbl then
musicStatusLbl.Text = text
if color then musicStatusLbl.TextColor3 = color end
end
end
-- Watches the actual replicated Sound object (created server-side) to
-- report real load/playback state — this reflects reality for every
-- player, not just an optimistic guess on the requesting client.
local function watchMusicStatus(expectedId)
task.spawn(function()
local myTick = musicRequestTick
local deadline = os.clock() + 8
local snd = nil
while os.clock() < deadline and myTick == musicRequestTick do
snd = SoundService:FindFirstChild(MUSIC_SOUND_NAME)
if snd and snd.SoundId:find(expectedId, 1, true) then break end
task.wait(0.2)
end
if myTick ~= musicRequestTick then return end -- a newer request superseded this one
if not snd or not snd.SoundId:find(expectedId, 1, true) then
setMusicStatus("✖ NO SERVER SCRIPT FOUND", Color3.fromRGB(255,80,80))
return
end
-- give it a moment to load, then check IsLoaded
local loadDeadline = os.clock() + 6
while os.clock() < loadDeadline and myTick == musicRequestTick do
if snd.IsLoaded then break end
task.wait(0.15)
end
if myTick ~= musicRequestTick then return end
if snd.IsLoaded and snd.IsPlaying then
setMusicStatus("✔ PLAYING — allowed in this game", Color3.fromRGB(80,220,120))
elseif snd.IsLoaded then
setMusicStatus("● loaded, not playing", Color3.fromRGB(255,180,60))
else
setMusicStatus("✖ NOT PLAYABLE HERE (blocked/private asset)", Color3.fromRGB(255,80,80))
end
end)
end
local function playMusicTrack(id)
if not musicRemote then
setMusicStatus("✖ NO SERVER SCRIPT FOUND", Color3.fromRGB(255,80,80))
return
end
musicRequestTick = musicRequestTick + 1
setMusicStatus("⏳ requesting...", Color3.fromRGB(0,200,255))
musicRemote:FireServer("play", id)
watchMusicStatus(id)
end
local function stopMusicTrack()
if not musicRemote then return end
musicRequestTick = musicRequestTick + 1
musicRemote:FireServer("stop")
setMusicStatus("● stopped", Color3.fromRGB(160,80,80))
end
-- ============================================================
-- ANIMATION SEQUENCE MAKER
-- ============================================================
local function playSequence()
if seqPlaying or #sequence == 0 or not animator then return end
seqPlaying = true
task.spawn(function()
for _, step in ipairs(sequence) do
if not seqPlaying then break end
if step.id and step.id ~= "" then
destroySeqTrack()
local ok, result = pcall(function()
local anim = Instance.new("Animation")
anim.AnimationId = "rbxassetid://"..step.id
local t = animator:LoadAnimation(anim)
t.Priority = PRIORITY; t.Looped = false
return t
end)
if ok then
seqTrack = result; seqTrack:Play(0)
updatePreviewAnimation(step.id, "SEQUENCE")
end
end
task.wait(math.max(0.01, step.delay or 0.5))
end
destroySeqTrack()
seqPlaying = false
end)
end
local function stopSequence()
seqPlaying = false
destroySeqTrack()
end
-- ============================================================
-- PLAYLIST MANAGER
-- ============================================================
local function playPlaylist()
if playlistPlaying or #playlist == 0 or not animator then return end
playlistPlaying = true
task.spawn(function()
while playlistPlaying do
local step = playlist[playlistIndex]
if step and step.id and step.id ~= "" then
destroySeqTrack()
local ok, result = pcall(function()
local anim = Instance.new("Animation")
anim.AnimationId = "rbxassetid://"..step.id
local t = animator:LoadAnimation(anim)
t.Priority = PRIORITY; t.Looped = false
return t
end)
if ok then
seqTrack = result; seqTrack:Play(0)
updatePreviewAnimation(step.id, step.name or ("Playlist #"..playlistIndex))
end
end
task.wait(math.max(0.05, (step and step.delay) or 1))
if not playlistPlaying then break end
playlistIndex = playlistIndex + 1
if playlistIndex > #playlist then playlistIndex = 1 end
if refreshPlaylistUI then refreshPlaylistUI() end
end
destroySeqTrack()
end)
end
local function stopPlaylist()
playlistPlaying = false
destroySeqTrack()
end
local function nextPlaylistTrack()
if #playlist == 0 then return end
playlistIndex = playlistIndex + 1
if playlistIndex > #playlist then playlistIndex = 1 end
if refreshPlaylistUI then refreshPlaylistUI() end
end
local function prevPlaylistTrack()
if #playlist == 0 then return end
playlistIndex = playlistIndex - 1
if playlistIndex < 1 then playlistIndex = #playlist end
if refreshPlaylistUI then refreshPlaylistUI() end
end
-- ============================================================
-- INPUT HANDLER
-- ============================================================
local lastNoteTime = 0
local DOUBLE_NOTE_WINDOW = 0.08 -- 80 milliseconds to count as a "double note"
local function onInput(input, _gp)
if rebindingRow then
local kn = input.KeyCode and input.KeyCode.Name or "Unknown"
if input.UserInputState == Enum.UserInputState.Begin
and kn ~= "Unknown" and not kn:match("Mouse") then
rebindingRow.key = kn
rebindingRow.keyBtn.Text = kn
rebindingRow.keyBtn.BackgroundColor3 = Color3.fromRGB(30, 20, 45)
rebindingRow = nil
end
return
end
for _, row in ipairs(CurrentBindings) do
if input.KeyCode and input.KeyCode.Name == row.key then
if input.UserInputState == Enum.UserInputState.Begin then
-- Check for double notes
local now = os.clock()
local isDouble = (now - lastNoteTime) <= DOUBLE_NOTE_WINDOW
lastNoteTime = now
-- INTERRUPT: Instantly stop ALL other active directional animations
for k, track in pairs(animTracks) do
if track.IsPlaying then
track:Stop(0) -- 0 second blend-out = instant snap
end
end
-- TRIGGER AFTERIMAGE: If we hit a double note, spawn the ghost
if isDouble then
spawnAfterimage(character)
end
-- Play the new animation
local chosenId = rollBindingAnimId(row)
playAnim(row.key, chosenId, row.speedMult, row.holdLoop)
hitArrow(row.dir)
swapFace(row.dir)
triggerRPMovement()
registerBpmTap()
if refreshBpmDetectorUI then refreshBpmDetectorUI() end
elseif input.UserInputState == Enum.UserInputState.End then
stopAnim(row.key)
releaseArrow(row.dir)
restoreFace()
end
end
end
end
-- ============================================================
-- CHARACTER SETUP
-- ============================================================
local function buildPreviewClone(char)
if not previewCloneContainer then return end
if previewClone then previewClone:Destroy(); previewClone = nil end
-- Archivable must be true for Clone() to work; some games/anti-cheat set it false.
local wasArchivable = char.Archivable
if not wasArchivable then char.Archivable = true end
local ok, clone = pcall(function() return char:Clone() end)
if not wasArchivable then char.Archivable = wasArchivable end
if not ok or not clone then
notify("Preview failed: character isn't clonable right now", "error")
return
end
-- strip scripts so the clone doesn't run game logic
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("Script") or d:IsA("LocalScript") then d:Destroy() end
end
clone.Name = "PreviewClone"
clone.Parent = previewCloneContainer
local hum = clone:FindFirstChildOfClass("Humanoid")
if hum then
hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
previewAnimator = hum:FindFirstChildOfClass("Animator") or Instance.new("Animator", hum)
else
notify("Preview clone has no Humanoid — can't animate it", "warn")
end
-- Anchor ONLY the root part. ViewportFrame contents aren't part of the
-- main physics simulation, so unanchored limbs won't fall — but they DO
-- need to stay unanchored for the Animator's Motor6D joints to pose them.
-- Anchoring every part (as an earlier version of this script did) freezes
-- each limb independently and blocks animations from moving anything.
local root = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
if root then root.Anchored = true end
local okPivot = pcall(function()
clone:PivotTo(CFrame.new(0, 3, 0))
end)
if not okPivot then
local hrp = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
if hrp then clone:SetPrimaryPartCFrame(CFrame.new(0, 3, 0)) end
end
previewClone = clone
end
local function setupCharacter(char)
cleanupAll(); character = char
pcall(function() character:WaitForChild("HumanoidRootPart", 10) end)
animator = getAnimator(char)
if not animator then return end
initFace(char)
if legsLocked then applyLegsLock(true) end
table.insert(connections, UserInputService.InputBegan:Connect(onInput))
table.insert(connections, UserInputService.InputEnded:Connect(onInput))
local hum = char:FindFirstChildOfClass("Humanoid")
if hum then table.insert(connections, hum.Died:Connect(cleanupAll)) end
task.defer(buildPreviewClone, char)
log("Character ready.")
notify("Character loaded — ready to inject", "success")
end
if LocalPlayer.Character then task.spawn(setupCharacter, LocalPlayer.Character) end
LocalPlayer.CharacterAdded:Connect(function(c) task.spawn(setupCharacter, c) end)
-- ============================================================
-- NEARBY PLAYER ANIM SCANNER
-- Reads whatever animation IDs are currently playing on other
-- players' characters. This only reads data Roblox already
-- replicates to every client (the same way you see other
-- players' emotes/moves play out in real time) — it can't see
-- anything that isn't actively playing right now.
-- ============================================================
local function scanPlayingAnimations()
local found = {}
for _, plr in ipairs(Players:GetPlayers()) do
if plr ~= LocalPlayer and plr.Character then
local hum = plr.Character:FindFirstChildOfClass("Humanoid")
local anr = hum and hum:FindFirstChildOfClass("Animator")
if anr then
for _, track in ipairs(anr:GetPlayingAnimationTracks()) do
local animObj = track.Animation
if animObj and animObj.AnimationId ~= "" then
local id = animObj.AnimationId:match("%d+") or ""
if id ~= "" then
table.insert(found, { player = plr.Name, id = id })
end
end
end
end
end
end
return found
end
local function uiCorner(p, r)
local c = Instance.new("UICorner"); c.CornerRadius = r or UDim.new(0,8); c.Parent = p
end
local function uiStroke(p, col, th)
local s = Instance.new("UIStroke"); s.Color = col; s.Thickness = th or 1.5; s.Parent = p; return s
end
local function makeLabel(parent, text, pos, size, color, xAlign, ts)
local l = Instance.new("TextLabel")
l.Size = size; l.Position = pos; l.BackgroundTransparency = 1
l.Text = text; l.TextColor3 = color or Color3.fromRGB(180,180,180)
l.TextSize = ts or 11; l.Font = Enum.Font.GothamBold
l.TextXAlignment = xAlign or Enum.TextXAlignment.Center
l.Parent = parent; return l
end
local function makeBtn(parent, text, pos, size, bg, tc, ts)
local b = Instance.new("TextButton")
b.Size = size; b.Position = pos; b.BackgroundColor3 = bg
b.Text = text; b.TextColor3 = tc or Color3.fromRGB(255,255,255)
b.Font = Enum.Font.GothamBold; b.TextSize = ts or 12
b.BorderSizePixel = 0; b.Parent = parent; b.AutoButtonColor = false
uiCorner(b)
-- hover / glow behaviour
local baseSize = size
local hoverGlow = uiStroke(b, Color3.fromRGB(255,255,255), 0)
hoverGlow.Transparency = 1
b.MouseEnter:Connect(function()
TweenService:Create(b, TweenInfo.new(0.12, Enum.EasingStyle.Quad), {
Size = UDim2.new(baseSize.X.Scale, baseSize.X.Offset + 4, baseSize.Y.Scale, baseSize.Y.Offset + 2)
}):Play()
TweenService:Create(hoverGlow, TweenInfo.new(0.12), { Thickness = 2, Transparency = 0.2 }):Play()
end)
b.MouseLeave:Connect(function()
TweenService:Create(b, TweenInfo.new(0.15, Enum.EasingStyle.Quad), { Size = baseSize }):Play()
TweenService:Create(hoverGlow, TweenInfo.new(0.15), { Thickness = 0, Transparency = 1 }):Play()
end)
b.MouseButton1Down:Connect(function()
TweenService:Create(b, TweenInfo.new(0.06), { Size = UDim2.new(baseSize.X.Scale, baseSize.X.Offset - 3, baseSize.Y.Scale, baseSize.Y.Offset - 3) }):Play()
end)
b.MouseButton1Up:Connect(function()
TweenService:Create(b, TweenInfo.new(0.08), { Size = baseSize }):Play()
end)
return b
end
local function makeDivider(parent, yPos, color)
local d = Instance.new("Frame"); d.Size = UDim2.new(1,-16,0,1)
d.Position = UDim2.new(0,8,0,yPos)
d.BackgroundColor3 = color or Color3.fromRGB(0,200,255)
d.BorderSizePixel = 0; d.Parent = parent
return d
end
local function makeBox(parent, pos, size, placeholder, default)
local b = Instance.new("TextBox")
b.Size = size; b.Position = pos
b.BackgroundColor3 = Color3.fromRGB(22,22,32)
b.PlaceholderText = placeholder or ""; b.PlaceholderColor3 = Color3.fromRGB(90,90,110)
b.Text = default or ""; b.TextColor3 = Color3.fromRGB(255,255,255)
b.ClearTextOnFocus = false; b.Font = Enum.Font.Code; b.TextSize = 11
b.Parent = parent; uiCorner(b); return b
end
-- Draggable slider: track + fill + handle. Works with mouse and touch.
local function makeSlider(parent, pos, size, minVal, maxVal, default, onChange)
local track = Instance.new("Frame")
track.Size = size; track.Position = pos
track.BackgroundColor3 = Color3.fromRGB(28,28,42)
track.BorderSizePixel = 0; track.Active = true
track.Parent = parent; uiCorner(track, UDim.new(0,6))
local fill = Instance.new("Frame")
fill.BackgroundColor3 = Color3.fromRGB(255,80,180)
fill.BorderSizePixel = 0; fill.Size = UDim2.new(0,0,1,0)
fill.Parent = track; uiCorner(fill, UDim.new(0,6))
regBg(fill)
local handle = Instance.new("Frame")
handle.Size = UDim2.new(0,14,0,14)
handle.AnchorPoint = Vector2.new(0.5,0.5)
handle.Position = UDim2.new(0,0,0.5,0)
handle.BackgroundColor3 = Color3.fromRGB(255,255,255)
handle.BorderSizePixel = 0; handle.ZIndex = 2
handle.Parent = track; uiCorner(handle, UDim.new(1,0))
local hStroke = uiStroke(handle, Color3.fromRGB(255,80,180), 2)
regStroke(hStroke)
local value = default
local dragging = false
local function setValue(v, fire)
v = math.clamp(v, minVal, maxVal)
value = v
local pct = (maxVal > minVal) and (v - minVal) / (maxVal - minVal) or 0
fill.Size = UDim2.new(pct, 0, 1, 0)
handle.Position = UDim2.new(pct, 0, 0.5, 0)
if fire and onChange then onChange(v) end
end
local function updateFromX(x)
local abs = track.AbsolutePosition.X
local w = track.AbsoluteSize.X
if w <= 0 then return end
local pct = math.clamp((x - abs) / w, 0, 1)
setValue(minVal + pct * (maxVal - minVal), true)
end
local function tryBegin(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging = true
updateFromX(input.Position.X)
end
end
track.InputBegan:Connect(tryBegin)
handle.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging = true
end
end)
UserInputService.InputChanged:Connect(function(input)
if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement
or input.UserInputType == Enum.UserInputType.Touch) then
updateFromX(input.Position.X)
end
end)
UserInputService.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging = false
end
end)
setValue(default, false)
return { track = track, fill = fill, handle = handle, setValue = setValue, getValue = function() return value end }
end
-- ============================================================
-- PER-BINDING ALT-ANIMATION LIST UI HELPER
-- ============================================================
-- Renders the current alt-animation entries for one binding row into a
-- given ScrollingFrame, with a remove button per entry and a running
-- total % footer. Self-referencing so removal can re-render in place.
local function refreshRowAltList(row, listFrame)
for _, c in ipairs(listFrame:GetChildren()) do
if c:IsA("Frame") or c:IsA("TextLabel") then c:Destroy() end
end
for j, alt in ipairs(row.alts) do
local entry = Instance.new("Frame")
entry.Size = UDim2.new(1,-4,0,20)
entry.BackgroundColor3 = Color3.fromRGB(24,24,36)
entry.BorderSizePixel = 0
entry.LayoutOrder = j
entry.Parent = listFrame
uiCorner(entry, UDim.new(0,4))
makeLabel(entry, string.format("⭐ %s — %d%%", alt.id, alt.chance),
UDim2.new(0,6,0,0), UDim2.new(1,-28,1,0), Color3.fromRGB(200,200,215), Enum.TextXAlignment.Left, 9)
local rem = makeBtn(entry, "✕", UDim2.new(1,-22,0,0), UDim2.new(0,20,0,20), Color3.fromRGB(170,35,35), nil, 9)
rem.MouseButton1Click:Connect(function()
table.remove(row.alts, j)
refreshRowAltList(row, listFrame)
end)
end
local total = 0
for _, a in ipairs(row.alts) do total += a.chance end
local footer = makeLabel(listFrame, string.format("Total: %d%% (remainder = primary anim)", total),
UDim2.new(1,-4,0,14), UDim2.new(1,-4,0,14),
(total > 100) and Color3.fromRGB(255,80,80) or Color3.fromRGB(140,140,160),
Enum.TextXAlignment.Right, 9)
footer.LayoutOrder = 9999
end
-- ============================================================
-- ARROW HUD BUILD
-- ============================================================
local function buildArrowHud()
local old = PlayerGui:FindFirstChild("FNFArrowHud")
if old then old:Destroy() end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFArrowHud"; sg.ResetOnSpawn = false
sg.ZIndexBehavior = Enum.ZIndexBehavior.Sibling; sg.Parent = PlayerGui
local totalW = #ARROW_ORDER * ARROW_SIZE + (#ARROW_ORDER - 1) * ARROW_GAP
local bottomY = 50 -- px from bottom
for i, dir in ipairs(ARROW_ORDER) do
local colors = ARROW_COLORS[dir]
local cx = -totalW/2 + (i-1)*(ARROW_SIZE + ARROW_GAP) + ARROW_SIZE/2
local cy = -(ARROW_SIZE/2 + bottomY)
local bg = Instance.new("Frame")
bg.AnchorPoint = Vector2.new(0.5, 0.5)
bg.Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE)
bg.Position = UDim2.new(0.5, cx, 1, cy)
bg.BackgroundColor3 = colors.idle
bg.BorderSizePixel = 0; bg.Parent = sg
uiCorner(bg, UDim.new(0, 10))
local stroke = uiStroke(bg, Color3.fromRGB(255,255,255), 2)
local glow = Instance.new("Frame")
glow.Size = UDim2.new(1,0,1,0); glow.BackgroundTransparency = 0.7
glow.BackgroundColor3 = colors.hit; glow.BorderSizePixel = 0; glow.Parent = bg
uiCorner(glow, UDim.new(0,10))
glow.Visible = false
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.new(1,0,1,0); lbl.BackgroundTransparency = 1
lbl.Text = ARROW_SYMBOLS[dir]
lbl.TextColor3 = Color3.fromRGB(255,255,255)
lbl.TextSize = 26; lbl.Font = Enum.Font.GothamBold; lbl.Parent = bg
arrowHudFrames[dir] = { bg = bg, lbl = lbl, stroke = stroke, glow = glow }
end
end
-- ============================================================
-- MIC SPECTROGRAM SETUP
-- ============================================================
-- Tries to wire up Roblox's Audio API (AudioDeviceInput -> AudioAnalyzer)
-- so the spectrogram can react to real microphone input. This part of the
-- engine is newer and can vary by client/game settings, so every step is
-- pcall-guarded. If anything fails, micAvailable stays false and the
-- spectrogram loop below falls back to reacting to the animation/BPM state
-- instead (same as the equalizer), so the widget still looks alive.
local function setupMicSpectrogram()
micAvailable = false
local ok = pcall(function()
micDeviceInput = Instance.new("AudioDeviceInput")
micDeviceInput.Parent = LocalPlayer
micAnalyzer = Instance.new("AudioAnalyzer")
micAnalyzer.WindowSize = 1024
micAnalyzer.Parent = LocalPlayer
micWire = Instance.new("Wire")
micWire.SourceInstance = micDeviceInput
micWire.TargetInstance = micAnalyzer
micWire.Parent = micAnalyzer
micDeviceInput.Active = true
end)
micAvailable = ok and micDeviceInput ~= nil and micAnalyzer ~= nil
if not micAvailable then
log("Mic spectrogram unavailable on this client/game — using animation-synced fallback")
end
return micAvailable
end
-- ============================================================
-- BEAT / VISUALIZER LOOPS (equalizer, waveform, floating notes, beat pulse, particles)
-- ============================================================
local eqBars, waveBars, beatDot, notesLayer, particleLayer = nil, nil, nil, nil, nil
local function startVisualizerLoops()
-- Equalizer bars: random-ish heights, faster while playing
task.spawn(function()
while true do
local speed = StatusState.playing and 0.08 or 0.22
task.wait(speed)
if eqBars then
for _, bar in ipairs(eqBars) do
local h = StatusState.playing and (0.25 + math.random() * 0.75) or (0.08 + math.random() * 0.2)
TweenService:Create(bar, TweenInfo.new(speed, Enum.EasingStyle.Sine), {
Size = UDim2.new(1, 0, h, 0)
}):Play()
end
end
end
end)
-- BPM waveform: sine ripple scaled to bpm
task.spawn(function()
local t = 0
while true do
RunService.Heartbeat:Wait()
t = t + (StatusState.bpm / 60) * 0.03
if waveBars then
for i, bar in ipairs(waveBars) do
local h = 0.15 + 0.35 * (0.5 + 0.5 * math.sin(t + i * 0.6))
bar.Size = UDim2.new(1, 0, h, 0)
end
end
end
end)
-- Beat sync pulse: fires exactly on the beat interval
task.spawn(function()
while true do
local interval = 60 / math.max(1, StatusState.bpm)
task.wait(interval)
if beatDot then
beatDot.Size = UDim2.new(0, 22, 0, 22)
TweenService:Create(beatDot, TweenInfo.new(interval * 0.85, Enum.EasingStyle.Quad), {
Size = UDim2.new(0, 10, 0, 10)
}):Play()
end
end
end)
-- Floating music notes
task.spawn(function()
local symbols = { "♪", "♫", "♬", "♩" }
while true do
task.wait(0.7 + math.random() * 0.8)
if notesLayer then
local n = Instance.new("TextLabel")
n.Size = UDim2.new(0, 20, 0, 20)
n.Position = UDim2.new(math.random(), 0, 1, 0)
n.BackgroundTransparency = 1
n.Text = symbols[math.random(1, #symbols)]
n.TextColor3 = THEMES[currentThemeName].accent
n.TextTransparency = 0.2
n.Font = Enum.Font.GothamBold
n.TextSize = 14 + math.random(0, 10)
n.Parent = notesLayer
regText(n)
local tw = TweenService:Create(n, TweenInfo.new(3 + math.random()*2, Enum.EasingStyle.Linear), {
Position = UDim2.new(n.Position.X.Scale, 0, -0.15, 0),
TextTransparency = 1
})
tw:Play()
tw.Completed:Connect(function() n:Destroy() end)
end
end
end)
-- Ambient particles
task.spawn(function()
while true do
task.wait(0.4 + math.random() * 0.5)
if particleLayer then
local p = Instance.new("Frame")
local sz = math.random(2, 4)
p.Size = UDim2.new(0, sz, 0, sz)
p.Position = UDim2.new(math.random(), 0, 1, 0)
p.BackgroundColor3 = THEMES[currentThemeName].accent2
p.BackgroundTransparency = 0.3
p.BorderSizePixel = 0
p.Parent = particleLayer
uiCorner(p, UDim.new(1,0))
local tw = TweenService:Create(p, TweenInfo.new(1.5 + math.random(), Enum.EasingStyle.Sine), {
Position = UDim2.new(p.Position.X.Scale + (math.random()-0.5)*0.1, 0, -0.05, 0),
BackgroundTransparency = 1
})
tw:Play()
tw.Completed:Connect(function() p:Destroy() end)
end
end
end)
-- Vinyl record spin: RPM scales with whatever's currently playing —
-- directional anim speedMult if one is active, otherwise idle BPM ratio.
task.spawn(function()
while true do
RunService.Heartbeat:Wait()
if vinylImage then
local rpm
if StatusState.playing and StatusState.speedMult then
rpm = vinylBaseRPM * StatusState.speedMult
else
rpm = vinylBaseRPM * idleSpeedFromBPM()
end
vinylRotation = (vinylRotation + (rpm / 60) * 360 * (1/60)) % 360
vinylImage.Rotation = vinylRotation
end
end
end)
-- Spectrogram: real mic spectrum if available, otherwise an
-- animation/BPM-synced fallback so the bars are never just dead.
task.spawn(function()
while true do
task.wait(0.05)
if #specBars > 0 then
if micAvailable and micAnalyzer then
local ok, spectrum = pcall(function() return micAnalyzer:GetSpectrum() end)
if ok and spectrum then
local n = #spectrum
for i, bar in ipairs(specBars) do
local idx = math.max(1, math.floor((i / #specBars) * n))
local mag = spectrum[idx] or 0
local h = math.clamp(mag * 2, 0.05, 1)
TweenService:Create(bar, TweenInfo.new(0.05), { Size = UDim2.new(1,0,h,0) }):Play()
end
else
micAvailable = false -- analyzer stopped working mid-session, fall back
end
else
for i, bar in ipairs(specBars) do
local base = StatusState.playing and 0.3 or 0.08
local h = base + math.random() * (StatusState.playing and 0.6 or 0.15)
TweenService:Create(bar, TweenInfo.new(0.05), { Size = UDim2.new(1,0,h,0) }):Play()
end
end
end
end
end)
-- CRT flicker: tiny random transparency jitter on the scanline overlay
task.spawn(function()
while true do
task.wait(0.08 + math.random() * 0.15)
if crtEnabled and crtOverlay then
crtOverlay.BackgroundTransparency = 0.82 + math.random() * 0.08
end
end
end)
-- (preview camera orbit loop lives in buildGUI, started once previewCam exists)
end
-- ============================================================
-- MAIN GUI BUILD
-- ============================================================
local function buildGUI()
local old = PlayerGui:FindFirstChild("FNFInjectorGui")
if old then old:Destroy() end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFInjectorGui"
sg.ResetOnSpawn = false
sg.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
sg.Parent = PlayerGui
-- Notification stack (top-right of screen, independent of main panel)
notifyContainer = Instance.new("Frame")
notifyContainer.Size = UDim2.new(0, 260, 1, -20)
notifyContainer.Position = UDim2.new(1, -270, 0, 10)
notifyContainer.BackgroundTransparency = 1
notifyContainer.Parent = sg
local notifyLayout = Instance.new("UIListLayout")
notifyLayout.SortOrder = Enum.SortOrder.LayoutOrder
notifyLayout.Padding = UDim.new(0, 6)
notifyLayout.Parent = notifyContainer
local MAIN_W, MAIN_H = 430, 600
local main = Instance.new("Frame")
main.Name = "Main"; main.Size = UDim2.new(0,MAIN_W,0,MAIN_H)
main.Position = UDim2.new(0,60,0,60)
main.BackgroundColor3 = Color3.fromRGB(13,13,18)
main.BorderSizePixel = 0; main.Active = true; main.Draggable = true
main.Parent = sg; uiCorner(main, UDim.new(0,10))
local mainStroke = uiStroke(main, Color3.fromRGB(255,80,180), 2)
regStroke(mainStroke)
-- decorative background particle / notes layers, clipped to main panel
particleLayer = Instance.new("Frame")
particleLayer.Size = UDim2.new(1,0,1,0); particleLayer.BackgroundTransparency = 1
particleLayer.ClipsDescendants = true; particleLayer.ZIndex = 0; particleLayer.Parent = main
notesLayer = Instance.new("Frame")
notesLayer.Size = UDim2.new(1,0,1,0); notesLayer.BackgroundTransparency = 1
notesLayer.ClipsDescendants = true; notesLayer.ZIndex = 0; notesLayer.Parent = main
-- CRT overlay: green tint + scanlines, sits above everything else in
-- the panel but ignores input so it never blocks clicks. Hidden by
-- default; toggled from the FX tab.
crtOverlay = Instance.new("Frame")
crtOverlay.Size = UDim2.new(1,0,1,0); crtOverlay.BackgroundColor3 = Color3.fromRGB(20,255,120)
crtOverlay.BackgroundTransparency = 0.88
crtOverlay.BorderSizePixel = 0; crtOverlay.ZIndex = 50
crtOverlay.Visible = false
crtOverlay.Parent = main
local crtOverlayCorner = Instance.new("UICorner"); crtOverlayCorner.CornerRadius = UDim.new(0,10); crtOverlayCorner.Parent = crtOverlay
local scanlineHolder = Instance.new("Frame")
scanlineHolder.Size = UDim2.new(1,0,1,0); scanlineHolder.BackgroundTransparency = 1
scanlineHolder.ClipsDescendants = true; scanlineHolder.ZIndex = 51
scanlineHolder.Parent = crtOverlay
local SCANLINE_GAP = 4
for y = 0, 700, SCANLINE_GAP do
local line = Instance.new("Frame")
line.Size = UDim2.new(1,0,0,1)
line.Position = UDim2.new(0,0,0,y)
line.BackgroundColor3 = Color3.fromRGB(0,0,0)
line.BackgroundTransparency = 0.55
line.BorderSizePixel = 0
line.ZIndex = 51
line.Parent = scanlineHolder
end
-- Title bar
local titleBar = Instance.new("Frame")
titleBar.Size = UDim2.new(1,0,0,36); titleBar.BackgroundColor3 = Color3.fromRGB(22,8,32)
titleBar.BorderSizePixel = 0; titleBar.Parent = main; uiCorner(titleBar, UDim.new(0,10))
local titleLbl = makeLabel(titleBar, "🎵 FNF ANIM INJECTOR v6.1",
UDim2.new(0,12,0,0), UDim2.new(1,-80,1,0),
Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 13)
regText(titleLbl)
local toggleBtn = makeBtn(titleBar, "HIDE",
UDim2.new(1,-68,0,5), UDim2.new(0,60,0,26), Color3.fromRGB(255,80,180))
regBg(toggleBtn)
-- Tab bar
local TAB_H = 32
local tabBar = Instance.new("Frame")
tabBar.Size = UDim2.new(1,0,0,TAB_H); tabBar.Position = UDim2.new(0,0,0,36)
tabBar.BackgroundColor3 = Color3.fromRGB(18,8,28)
tabBar.BorderSizePixel = 0; tabBar.Parent = main
-- Content area
local contentArea = Instance.new("Frame")
contentArea.Size = UDim2.new(1,0,1,-(36+TAB_H))
contentArea.Position = UDim2.new(0,0,0,36+TAB_H)
contentArea.BackgroundTransparency = 1; contentArea.Parent = main
-- ── Tabs ──
local tabNames = { "📋 BINDS", "🎬 MAKER", "👁 PREVIEW", "❤ FAVES", "🎶 LIST", "🎛 FX", "⚙ SETTINGS" }
local pages = {}
local tabButtons = {}
local tabW = math.floor(MAIN_W / #tabNames)
for i, name in ipairs(tabNames) do
local page = Instance.new("ScrollingFrame")
page.Size = UDim2.new(1,0,1,0); page.BackgroundTransparency = 1
page.ScrollBarThickness = 4
page.CanvasSize = UDim2.new(0,0,0,0)
page.AutomaticCanvasSize = Enum.AutomaticSize.Y
page.Visible = (i == 1); page.Parent = contentArea
pages[name] = page
local tb = makeBtn(tabBar, name,
UDim2.new(0,(i-1)*tabW,0,0), UDim2.new(0,tabW,1,0),
i == 1 and Color3.fromRGB(255,80,180) or Color3.fromRGB(28,12,42), nil, 9)
tabButtons[name] = tb
if i == 1 then regBg(tb) end
tb.MouseButton1Click:Connect(function()
for n, p in pairs(pages) do
p.Visible = (n == name)
end
for n, btn in pairs(tabButtons) do
local isActive = (n == name)
btn.BackgroundColor3 = isActive and THEMES[currentThemeName].accent or Color3.fromRGB(28,12,42)
-- swap registration so theme/RGB cycling keeps following the active tab
for j = #themedBgs, 1, -1 do if themedBgs[j] == btn then table.remove(themedBgs, j) end end
if isActive then regBg(btn) end
end
end)
end
-- ============================================================
-- TAB 1 : BINDS + IDLE + BPM
-- ============================================================
local pg1 = pages["📋 BINDS"]
makeLabel(pg1, "DIR", UDim2.new(0,8,0,6), UDim2.new(0,36,0,20), Color3.fromRGB(255,80,180))
makeLabel(pg1, "KEY", UDim2.new(0,48,0,6), UDim2.new(0,62,0,20), Color3.fromRGB(255,80,180))
makeLabel(pg1, "ANIM ID", UDim2.new(0,116,0,6), UDim2.new(0,180,0,20),Color3.fromRGB(255,80,180))
makeLabel(pg1, "TEST", UDim2.new(0,308,0,6), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180))
makeDivider(pg1, 28, Color3.fromRGB(0,200,255))
-- ROW_H covers: dir/key/id/test line, speed slider line, hold-loop +
-- alt-input line, and a small scrollable alt list — per binding row.
local ROW_H = 150
local startY = 36
for i, row in ipairs(CurrentBindings) do
local y = startY + (i-1)*ROW_H
local dirColor = ARROW_COLORS[row.dir] and ARROW_COLORS[row.dir].hit or Color3.fromRGB(255,255,255)
makeLabel(pg1, row.dir, UDim2.new(0,8,0,y), UDim2.new(0,36,0,28), dirColor)
local keyBtn = makeBtn(pg1, row.key, UDim2.new(0,48,0,y), UDim2.new(0,60,0,28), Color3.fromRGB(25,35,48))
row.keyBtn = keyBtn
keyBtn.MouseButton1Click:Connect(function()
rebindingRow = row; keyBtn.Text = "..."
keyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
end)
local idBox = makeBox(pg1, UDim2.new(0,116,0,y), UDim2.new(0,180,0,28), "anim id", row.id)
idBox.FocusLost:Connect(function()
local n = idBox.Text:gsub("%D","")
if n ~= "" then row.id = n; destroyTrack(row.key) else idBox.Text = row.id end
end)
local testBtn = makeBtn(pg1, "▶ TEST", UDim2.new(0,306,0,y), UDim2.new(0,90,0,28), Color3.fromRGB(0,180,230), nil, 11)
testBtn.MouseButton1Down:Connect(function()
local chosenId = rollBindingAnimId(row)
playAnim(row.key, chosenId, row.speedMult, row.holdLoop); hitArrow(row.dir); swapFace(row.dir)
end)
testBtn.MouseButton1Up:Connect(function() stopAnim(row.key); releaseArrow(row.dir); restoreFace() end)
local sy = y + 30
makeLabel(pg1, "SPD", UDim2.new(0,8,0,sy), UDim2.new(0,32,0,22), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local spdValLbl = makeLabel(pg1, string.format("x%.2f", row.speedMult),
UDim2.new(0,280,0,sy), UDim2.new(0,112,0,22), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
regText(spdValLbl)
makeSlider(pg1, UDim2.new(0,42,0,sy+2), UDim2.new(0,232,0,14), 0.25, 3, row.speedMult, function(v)
row.speedMult = v
spdValLbl.Text = string.format("x%.2f", v)
if StatusState.currentAnimName == row.key then
StatusState.speedMult = v
if refreshStatusUI then refreshStatusUI() end
end
end)
-- ── HOLD-LOOP toggle + alt-animation entry inputs ──
local sy2 = y + 58
local holdBtn = makeBtn(pg1, "🔁 HOLD-LOOP: OFF", UDim2.new(0,8,0,sy2), UDim2.new(0,140,0,24), Color3.fromRGB(35,35,55), nil, 9)
holdBtn.MouseButton1Click:Connect(function()
row.holdLoop = not row.holdLoop
holdBtn.Text = row.holdLoop and "🔁 HOLD-LOOP: ON" or "🔁 HOLD-LOOP: OFF"
holdBtn.BackgroundColor3 = row.holdLoop and Color3.fromRGB(0,180,230) or Color3.fromRGB(35,35,55)
end)
local altIdBox = makeBox(pg1, UDim2.new(0,154,0,sy2), UDim2.new(0,142,0,24), "alt anim id")
local altPctBox = makeBox(pg1, UDim2.new(0,300,0,sy2), UDim2.new(0,52,0,24), "%")
local addAltBtn = makeBtn(pg1, "+", UDim2.new(0,356,0,sy2), UDim2.new(0,40,0,24), Color3.fromRGB(60,190,120), nil, 12)
-- ── mini scrolling list of this row's configured alts ──
local sy3 = y + 86
local altListFrame = Instance.new("ScrollingFrame")
altListFrame.Size = UDim2.new(1,-16,0,56)
altListFrame.Position = UDim2.new(0,8,0,sy3)
altListFrame.BackgroundColor3 = Color3.fromRGB(18,18,26)
altListFrame.BorderSizePixel = 0
altListFrame.ScrollBarThickness = 3
altListFrame.CanvasSize = UDim2.new(0,0,0,0)
altListFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
altListFrame.Parent = pg1
uiCorner(altListFrame, UDim.new(0,6))
local altListLayout = Instance.new("UIListLayout")
altListLayout.Padding = UDim.new(0,2)
altListLayout.Parent = altListFrame
local altListPad = Instance.new("UIPadding")
altListPad.PaddingTop = UDim.new(0,2); altListPad.PaddingLeft = UDim.new(0,2); altListPad.PaddingRight = UDim.new(0,2)
altListPad.Parent = altListFrame
addAltBtn.MouseButton1Click:Connect(function()
local id = altIdBox.Text:gsub("%D","")
local chance = tonumber(altPctBox.Text)
if id == "" or not chance then notify("Enter an alt anim ID + % chance", "warn"); return end
table.insert(row.alts, { id = id, chance = math.clamp(chance, 0, 100) })
altIdBox.Text = ""; altPctBox.Text = ""
refreshRowAltList(row, altListFrame)
end)
refreshRowAltList(row, altListFrame)
end
-- Idle section
local idleY = startY + #CurrentBindings * ROW_H + 6
makeDivider(pg1, idleY, Color3.fromRGB(255,80,180))
local idleTitleLbl = makeLabel(pg1, "🎵 IDLE ANIMATION",
UDim2.new(0,8,0,idleY+6), UDim2.new(0,200,0,20),
Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left)
regText(idleTitleLbl)
local statusLbl = makeLabel(pg1, "● OFF",
UDim2.new(0,200,0,idleY+6), UDim2.new(0,196,0,20),
Color3.fromRGB(160,80,80), Enum.TextXAlignment.Right)
local idleIdBox = makeBox(pg1, UDim2.new(0,8,0,idleY+30), UDim2.new(1,-16,0,28), "Paste idle anim ID...")
local loopBtn = makeBtn(pg1, "⟳ LOOP", UDim2.new(0,8,0,idleY+64), UDim2.new(0,120,0,28), Color3.fromRGB(70,120,255))
local onceBtn = makeBtn(pg1, "▶ ONCE", UDim2.new(0,136,0,idleY+64), UDim2.new(0,120,0,28), Color3.fromRGB(60,190,120))
local idleStop = makeBtn(pg1, "■ STOP", UDim2.new(0,264,0,idleY+64), UDim2.new(0,130,0,28), Color3.fromRGB(190,50,50))
local function getIdleId() return idleIdBox.Text:gsub("%D","") end
loopBtn.MouseButton1Click:Connect(function()
local id = getIdleId(); if id == "" then notify("Enter an idle anim ID first", "warn"); return end
playIdleLoop(id); statusLbl.Text = "● LOOPING"; statusLbl.TextColor3 = Color3.fromRGB(80,220,120)
notify("Idle looping started", "success")
end)
onceBtn.MouseButton1Click:Connect(function()
local id = getIdleId(); if id == "" then notify("Enter an idle anim ID first", "warn"); return end
playIdleOnce(id, function()
if not idleLooping then statusLbl.Text = "● OFF"; statusLbl.TextColor3 = Color3.fromRGB(160,80,80) end
end)
statusLbl.Text = "▶ PLAYING"; statusLbl.TextColor3 = Color3.fromRGB(0,200,255)
end)
idleStop.MouseButton1Click:Connect(function()
stopIdle(); statusLbl.Text = "● OFF"; statusLbl.TextColor3 = Color3.fromRGB(160,80,80)
end)
-- BPM controls
local bpmY = idleY + 98
makeLabel(pg1, "BPM", UDim2.new(0,8,0,bpmY), UDim2.new(0,38,0,28),
Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
local bpmBox = makeBox(pg1, UDim2.new(0,44,0,bpmY), UDim2.new(0,68,0,28), "100", tostring(idleBPM))
makeLabel(pg1, "BASE", UDim2.new(0,120,0,bpmY), UDim2.new(0,44,0,28),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local baseBox = makeBox(pg1, UDim2.new(0,164,0,bpmY), UDim2.new(0,68,0,28), "100", tostring(BASE_BPM))
local applyBpmBtn = makeBtn(pg1, "✓ APPLY", UDim2.new(0,240,0,bpmY), UDim2.new(0,154,0,28), Color3.fromRGB(70,120,255), nil, 11)
local bpmSliderY = bpmY + 36
makeLabel(pg1, "DRAG", UDim2.new(0,8,0,bpmSliderY), UDim2.new(0,50,0,22), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local bpmValLbl = makeLabel(pg1, string.format("%d BPM", idleBPM),
UDim2.new(0,300,0,bpmSliderY), UDim2.new(0,94,0,22), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 11)
regText(bpmValLbl)
local bpmSlider = makeSlider(pg1, UDim2.new(0,62,0,bpmSliderY+2), UDim2.new(0,232,0,14), 40, 300, idleBPM, function(v)
v = math.floor(v + 0.5)
setIdleBPM(v)
bpmBox.Text = tostring(v)
bpmValLbl.Text = string.format("%d BPM", v)
end)
bpmBox.FocusLost:Connect(function()
local n = tonumber(bpmBox.Text)
if n and n > 0 then
idleBPM = n
bpmSlider.setValue(n, false)
bpmValLbl.Text = string.format("%d BPM", n)
setIdleBPM(n)
else
bpmBox.Text = tostring(idleBPM)
end
end)
baseBox.FocusLost:Connect(function()
local n = tonumber(baseBox.Text)
if n and n > 0 then BASE_BPM = n else baseBox.Text = tostring(BASE_BPM) end
setIdleBPM(idleBPM)
end)
applyBpmBtn.MouseButton1Click:Connect(function()
local n = tonumber(bpmBox.Text)
local b = tonumber(baseBox.Text)
if n and n > 0 then idleBPM = n; bpmSlider.setValue(n, false); bpmValLbl.Text = string.format("%d BPM", n) end
if b and b > 0 then BASE_BPM = b end
setIdleBPM(idleBPM)
notify("BPM settings applied", "info")
end)
-- Beat-sync indicator + mini equalizer, docked at bottom of binds tab
local vizY = bpmSliderY + 34
makeDivider(pg1, vizY, Color3.fromRGB(0,200,255))
makeLabel(pg1, "BEAT SYNC", UDim2.new(0,8,0,vizY+6), UDim2.new(0,90,0,18), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
beatDot = Instance.new("Frame")
beatDot.AnchorPoint = Vector2.new(0.5,0.5)
beatDot.Size = UDim2.new(0,10,0,10)
beatDot.Position = UDim2.new(0,100,0,vizY+15)
beatDot.BackgroundColor3 = Color3.fromRGB(255,80,180)
beatDot.BorderSizePixel = 0; beatDot.Parent = pg1
uiCorner(beatDot, UDim.new(1,0))
regBg(beatDot)
makeLabel(pg1, "EQUALIZER", UDim2.new(0,140,0,vizY+6), UDim2.new(0,100,0,18), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local eqHolder = Instance.new("Frame")
eqHolder.Size = UDim2.new(0,120,0,26); eqHolder.Position = UDim2.new(0,140,0,vizY+22)
eqHolder.BackgroundTransparency = 1; eqHolder.Parent = pg1
local eqLayout = Instance.new("UIListLayout")
eqLayout.FillDirection = Enum.FillDirection.Horizontal
eqLayout.Padding = UDim.new(0,3)
eqLayout.VerticalAlignment = Enum.VerticalAlignment.Bottom
eqLayout.Parent = eqHolder
eqBars = {}
for i = 1, 8 do
local bar = Instance.new("Frame")
bar.Size = UDim2.new(0, 10, 0.2, 0)
bar.BackgroundColor3 = THEMES[currentThemeName].accent
bar.BorderSizePixel = 0
bar.Parent = eqHolder
uiCorner(bar, UDim.new(0,2))
regBg(bar)
table.insert(eqBars, bar)
end
makeLabel(pg1, "WAVEFORM", UDim2.new(0,270,0,vizY+6), UDim2.new(0,120,0,18), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local waveHolder = Instance.new("Frame")
waveHolder.Size = UDim2.new(0,130,0,26); waveHolder.Position = UDim2.new(0,270,0,vizY+22)
waveHolder.BackgroundTransparency = 1; waveHolder.Parent = pg1
local waveLayout = Instance.new("UIListLayout")
waveLayout.FillDirection = Enum.FillDirection.Horizontal
waveLayout.Padding = UDim.new(0,2)
waveLayout.VerticalAlignment = Enum.VerticalAlignment.Center
waveLayout.Parent = waveHolder
waveBars = {}
for i = 1, 16 do
local bar = Instance.new("Frame")
bar.Size = UDim2.new(0, 5, 0.3, 0)
bar.BackgroundColor3 = THEMES[currentThemeName].accent2
bar.BorderSizePixel = 0
bar.Parent = waveHolder
uiCorner(bar, UDim.new(0,2))
table.insert(waveBars, bar)
end
-- ============================================================
-- STATUS PANEL (bottom-fixed strip inside binds tab; also mirrored on preview tab)
-- ============================================================
local statusY = vizY + 60
makeDivider(pg1, statusY, Color3.fromRGB(255,80,180))
local statusTitle = makeLabel(pg1, "📊 STATUS", UDim2.new(0,8,0,statusY+6), UDim2.new(0,120,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
regText(statusTitle)
local statLbl1 = makeLabel(pg1, "Anim: —", UDim2.new(0,8,0,statusY+26), UDim2.new(1,-16,0,16), Color3.fromRGB(210,210,220), Enum.TextXAlignment.Left, 10)
local statLbl2 = makeLabel(pg1, "Speed: x1.00 BPM: 100 State: STOPPED", UDim2.new(0,8,0,statusY+44), UDim2.new(1,-16,0,16), Color3.fromRGB(160,160,180), Enum.TextXAlignment.Left, 10)
function refreshStatusUI()
statLbl1.Text = string.format("Anim: %s (%s)", StatusState.currentAnimName, StatusState.currentAnimId ~= "" and StatusState.currentAnimId or "—")
statLbl2.Text = string.format("Speed: x%.2f BPM: %d State: %s",
StatusState.speedMult, StatusState.bpm, StatusState.playing and "PLAYING" or "STOPPED")
end
refreshStatusUI()
-- ============================================================
-- TAB 2 : ANIMATION SEQUENCE MAKER
-- ============================================================
local pg2 = pages["🎬 MAKER"]
local makerTitle = makeLabel(pg2, "🎬 ANIMATION SEQUENCE MAKER",
UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20),
Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(makerTitle)
makeDivider(pg2, 28, Color3.fromRGB(0,200,255))
makeLabel(pg2, "ANIM ID", UDim2.new(0,8,0,34), UDim2.new(0,220,0,16), Color3.fromRGB(160,160,160))
makeLabel(pg2, "DELAY (s)", UDim2.new(0,232,0,34), UDim2.new(0,80,0,16), Color3.fromRGB(160,160,160))
local seqScroll = Instance.new("ScrollingFrame")
seqScroll.Size = UDim2.new(1,-16,0,182); seqScroll.Position = UDim2.new(0,8,0,52)
seqScroll.BackgroundColor3 = Color3.fromRGB(18,18,26); seqScroll.BorderSizePixel = 0
seqScroll.ScrollBarThickness = 4; seqScroll.CanvasSize = UDim2.new(0,0,0,0)
seqScroll.Parent = pg2; uiCorner(seqScroll)
local seqLayout = Instance.new("UIListLayout")
seqLayout.SortOrder = Enum.SortOrder.LayoutOrder; seqLayout.Padding = UDim.new(0,4)
seqLayout.Parent = seqScroll
local seqPad = Instance.new("UIPadding")
seqPad.PaddingTop = UDim.new(0,4); seqPad.PaddingLeft = UDim.new(0,4); seqPad.PaddingRight = UDim.new(0,4)
seqPad.Parent = seqScroll
local function updateCanvas()
seqScroll.CanvasSize = UDim2.new(0,0,0, seqLayout.AbsoluteContentSize.Y + 10)
end
seqLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(updateCanvas)
local seqStatusLbl = makeLabel(pg2, "● IDLE",
UDim2.new(0,8,0,310), UDim2.new(1,-16,0,18), Color3.fromRGB(160,80,80), Enum.TextXAlignment.Left)
local function addSeqRow(id, delay)
local step = { id = id or "", delay = delay or 0.5 }
table.insert(sequence, step)
local row = Instance.new("Frame")
row.Size = UDim2.new(1,-8,0,30); row.BackgroundColor3 = Color3.fromRGB(24,24,36)
row.BorderSizePixel = 0; row.LayoutOrder = #sequence; row.Parent = seqScroll
uiCorner(row, UDim.new(0,6))
local idIn = makeBox(row, UDim2.new(0,4,0,3), UDim2.new(0,206,0,24), "Animation ID", step.id)
idIn.FocusLost:Connect(function() step.id = idIn.Text:gsub("%D","") end)
local dlIn = makeBox(row, UDim2.new(0,216,0,3), UDim2.new(0,64,0,24), "0.5", tostring(step.delay))
dlIn.FocusLost:Connect(function() step.delay = tonumber(dlIn.Text) or 0.5 end)
local rem = makeBtn(row, "✕", UDim2.new(0,286,0,3), UDim2.new(0,28,0,24), Color3.fromRGB(170,35,35), nil, 12)
rem.MouseButton1Click:Connect(function()
for j, s in ipairs(sequence) do if s == step then table.remove(sequence, j); break end end
for j, r in ipairs(seqRowFrames) do if r == row then table.remove(seqRowFrames, j); break end end
row:Destroy(); updateCanvas()
end)
table.insert(seqRowFrames, row); updateCanvas()
end
makeBtn(pg2, "+ ADD STEP", UDim2.new(0,8,0,240), UDim2.new(1,-16,0,26), Color3.fromRGB(35,35,55))
.MouseButton1Click:Connect(function() addSeqRow("", 0.5) end)
local seqPlay = makeBtn(pg2, "▶ PLAY SEQUENCE", UDim2.new(0,8,0,272), UDim2.new(0,176,0,30), Color3.fromRGB(60,190,120))
local seqStop = makeBtn(pg2, "■ STOP", UDim2.new(0,192,0,272), UDim2.new(0,90,0,30), Color3.fromRGB(190,50,50))
local seqClear = makeBtn(pg2, "🗑 CLEAR", UDim2.new(0,290,0,272), UDim2.new(0,106,0,30), Color3.fromRGB(70,25,70))
seqPlay.MouseButton1Click:Connect(function()
if #sequence == 0 then seqStatusLbl.Text = "⚠ Add steps first"; notify("Add a step first", "warn"); return end
playSequence()
seqStatusLbl.Text = "▶ RUNNING"; seqStatusLbl.TextColor3 = Color3.fromRGB(80,220,120)
notify("Sequence started", "info")
task.spawn(function()
while seqPlaying do task.wait(0.1) end
seqStatusLbl.Text = "● IDLE"; seqStatusLbl.TextColor3 = Color3.fromRGB(160,80,80)
end)
end)
seqStop.MouseButton1Click:Connect(function()
stopSequence(); seqStatusLbl.Text = "● IDLE"; seqStatusLbl.TextColor3 = Color3.fromRGB(160,80,80)
end)
seqClear.MouseButton1Click:Connect(function()
stopSequence(); sequence = {}
for _, r in ipairs(seqRowFrames) do pcall(function() r:Destroy() end) end
seqRowFrames = {}; updateCanvas()
seqStatusLbl.Text = "● IDLE"; seqStatusLbl.TextColor3 = Color3.fromRGB(160,80,80)
end)
-- Quick preset loader
makeDivider(pg2, 330, Color3.fromRGB(0,200,255))
local presetTitle = makeLabel(pg2, "⚡ QUICK PRESETS", UDim2.new(0,8,0,336), UDim2.new(1,-16,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
regText(presetTitle)
for i, preset in ipairs(PRESETS) do
local py = 356 + (i-1)*32
local pbtn = makeBtn(pg2, "📥 "..preset.name, UDim2.new(0,8,0,py), UDim2.new(1,-16,0,26), Color3.fromRGB(28,12,42), nil, 11)
pbtn.MouseButton1Click:Connect(function()
for _, row in ipairs(CurrentBindings) do
local newId = preset.ids[row.dir]
if newId then
row.id = newId
if row.keyBtn then destroyTrack(row.key) end
end
end
notify("Loaded preset: "..preset.name, "success")
end)
end
-- ============================================================
-- TAB 3 : LIVE PREVIEW
-- ============================================================
local pg3v = pages["👁 PREVIEW"]
local pvTitle = makeLabel(pg3v, "👁 LIVE CHARACTER PREVIEW", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(pvTitle)
makeDivider(pg3v, 28, Color3.fromRGB(0,200,255))
local viewport = Instance.new("ViewportFrame")
viewport.Size = UDim2.new(1,-16,0,260); viewport.Position = UDim2.new(0,8,0,36)
viewport.BackgroundColor3 = Color3.fromRGB(10,10,16)
-- ViewportFrames render pure black without an explicit light source —
-- these three properties act as a simple fill light for the clone.
viewport.Ambient = Color3.fromRGB(140,140,150)
viewport.LightColor = Color3.fromRGB(255,255,255)
viewport.LightDirection = Vector3.new(-0.6, -1, -0.4)
viewport.Parent = pg3v
uiCorner(viewport)
local vpStroke = uiStroke(viewport, Color3.fromRGB(255,80,180), 2)
regStroke(vpStroke)
previewCloneContainer = Instance.new("WorldModel")
previewCloneContainer.Parent = viewport
previewCam = Instance.new("Camera")
previewCam.FieldOfView = 60
previewCam.CFrame = CFrame.new(Vector3.new(0, 3, 6), Vector3.new(0, 3, 0))
viewport.CurrentCamera = previewCam
previewCam.Parent = viewport
-- fix the earlier placeholder orbit loop now that previewCam/container exist
task.spawn(function()
while true do
RunService.Heartbeat:Wait()
if previewCam then
previewAngle = previewAngle + 0.3
local rad = math.rad(previewAngle)
local dist = 6
local pos = Vector3.new(math.sin(rad) * dist, 4, math.cos(rad) * dist)
previewCam.CFrame = CFrame.new(pos, Vector3.new(0, 3, 0))
end
end
end)
local pvHint = makeLabel(pg3v, "Rotates automatically. Plays whatever anim you last triggered.",
UDim2.new(0,8,0,300), UDim2.new(1,-16,0,18), Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)
local pvRefreshBtn = makeBtn(pg3v, "🔄 REFRESH CLONE", UDim2.new(0,8,0,324), UDim2.new(1,-16,0,28), Color3.fromRGB(70,120,255))
pvRefreshBtn.MouseButton1Click:Connect(function()
if character then buildPreviewClone(character); notify("Preview clone refreshed", "info") end
end)
-- Status mirror on preview tab
makeDivider(pg3v, 362, Color3.fromRGB(255,80,180))
local pvStat1 = makeLabel(pg3v, "Anim: —", UDim2.new(0,8,0,370), UDim2.new(1,-16,0,16), Color3.fromRGB(210,210,220), Enum.TextXAlignment.Left, 10)
local pvStat2 = makeLabel(pg3v, "Speed: x1.00 BPM: 100 State: STOPPED", UDim2.new(0,8,0,388), UDim2.new(1,-16,0,16), Color3.fromRGB(160,160,180), Enum.TextXAlignment.Left, 10)
local oldRefresh = refreshStatusUI
refreshStatusUI = function()
oldRefresh()
pvStat1.Text = string.format("Anim: %s (%s)", StatusState.currentAnimName, StatusState.currentAnimId ~= "" and StatusState.currentAnimId or "—")
pvStat2.Text = string.format("Speed: x%.2f BPM: %d State: %s",
StatusState.speedMult, StatusState.bpm, StatusState.playing and "PLAYING" or "STOPPED")
end
-- Recents list
makeDivider(pg3v, 414, Color3.fromRGB(0,200,255))
local recentsTitle = makeLabel(pg3v, "🕐 RECENT ANIMATIONS", UDim2.new(0,8,0,420), UDim2.new(1,-16,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
regText(recentsTitle)
local recentsScroll = Instance.new("ScrollingFrame")
recentsScroll.Size = UDim2.new(1,-16,0,120); recentsScroll.Position = UDim2.new(0,8,0,442)
recentsScroll.BackgroundColor3 = Color3.fromRGB(18,18,26); recentsScroll.BorderSizePixel = 0
recentsScroll.ScrollBarThickness = 4; recentsScroll.CanvasSize = UDim2.new(0,0,0,0)
recentsScroll.Parent = pg3v; uiCorner(recentsScroll)
local recentsLayout = Instance.new("UIListLayout")
recentsLayout.Padding = UDim.new(0,4); recentsLayout.Parent = recentsScroll
function refreshRecentsUI()
for _, c in ipairs(recentsScroll:GetChildren()) do
if c:IsA("Frame") then c:Destroy() end
end
for i, r in ipairs(recents) do
local row = Instance.new("Frame")
row.Size = UDim2.new(1,-8,0,26); row.BackgroundColor3 = Color3.fromRGB(24,24,36)
row.BorderSizePixel = 0; row.LayoutOrder = i; row.Parent = recentsScroll
uiCorner(row, UDim.new(0,6))
makeLabel(row, r.label.." — "..r.id, UDim2.new(0,8,0,0), UDim2.new(1,-90,1,0), Color3.fromRGB(200,200,215), Enum.TextXAlignment.Left, 10)
local playBtn = makeBtn(row, "▶", UDim2.new(1,-40,0,2), UDim2.new(0,32,0,22), Color3.fromRGB(60,190,120), nil, 11)
playBtn.MouseButton1Click:Connect(function()
updatePreviewAnimation(r.id, r.label)
notify("Previewing "..r.label, "info")
end)
end
end
refreshRecentsUI()
-- ============================================================
-- TAB 4 : FAVORITES
-- ============================================================
local pg4 = pages["❤ FAVES"]
local favTitle = makeLabel(pg4, "❤ FAVORITE ANIMATIONS", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(favTitle)
makeDivider(pg4, 28, Color3.fromRGB(0,200,255))
local favNameBox = makeBox(pg4, UDim2.new(0,8,0,36), UDim2.new(0,180,0,28), "Name")
local favIdBox = makeBox(pg4, UDim2.new(0,196,0,36), UDim2.new(0,138,0,28), "Anim ID")
local favAddBtn = makeBtn(pg4, "+ SAVE", UDim2.new(0,340,0,36), UDim2.new(0,74,0,28), Color3.fromRGB(60,190,120), nil, 11)
local favScroll = Instance.new("ScrollingFrame")
favScroll.Size = UDim2.new(1,-16,0,300); favScroll.Position = UDim2.new(0,8,0,72)
favScroll.BackgroundColor3 = Color3.fromRGB(18,18,26); favScroll.BorderSizePixel = 0
favScroll.ScrollBarThickness = 4; favScroll.CanvasSize = UDim2.new(0,0,0,0)
favScroll.Parent = pg4; uiCorner(favScroll)
local favLayout = Instance.new("UIListLayout")
favLayout.Padding = UDim.new(0,4); favLayout.Parent = favScroll
local favPad = Instance.new("UIPadding")
favPad.PaddingTop = UDim.new(0,4); favPad.PaddingLeft = UDim.new(0,4); favPad.PaddingRight = UDim.new(0,4)
favPad.Parent = favScroll
local function refreshFavUI()
for _, c in ipairs(favScroll:GetChildren()) do
if c:IsA("Frame") then c:Destroy() end
end
for i, f in ipairs(favorites) do
local row = Instance.new("Frame")
row.Size = UDim2.new(1,-8,0,32); row.BackgroundColor3 = Color3.fromRGB(24,24,36)
row.BorderSizePixel = 0; row.LayoutOrder = i; row.Parent = favScroll
uiCorner(row, UDim.new(0,6))
makeLabel(row, f.name.." — "..f.id, UDim2.new(0,8,0,0), UDim2.new(1,-116,1,0), Color3.fromRGB(200,200,215), Enum.TextXAlignment.Left, 10)
local playBtn = makeBtn(row, "▶", UDim2.new(1,-146,0,3), UDim2.new(0,34,0,26), Color3.fromRGB(0,180,230), nil, 11)
local addPlBtn = makeBtn(row, "➕LIST", UDim2.new(1,-108,0,3), UDim2.new(0,58,0,26), Color3.fromRGB(70,120,255), nil, 9)
local delBtn = makeBtn(row, "✕", UDim2.new(1,-42,0,3), UDim2.new(0,34,0,26), Color3.fromRGB(170,35,35), nil, 11)
playBtn.MouseButton1Click:Connect(function()
updatePreviewAnimation(f.id, f.name)
pushRecent(f.id, f.name)
notify("Previewing favorite: "..f.name, "info")
end)
addPlBtn.MouseButton1Click:Connect(function()
table.insert(playlist, { name = f.name, id = f.id, delay = 1 })
if refreshPlaylistUI then refreshPlaylistUI() end
notify(f.name.." added to playlist", "success")
end)
delBtn.MouseButton1Click:Connect(function()
table.remove(favorites, i)
refreshFavUI()
end)
end
end
favAddBtn.MouseButton1Click:Connect(function()
local id = favIdBox.Text:gsub("%D","")
local nm = favNameBox.Text ~= "" and favNameBox.Text or ("Fav "..(#favorites+1))
if id == "" then notify("Enter an anim ID to save", "warn"); return end
table.insert(favorites, { name = nm, id = id })
favNameBox.Text = ""; favIdBox.Text = ""
refreshFavUI()
notify("Saved to favorites: "..nm, "success")
end)
refreshFavUI()
-- ============================================================
-- TAB 5 : PLAYLIST MANAGER
-- ============================================================
local pg5 = pages["🎶 LIST"]
local plTitle = makeLabel(pg5, "🎶 PLAYLIST MANAGER", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(plTitle)
makeDivider(pg5, 28, Color3.fromRGB(0,200,255))
local plScroll = Instance.new("ScrollingFrame")
plScroll.Size = UDim2.new(1,-16,0,260); plScroll.Position = UDim2.new(0,8,0,36)
plScroll.BackgroundColor3 = Color3.fromRGB(18,18,26); plScroll.BorderSizePixel = 0
plScroll.ScrollBarThickness = 4; plScroll.CanvasSize = UDim2.new(0,0,0,0)
plScroll.Parent = pg5; uiCorner(plScroll)
local plLayout = Instance.new("UIListLayout")
plLayout.Padding = UDim.new(0,4); plLayout.Parent = plScroll
local plPad = Instance.new("UIPadding")
plPad.PaddingTop = UDim.new(0,4); plPad.PaddingLeft = UDim.new(0,4); plPad.PaddingRight = UDim.new(0,4)
plPad.Parent = plScroll
function refreshPlaylistUI()
for _, c in ipairs(plScroll:GetChildren()) do
if c:IsA("Frame") then c:Destroy() end
end
for i, item in ipairs(playlist) do
local row = Instance.new("Frame")
row.Size = UDim2.new(1,-8,0,30)
row.BackgroundColor3 = (i == playlistIndex and playlistPlaying) and Color3.fromRGB(50,30,60) or Color3.fromRGB(24,24,36)
row.BorderSizePixel = 0; row.LayoutOrder = i; row.Parent = plScroll
uiCorner(row, UDim.new(0,6))
makeLabel(row, string.format("%d. %s — %s", i, item.name or item.id, item.id),
UDim2.new(0,8,0,0), UDim2.new(1,-40,1,0), Color3.fromRGB(200,200,215), Enum.TextXAlignment.Left, 10)
local rem = makeBtn(row, "✕", UDim2.new(1,-34,0,3), UDim2.new(0,26,0,24), Color3.fromRGB(170,35,35), nil, 11)
rem.MouseButton1Click:Connect(function()
table.remove(playlist, i)
refreshPlaylistUI()
end)
end
end
refreshPlaylistUI()
local plAddBox = makeBox(pg5, UDim2.new(0,8,0,300), UDim2.new(0,220,0,26), "Animation ID to add")
local plAddBtn = makeBtn(pg5, "+ ADD", UDim2.new(0,236,0,300), UDim2.new(0,80,0,26), Color3.fromRGB(35,35,55))
plAddBtn.MouseButton1Click:Connect(function()
local id = plAddBox.Text:gsub("%D","")
if id == "" then notify("Enter an anim ID", "warn"); return end
table.insert(playlist, { name = "Track "..(#playlist+1), id = id, delay = 1 })
plAddBox.Text = ""
refreshPlaylistUI()
end)
local plPlay = makeBtn(pg5, "▶ PLAY", UDim2.new(0,8,0,332), UDim2.new(0,96,0,28), Color3.fromRGB(60,190,120), nil, 11)
local plPrev = makeBtn(pg5, "⏮ PREV", UDim2.new(0,110,0,332), UDim2.new(0,86,0,28), Color3.fromRGB(70,120,255), nil, 11)
local plNext = makeBtn(pg5, "NEXT ⏭", UDim2.new(0,202,0,332), UDim2.new(0,86,0,28), Color3.fromRGB(70,120,255), nil, 11)
local plStop = makeBtn(pg5, "■ STOP", UDim2.new(0,294,0,332), UDim2.new(0,100,0,28), Color3.fromRGB(190,50,50), nil, 11)
plPlay.MouseButton1Click:Connect(function()
if #playlist == 0 then notify("Playlist is empty", "warn"); return end
playPlaylist(); notify("Playlist started", "info")
end)
plPrev.MouseButton1Click:Connect(prevPlaylistTrack)
plNext.MouseButton1Click:Connect(nextPlaylistTrack)
plStop.MouseButton1Click:Connect(function() stopPlaylist(); refreshPlaylistUI() end)
-- ============================================================
-- TAB : FX (vinyl record / CRT mode / spectrogram / BPM detector)
-- ============================================================
local pgFx = pages["🎛 FX"]
-- ── Vinyl record widget ──
local vinylTitle = makeLabel(pgFx, "💿 VINYL RECORD", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(vinylTitle)
makeDivider(pgFx, 28, Color3.fromRGB(0,200,255))
makeLabel(pgFx, "Spins faster the higher the current speed / BPM is.",
UDim2.new(0,8,0,34), UDim2.new(1,-16,0,18), Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)
local vinylHolder = Instance.new("Frame")
vinylHolder.Size = UDim2.new(0,120,0,120); vinylHolder.Position = UDim2.new(0,8,0,56)
vinylHolder.BackgroundColor3 = Color3.fromRGB(8,8,10); vinylHolder.BorderSizePixel = 0
vinylHolder.Parent = pgFx; uiCorner(vinylHolder, UDim.new(1,0))
local vinylHolderStroke = uiStroke(vinylHolder, Color3.fromRGB(255,80,180), 2)
regStroke(vinylHolderStroke)
vinylImage = Instance.new("ImageLabel")
vinylImage.Size = UDim2.new(1,-8,1,-8); vinylImage.Position = UDim2.new(0,4,0,4)
vinylImage.BackgroundTransparency = 1
vinylImage.Image = VINYL_DECAL_ID ~= "" and ("rbxassetid://"..VINYL_DECAL_ID) or ""
vinylImage.ScaleType = Enum.ScaleType.Fit
vinylImage.Parent = vinylHolder
-- fallback ring so it still reads as a record before a decal is set
local vinylRing = Instance.new("Frame")
vinylRing.Size = UDim2.new(1,0,1,0); vinylRing.BackgroundTransparency = 1
vinylRing.Parent = vinylHolder
uiCorner(vinylRing, UDim.new(1,0))
uiStroke(vinylRing, Color3.fromRGB(60,60,65), 1)
local vinylCenter = Instance.new("Frame")
vinylCenter.Size = UDim2.new(0,14,0,14); vinylCenter.AnchorPoint = Vector2.new(0.5,0.5)
vinylCenter.Position = UDim2.new(0.5,0,0.5,0); vinylCenter.BackgroundColor3 = Color3.fromRGB(255,80,180)
vinylCenter.BorderSizePixel = 0; vinylCenter.Parent = vinylHolder
uiCorner(vinylCenter, UDim.new(1,0)); regBg(vinylCenter)
local vinylIdBox = makeBox(pgFx, UDim2.new(0,140,0,56), UDim2.new(1,-148,0,28), "Vinyl decal asset ID")
vinylIdBox.FocusLost:Connect(function()
local id = vinylIdBox.Text:gsub("%D","")
VINYL_DECAL_ID = id
vinylImage.Image = id ~= "" and ("rbxassetid://"..id) or ""
end)
makeLabel(pgFx, "Paste your record-label decal ID above — art rotates with the disc.",
UDim2.new(0,140,0,88), UDim2.new(1,-148,0,32), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)
makeLabel(pgFx, "BASE RPM", UDim2.new(0,140,0,124), UDim2.new(0,70,0,24), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vinylRpmBox = makeBox(pgFx, UDim2.new(0,214,0,124), UDim2.new(0,70,0,24), "33.3", tostring(vinylBaseRPM))
vinylRpmBox.FocusLost:Connect(function()
local n = tonumber(vinylRpmBox.Text)
if n and n > 0 then vinylBaseRPM = n else vinylRpmBox.Text = tostring(vinylBaseRPM) end
end)
-- ── CRT monitor mode ──
makeDivider(pgFx, 190, Color3.fromRGB(255,80,180))
local crtTitle = makeLabel(pgFx, "📺 CRT MONITOR MODE", UDim2.new(0,8,0,196), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(crtTitle)
makeLabel(pgFx, "Adds scanlines + a green-tinted flicker over the whole panel.",
UDim2.new(0,8,0,218), UDim2.new(1,-16,0,18), Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)
local crtBtn = makeBtn(pgFx, "📴 CRT MODE OFF", UDim2.new(0,8,0,240), UDim2.new(1,-16,0,32), Color3.fromRGB(35,35,55), nil, 11)
crtBtn.MouseButton1Click:Connect(function()
crtEnabled = not crtEnabled
if crtOverlay then crtOverlay.Visible = crtEnabled end
if crtEnabled then
crtBtn.Text = "📺 CRT MODE ON"
crtBtn.BackgroundColor3 = THEMES[currentThemeName].accent
else
crtBtn.Text = "📴 CRT MODE OFF"
crtBtn.BackgroundColor3 = Color3.fromRGB(35,35,55)
end
notify(crtEnabled and "CRT mode enabled" or "CRT mode disabled", "info")
end)
-- ── Mic spectrogram ──
makeDivider(pgFx, 284, Color3.fromRGB(0,200,255))
local specTitle = makeLabel(pgFx, "🎙 SPECTROGRAM (MIC)", UDim2.new(0,8,0,290), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(specTitle)
local specStatusLbl = makeLabel(pgFx, "● not connected", UDim2.new(0,200,0,290), UDim2.new(1,-208,0,20), Color3.fromRGB(160,80,80), Enum.TextXAlignment.Right, 10)
makeLabel(pgFx, "Uses Roblox's mic Audio API if this client/game supports it; otherwise the bars react to animation state instead.",
UDim2.new(0,8,0,312), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)
local specHolder = Instance.new("Frame")
specHolder.Size = UDim2.new(1,-16,0,60); specHolder.Position = UDim2.new(0,8,0,346)
specHolder.BackgroundColor3 = Color3.fromRGB(18,18,26); specHolder.BorderSizePixel = 0
specHolder.Parent = pgFx; uiCorner(specHolder)
local specLayout = Instance.new("UIListLayout")
specLayout.FillDirection = Enum.FillDirection.Horizontal
specLayout.Padding = UDim.new(0,3)
specLayout.VerticalAlignment = Enum.VerticalAlignment.Bottom
specLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
specLayout.Parent = specHolder
specBars = {}
for i = 1, SPEC_BAR_COUNT do
local bar = Instance.new("Frame")
bar.Size = UDim2.new(0, 6, 0.15, 0)
bar.BackgroundColor3 = THEMES[currentThemeName].accent2
bar.BorderSizePixel = 0
bar.Parent = specHolder
uiCorner(bar, UDim.new(0,2))
table.insert(specBars, bar)
end
local micBtn = makeBtn(pgFx, "🎙 REQUEST MIC ACCESS", UDim2.new(0,8,0,414), UDim2.new(1,-16,0,30), Color3.fromRGB(70,120,255), nil, 11)
micBtn.MouseButton1Click:Connect(function()
local success = setupMicSpectrogram()
if success then
specStatusLbl.Text = "● mic connected"; specStatusLbl.TextColor3 = Color3.fromRGB(80,220,120)
notify("Mic spectrogram connected", "success")
else
specStatusLbl.Text = "● using fallback"; specStatusLbl.TextColor3 = Color3.fromRGB(255,180,60)
notify("Mic unavailable here — using animation-synced fallback", "warn")
end
end)
-- ── Animation BPM detector ──
makeDivider(pgFx, 456, Color3.fromRGB(255,80,180))
local bpmDetectTitle = makeLabel(pgFx, "🎯 BPM DETECTOR", UDim2.new(0,8,0,462), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(bpmDetectTitle)
makeLabel(pgFx, "Tap mode reads the rhythm of your Z/X/C/V presses. Idle mode reads the loaded idle clip's length.",
UDim2.new(0,8,0,484), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)
local detectedLbl = makeLabel(pgFx, "Detected: — BPM", UDim2.new(0,8,0,516), UDim2.new(1,-16,0,22), Color3.fromRGB(0,200,255), Enum.TextXAlignment.Left, 13)
function refreshBpmDetectorUI()
if detectedBPM then
detectedLbl.Text = string.format("Detected: %d BPM (from %d taps)", detectedBPM, #bpmDetectTaps)
end
end
makeLabel(pgFx, "BEATS/LOOP", UDim2.new(0,8,0,542), UDim2.new(0,80,0,26), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local beatsBox = makeBox(pgFx, UDim2.new(0,90,0,542), UDim2.new(0,50,0,26), "4", tostring(beatsPerIdleLoop))
beatsBox.FocusLost:Connect(function()
local n = tonumber(beatsBox.Text)
if n and n > 0 then beatsPerIdleLoop = n else beatsBox.Text = tostring(beatsPerIdleLoop) end
end)
local idleDetectBtn = makeBtn(pgFx, "📐 FROM IDLE LENGTH", UDim2.new(0,148,0,542), UDim2.new(0,150,0,26), Color3.fromRGB(70,120,255), nil, 10)
idleDetectBtn.MouseButton1Click:Connect(function()
local bpm = detectBpmFromIdleLength(beatsPerIdleLoop)
if bpm then
refreshBpmDetectorUI()
notify("Detected "..bpm.." BPM from idle length", "success")
else
notify("Load an idle animation first", "warn")
end
end)
local resetTapBtn = makeBtn(pgFx, "↺ RESET TAPS", UDim2.new(0,306,0,542), UDim2.new(0,116,0,26), Color3.fromRGB(170,35,35), nil, 10)
resetTapBtn.MouseButton1Click:Connect(function()
bpmDetectTaps = {}; detectedBPM = nil
detectedLbl.Text = "Detected: — BPM"
notify("Tap history cleared", "info")
end)
local applyDetectedBtn = makeBtn(pgFx, "✓ APPLY DETECTED BPM TO IDLE", UDim2.new(0,8,0,574), UDim2.new(1,-16,0,28), Color3.fromRGB(60,190,120), nil, 11)
applyDetectedBtn.MouseButton1Click:Connect(function()
if detectedBPM then
setIdleBPM(detectedBPM)
notify("Idle BPM set to "..detectedBPM, "success")
else
notify("No BPM detected yet — tap some keys or use idle length", "warn")
end
end)
-- ── Shared music player (no VC) ──
makeDivider(pgFx, 616, Color3.fromRGB(0,200,255))
local musicTitle = makeLabel(pgFx, "🎵 MUSIC PLAYER (NO VC)", UDim2.new(0,8,0,622), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(musicTitle)
makeLabel(pgFx, "Plays an uploaded audio asset for the whole server directly — no voice chat involved, so no mic compression/suppression.",
UDim2.new(0,8,0,644), UDim2.new(1,-16,0,30), Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)
makeLabel(pgFx, "Needs the companion server script installed once in ServerScriptService (see notes).",
UDim2.new(0,8,0,676), UDim2.new(1,-16,0,18), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local musicIdBox = makeBox(pgFx, UDim2.new(0,8,0,698), UDim2.new(1,-16,0,28), "Audio asset ID (e.g. 1234567890)")
local musicPlayBtn = makeBtn(pgFx, "▶ PLAY FOR EVERYONE", UDim2.new(0,8,0,730), UDim2.new(0,220,0,30), Color3.fromRGB(60,190,120), nil, 11)
local musicStopBtn = makeBtn(pgFx, "■ STOP", UDim2.new(0,236,0,730), UDim2.new(0,160,0,30), Color3.fromRGB(190,50,50), nil, 11)
musicStatusLbl = makeLabel(pgFx, "● idle", UDim2.new(0,8,0,766), UDim2.new(1,-16,0,20), Color3.fromRGB(160,80,80), Enum.TextXAlignment.Left, 11)
musicPlayBtn.MouseButton1Click:Connect(function()
local id = musicIdBox.Text:gsub("%D","")
if id == "" then notify("Enter an audio asset ID first", "warn"); return end
playMusicTrack(id)
notify("Requested track "..id.." for the whole server", "info")
end)
musicStopBtn.MouseButton1Click:Connect(function()
stopMusicTrack()
notify("Stopped shared music", "info")
end)
-- ============================================================
-- TAB 6 : SETTINGS (legs / face / theme)
-- ============================================================
local pg6 = pages["⚙ SETTINGS"]
local legsTitle = makeLabel(pg6, "🦵 LEGS ON GROUND (Optional)",
UDim2.new(0,8,0,8), UDim2.new(1,-16,0,20),
Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(legsTitle)
makeDivider(pg6, 30, Color3.fromRGB(0,200,255))
makeLabel(pg6, "Prevents walking and jumping — character stays planted.",
UDim2.new(0,8,0,36), UDim2.new(1,-16,0,28),
Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)
local legsBtn = makeBtn(pg6, "🔓 LEGS UNLOCKED (OFF)",
UDim2.new(0,8,0,68), UDim2.new(1,-16,0,32), Color3.fromRGB(35,35,55), nil, 11)
legsBtn.MouseButton1Click:Connect(function()
legsLocked = not legsLocked
applyLegsLock(legsLocked)
if legsLocked then
legsBtn.Text = "🔒 LEGS LOCKED (ON)"
legsBtn.BackgroundColor3 = THEMES[currentThemeName].accent
else
legsBtn.Text = "🔓 LEGS UNLOCKED (OFF)"
legsBtn.BackgroundColor3 = Color3.fromRGB(35,35,55)
end
notify(legsLocked and "Legs locked" or "Legs unlocked", "info")
end)
-- Dynamic face
makeDivider(pg6, 114, Color3.fromRGB(255,80,180))
local faceTitle = makeLabel(pg6, "😊 DYNAMIC FACE IDs (per arrow direction)",
UDim2.new(0,8,0,120), UDim2.new(1,-16,0,20),
Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(faceTitle)
makeLabel(pg6, "Only works if your character has a face decal on its head.",
UDim2.new(0,8,0,142), UDim2.new(1,-16,0,18),
Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)
local faceOrder = { "LEFT", "DOWN", "UP", "RIGHT" }
for i, dir in ipairs(faceOrder) do
local y = 164 + (i-1)*38
local col = ARROW_COLORS[dir] and ARROW_COLORS[dir].hit or Color3.fromRGB(255,255,255)
makeLabel(pg6, dir, UDim2.new(0,8,0,y), UDim2.new(0,52,0,28), col, Enum.TextXAlignment.Left)
local fb = makeBox(pg6, UDim2.new(0,66,0,y), UDim2.new(1,-74,0,28), "Face decal ID for "..dir, faceIds[dir])
local capturedDir = dir
fb.FocusLost:Connect(function()
faceIds[capturedDir] = fb.Text:gsub("%D","")
end)
end
-- Theme selector
makeDivider(pg6, 320, Color3.fromRGB(0,200,255))
local themeTitle = makeLabel(pg6, "🎨 THEME", UDim2.new(0,8,0,326), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(themeTitle)
local themeNames = { "Pink", "Cyan", "Purple", "RGB" }
local themeSwatch = { Pink = Color3.fromRGB(255,80,180), Cyan = Color3.fromRGB(0,200,255),
Purple = Color3.fromRGB(165,85,255), RGB = Color3.fromRGB(255,255,255) }
for i, tn in ipairs(themeNames) do
local tx = 8 + (i-1) * 104
local tbtn = makeBtn(pg6, tn, UDim2.new(0, tx, 0, 356), UDim2.new(0, 96, 0, 32), themeSwatch[tn], Color3.fromRGB(20,20,20), 11)
tbtn.MouseButton1Click:Connect(function() setTheme(tn) end)
end
-- ============================================================
-- RP TUG-OF-WAR UI
-- ============================================================
makeDivider(pg6, 400, Color3.fromRGB(0,200,255))
local rpTitle = makeLabel(pg6, "⚔ RP TUG-OF-WAR (PROXIMITY)", UDim2.new(0,8,0,406), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
regText(rpTitle)
makeLabel(pg6, "Moves you toward the target on every note press.", UDim2.new(0,8,0,428), UDim2.new(1,-16,0,18), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)
-- Toggle Button
local rpBtn = makeBtn(pg6, "📴 RP MODE OFF", UDim2.new(0,8,0,450), UDim2.new(0,160,0,32), Color3.fromRGB(35,35,55), nil, 11)
rpBtn.MouseButton1Click:Connect(function()
rpModeEnabled = not rpModeEnabled
if rpModeEnabled then
rpBtn.Text = "⚔ RP MODE ON"
rpBtn.BackgroundColor3 = THEMES[currentThemeName].accent
else
rpBtn.Text = "📴 RP MODE OFF"
rpBtn.BackgroundColor3 = Color3.fromRGB(35,35,55)
end
end)
-- Player Search Box
local targetBox = makeBox(pg6, UDim2.new(0,176,0,450), UDim2.new(1,-184,0,32), "Target Player Name...")
targetBox.FocusLost:Connect(function()
setTargetPlayer(targetBox.Text)
end)
-- Speed Slider
makeLabel(pg6, "SPEED", UDim2.new(0,8,0,490), UDim2.new(0,40,0,22), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local rpValLbl = makeLabel(pg6, string.format("%.1f studs", rpMoveStep), UDim2.new(0,300,0,490), UDim2.new(0,94,0,22), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 11)
regText(rpValLbl)
makeSlider(pg6, UDim2.new(0,50,0,494), UDim2.new(0,250,0,14), 0.5, 5, rpMoveStep, function(v)
rpMoveStep = v
rpValLbl.Text = string.format("%.1f studs", v)
end)
-- Hide / show toggle
toggleBtn.MouseButton1Click:Connect(function()
guiVisible = not guiVisible
contentArea.Visible = guiVisible
tabBar.Visible = guiVisible
toggleBtn.Text = guiVisible and "HIDE" or "SHOW"
end)
startVisualizerLoops()
notify("FNF Injector v6.1 loaded", "success")
return sg
end
-- ============================================================
-- LAUNCH
-- ============================================================
task.spawn(setupMusicRemote)
buildArrowHud()
buildGUI()
--[[
╔══════════════════════════════════════════════════════╗
║ FNF CINEMATIC STANDOFF INTRO — v1.0 ║
║ Back-to-back walk-off → turn → local VS split-cam ║
╚══════════════════════════════════════════════════════╝
HOW IT WORKS:
• Run this SAME script on both accounts (yours + your alt).
• Give each one a "role": "A" or "B", and the other account's
username. Role decides which spot / which walk direction.
• Movement (walking to spots, turning to face each other) is
done by tweening YOUR OWN HumanoidRootPart CFrame. Since you
have network ownership of your own character, this actually
replicates to everyone else in the server — they see two
players walk to their marks and square up, nothing more.
• The VS split-screen face-cam overlay is a local ScreenGui —
built from cloned heads in a ViewportFrame, never touches
the real character, so only the client running this sees it.
• True frame-perfect sync between two separate accounts needs
a server RemoteEvent (one is auto-detected if present, see
SYNC section). Without one, it falls back to a 3-2-1 on-screen
countdown so you just start both scripts and count together.
]]
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer:WaitForChild("PlayerGui")
local camera = workspace.CurrentCamera
-- ============================================================
-- CONFIG — tweak freely
-- ============================================================
local Config = {
TriggerKey = Enum.KeyCode.B, -- press this to arm/start
AltUsername = "mentransmigrasilo",-- who the other rig is
-- world anchor: standoff happens relative to wherever LocalPlayer
-- is standing the moment you press the trigger key
SpotDistance = 14, -- how far apart the final marks are
CenterGap = 3, -- how close together they start, back to back
WalkDuration = 2.4,
TurnDuration = 0.55,
HoldBeforeSplit = 0.35,
SplitCamDuration = 2.6, -- how long the VS face-cam stays up
CountdownEnabled = true, -- 3-2-1 fallback sync
WalkAnimId = "180426354", -- placeholder walk-forward loop
IdleStanceAnimId = "507766388", -- placeholder FNF-ready idle stance
-- role placement — "A" ends up on the right (Boyfriend side),
-- "B" ends up on the left (Opponent side), classic FNF stage layout
Role = "A", -- "A" or "B", set per-account
}
-- ============================================================
-- OPTIONAL SERVER SYNC (auto-detected, not required)
-- ============================================================
-- If a companion server script drops a "StandoffSyncRemote" RemoteEvent
-- into ReplicatedStorage, we use it to get a synced go-time. If it's not
-- there, we just fall back to the local 3-2-1 countdown.
local syncRemote = nil
task.spawn(function()
pcall(function()
syncRemote = ReplicatedStorage:WaitForChild("StandoffSyncRemote", 3)
end)
end)
-- ============================================================
-- STATE
-- ============================================================
local standoffActive = false
local rigConns = {}
local function cleanupConns()
for _, c in ipairs(rigConns) do pcall(function() c:Disconnect() end) end
rigConns = {}
end
-- ============================================================
-- CHARACTER / ANIMATOR HELPERS
-- ============================================================
local function getHumanoidBits(char)
if not char then return nil end
local hum = char:FindFirstChildOfClass("Humanoid")
local root = char:FindFirstChild("HumanoidRootPart")
if not hum or not root then return nil end
local animator = hum:FindFirstChildOfClass("Animator") or Instance.new("Animator", hum)
return { hum = hum, root = root, animator = animator }
end
local function loadAnim(animator, id, looped, priority)
local ok, track = pcall(function()
local anim = Instance.new("Animation")
anim.AnimationId = "rbxassetid://"..id
local t = animator:LoadAnimation(anim)
t.Looped = looped
t.Priority = priority or Enum.AnimationPriority.Action
return t
end)
if ok then return track end
return nil
end
-- Smoothly tween a HumanoidRootPart's CFrame. Root gets anchored for the
-- duration so physics doesn't fight the tween, then restored after.
local function tweenRoot(root, targetCFrame, duration, style, direction)
local wasAnchored = root.Anchored
root.Anchored = true
local tw = TweenService:Create(root, TweenInfo.new(duration, style or Enum.EasingStyle.Sine, direction or Enum.EasingDirection.InOut), {
CFrame = targetCFrame
})
tw:Play()
tw.Completed:Wait()
root.Anchored = wasAnchored
return true
end
-- ============================================================
-- ON-SCREEN 3-2-1 SYNC COUNTDOWN (local only, helps line up two clients)
-- ============================================================
local function showCountdown(cb)
local sg = Instance.new("ScreenGui")
sg.Name = "StandoffCountdown"; sg.ResetOnSpawn = false; sg.Parent = PlayerGui
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.new(0,300,0,150)
lbl.AnchorPoint = Vector2.new(0.5,0.5)
lbl.Position = UDim2.new(0.5,0,0.5,0)
lbl.BackgroundTransparency = 1
lbl.Font = Enum.Font.GothamBlack
lbl.TextSize = 90
lbl.TextColor3 = Color3.fromRGB(255,80,180)
lbl.TextStrokeTransparency = 0
lbl.TextStrokeColor3 = Color3.new(0,0,0)
lbl.Parent = sg
task.spawn(function()
for _, n in ipairs({"3","2","1","GO"}) do
lbl.Text = n
lbl.Size = UDim2.new(0,420,0,210)
TweenService:Create(lbl, TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
Size = UDim2.new(0,300,0,150)
}):Play()
task.wait(0.7)
end
sg:Destroy()
if cb then cb() end
end)
end
-- ============================================================
-- CINEMATIC CAMERA (local only)
-- ============================================================
local savedCamType, savedCamCFrame
local function lockCinematicCamera(startCFrame)
savedCamType = camera.CameraType
savedCamCFrame = camera.CFrame
camera.CameraType = Enum.CameraType.Scriptable
camera.CFrame = startCFrame
end
local function restoreCamera()
camera.CameraType = savedCamType or Enum.CameraType.Custom
end
-- Wide establishing dolly: pulls back slowly as both walk to their marks
local function dollyCamera(fromCFrame, toCFrame, duration)
local tw = TweenService:Create(camera, TweenInfo.new(duration, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {
CFrame = toCFrame
})
camera.CFrame = fromCFrame
tw:Play()
return tw
end
-- Subtle handheld shake, used during the VS face-cam hold
local shakeConn = nil
local function startCameraShake(intensity)
if shakeConn then shakeConn:Disconnect() end
shakeConn = RunService.RenderStepped:Connect(function()
if camera.CameraType == Enum.CameraType.Scriptable then
local ox = (math.random() - 0.5) * intensity
local oy = (math.random() - 0.5) * intensity
camera.CFrame = camera.CFrame * CFrame.new(ox, oy, 0)
end
end)
end
local function stopCameraShake()
if shakeConn then shakeConn:Disconnect(); shakeConn = nil end
end
-- ============================================================
-- VS FACE-CAM SPLIT SCREEN (local only — WorldModel clone trick,
-- same pattern as a standard viewport character preview)
-- ============================================================
local function buildFaceViewport(char, parent, position, size)
local vp = Instance.new("ViewportFrame")
vp.Size = size; vp.Position = position
vp.BackgroundColor3 = Color3.fromRGB(6,6,10)
vp.Ambient = Color3.fromRGB(160,160,170)
vp.LightColor = Color3.fromRGB(255,255,255)
vp.LightDirection = Vector3.new(-0.4,-0.6,-0.6)
vp.Parent = parent
local world = Instance.new("WorldModel")
world.Parent = vp
local wasArchivable = char.Archivable
if not wasArchivable then char.Archivable = true end
local ok, clone = pcall(function() return char:Clone() end)
if not wasArchivable then char.Archivable = wasArchivable end
if not ok or not clone then return vp end
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("Script") or d:IsA("LocalScript") then d:Destroy() end
end
clone.Parent = world
local head = clone:FindFirstChild("Head")
local hrp = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
if hrp then hrp.Anchored = true end
local cam = Instance.new("Camera")
if head then
cam.CFrame = CFrame.new(head.Position + Vector3.new(0,0,2.2), head.Position)
else
cam.CFrame = CFrame.new(Vector3.new(0,3,4), Vector3.new(0,3,0))
end
cam.FieldOfView = 45
cam.Parent = vp
vp.CurrentCamera = cam
return vp
end
local function playVSFaceCam(myChar, altChar, holdTime)
local sg = Instance.new("ScreenGui")
sg.Name = "StandoffVS"; sg.ResetOnSpawn = false; sg.IgnoreGuiInset = true
sg.Parent = PlayerGui
local root = Instance.new("Frame")
root.Size = UDim2.new(1,0,1,0); root.BackgroundColor3 = Color3.new(0,0,0)
root.BackgroundTransparency = 1; root.ClipsDescendants = true; root.Parent = sg
-- LEFT half (opponent / role B viewport by convention)
local leftHalf = Instance.new("Frame")
leftHalf.Size = UDim2.new(0.5,0,1,0); leftHalf.Position = UDim2.new(-0.5,0,0,0)
leftHalf.BackgroundTransparency = 1; leftHalf.ClipsDescendants = true; leftHalf.Parent = root
-- RIGHT half (player / role A viewport by convention)
local rightHalf = Instance.new("Frame")
rightHalf.Size = UDim2.new(0.5,0,1,0); rightHalf.Position = UDim2.new(1,0,0,0)
rightHalf.BackgroundTransparency = 1; rightHalf.ClipsDescendants = true; rightHalf.Parent = root
local isRoleA = (Config.Role == "A")
local myHalf, altHalf = (isRoleA and rightHalf or leftHalf), (isRoleA and leftHalf or rightHalf)
buildFaceViewport(altChar, altHalf, UDim2.new(0,0,0,0), UDim2.new(1,0,1,0))
buildFaceViewport(myChar, myHalf, UDim2.new(0,0,0,0), UDim2.new(1,0,1,0))
-- diagonal seam divider
local seam = Instance.new("Frame")
seam.Size = UDim2.new(0,10,2.4,0)
seam.AnchorPoint = Vector2.new(0.5,0.5)
seam.Position = UDim2.new(0.5,0,0.5,0)
seam.Rotation = 18
seam.BackgroundColor3 = Color3.fromRGB(255,80,180)
seam.BorderSizePixel = 0; seam.ZIndex = 5; seam.Parent = root
-- VS badge, pops in with a punchy scale tween
local vsLbl = Instance.new("TextLabel")
vsLbl.Size = UDim2.new(0,140,0,70)
vsLbl.AnchorPoint = Vector2.new(0.5,0.5)
vsLbl.Position = UDim2.new(0.5,0,0.5,0)
vsLbl.BackgroundTransparency = 1
vsLbl.Text = "VS"; vsLbl.Font = Enum.Font.GothamBlack; vsLbl.TextSize = 0
vsLbl.TextColor3 = Color3.fromRGB(255,255,255)
vsLbl.TextStrokeTransparency = 0; vsLbl.TextStrokeColor3 = Color3.fromRGB(255,80,180)
vsLbl.ZIndex = 6; vsLbl.Parent = root
-- slide both halves in
TweenService:Create(leftHalf, TweenInfo.new(0.45, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Position = UDim2.new(0,0,0,0) }):Play()
local rightTw = TweenService:Create(rightHalf, TweenInfo.new(0.45, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Position = UDim2.new(0.5,0,0,0) })
rightTw:Play()
task.wait(0.25)
TweenService:Create(vsLbl, TweenInfo.new(0.35, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { TextSize = 64 }):Play()
startCameraShake(0.02)
task.wait(holdTime)
stopCameraShake()
-- fade out
local fadeTime = 0.4
TweenService:Create(leftHalf, TweenInfo.new(fadeTime, Enum.EasingStyle.Quad), { Position = UDim2.new(-0.5,0,0,0) }):Play()
TweenService:Create(rightHalf, TweenInfo.new(fadeTime, Enum.EasingStyle.Quad), { Position = UDim2.new(1,0,0,0) }):Play()
TweenService:Create(vsLbl, TweenInfo.new(fadeTime, Enum.EasingStyle.Quad), { TextSize = 0 }):Play()
task.wait(fadeTime + 0.1)
sg:Destroy()
end
-- ============================================================
-- MAIN SEQUENCE
-- ============================================================
local function runStandoff()
if standoffActive then return end
standoffActive = true
local altPlayer = Players:FindFirstChild(Config.AltUsername)
if not altPlayer or not altPlayer.Character then
warn("[Standoff] Alt player not found or has no character.")
standoffActive = false
return
end
local myChar = LocalPlayer.Character
local altChar = altPlayer.Character
local myBits = getHumanoidBits(myChar)
local altBits = getHumanoidBits(altChar)
if not myBits or not altBits then
warn("[Standoff] Missing Humanoid/HumanoidRootPart on one of the rigs.")
standoffActive = false
return
end
local function begin()
-- anchor point = my current position, facing my current LookVector
local originCFrame = CFrame.new(myBits.root.Position, myBits.root.Position + myBits.root.CFrame.LookVector)
local isRoleA = (Config.Role == "A")
-- back-to-back start: both on the same line, facing opposite ways,
-- separated only by CenterGap
local halfGap = Config.CenterGap / 2
local myStart = originCFrame * CFrame.new(isRoleA and halfGap or -halfGap, 0, 0)
* (isRoleA and CFrame.Angles(0, math.rad(90), 0) or CFrame.Angles(0, math.rad(-90), 0))
tweenRoot(myBits.root, myStart, 0.01, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut)
-- cinematic camera: wide shot centered on both, looking down the line
local camStart = originCFrame * CFrame.new(0, 5, 10)
local camEnd = originCFrame * CFrame.new(0, 4, Config.SpotDistance * 0.9)
lockCinematicCamera(camStart)
local camTw = dollyCamera(camStart, camEnd, Config.WalkDuration + Config.TurnDuration)
-- walk animation while marching to spot
local walkTrack = loadAnim(myBits.animator, Config.WalkAnimId, true, Enum.AnimationPriority.Movement)
if walkTrack then walkTrack:Play(0.15) end
-- walk out to final mark, still facing away from each other
local mySpot = originCFrame * CFrame.new(isRoleA and Config.SpotDistance/2 or -Config.SpotDistance/2, 0, 0)
* (isRoleA and CFrame.Angles(0, math.rad(90), 0) or CFrame.Angles(0, math.rad(-90), 0))
tweenRoot(myBits.root, mySpot, Config.WalkDuration, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
if walkTrack then walkTrack:Stop(0.15) end
-- turn to face each other — role A faces -X (toward B), role B faces +X (toward A)
local turnCFrame = mySpot * (isRoleA and CFrame.Angles(0, math.rad(-90), 0) or CFrame.Angles(0, math.rad(90), 0))
-- correct: rebuild from position only, then rotate to actually look at the other spot
local lookTarget = originCFrame.Position + Vector3.new(isRoleA and -1 or 1, 0, 0)
turnCFrame = CFrame.new(mySpot.Position, Vector3.new(lookTarget.X, mySpot.Position.Y, mySpot.Position.Z))
tweenRoot(myBits.root, turnCFrame, Config.TurnDuration, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
-- FNF-ready idle stance
local idleTrack = loadAnim(myBits.animator, Config.IdleStanceAnimId, true, Enum.AnimationPriority.Idle)
if idleTrack then idleTrack:Play(0.2) end
task.wait(Config.HoldBeforeSplit)
-- local-only face-cam VS split
playVSFaceCam(myChar, altChar, Config.SplitCamDuration)
restoreCamera()
standoffActive = false
end
if Config.CountdownEnabled and not syncRemote then
showCountdown(begin)
elseif syncRemote then
-- server-synced start: fire our readiness, wait for GO from server
syncRemote:FireServer("ready")
local conn
conn = syncRemote.OnClientEvent:Connect(function(cmd)
if cmd == "go" then
conn:Disconnect()
task.spawn(begin)
end
end)
table.insert(rigConns, conn)
else
task.spawn(begin)
end
end
-- ============================================================
-- TRIGGER
-- ============================================================
table.insert(rigConns, UserInputService.InputBegan:Connect(function(input, gp)
if gp then return end
if input.KeyCode == Config.TriggerKey then
runStandoff()
end
end))
print("[Standoff] Loaded. Role: "..Config.Role.." — press "..Config.TriggerKey.Name.." to start, boss man.")No comments yet for this script.



