
KeylessUniversal

--[[
╔══════════════════════════════════════════════════════╗
║ FNF ANIMATION INJECTOR v6.2 ║
║ 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) ║
║ + Dynamic Spacing System (search/toggle/presets) ║
║ + NEW: Full Silhouette Mode (CAM tab) ║
╚══════════════════════════════════════════════════════╝
]]
-- ============================================================
-- SERVICES & GLOBALS
-- ============================================================
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")
local Lighting = game:GetService("Lighting")
local Workspace = game:GetService("Workspace")
local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer:WaitForChild("PlayerGui")
-- Single namespace table for every top-level function.
-- Luau allows only 200 locals per function scope; this keeps the
-- main chunk far under that ceiling.
local F = {}
-- ============================================================
-- 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
table.insert(CurrentBindings, { dir = v.dir, key = v.key, id = v.id, speedMult = 1, holdLoop = false,
alts = {}, -- extra animations for this note: { id = "...", chance = n }
mode = "CYCLE", -- CYCLE / RANDOM / WEIGHTED
cycleIdx = 0 })
end
-- ============================================================
-- SONG SECTIONS — switchable animation sets
-- ============================================================
local sections = {
{ name = "Default", ids = { LEFT="204062532", DOWN="204292303", UP="183412246", RIGHT="218504594" } },
{ name = "Intro", ids = { LEFT="0", DOWN="0", UP="0", RIGHT="0" } },
{ name = "Verse", ids = { LEFT="0", DOWN="0", UP="0", RIGHT="0" } },
{ name = "Chorus", ids = { LEFT="0", DOWN="0", UP="0", RIGHT="0" } },
{ name = "Bridge", ids = { LEFT="0", DOWN="0", UP="0", RIGHT="0" } },
}
local currentSectionIdx = 1
local sectionNextKey = "RightBracket" -- ] to go next
local sectionPrevKey = "LeftBracket" -- [ to go prev
-- ============================================================
-- STATE
-- ============================================================
local SpacingSystem -- ★ forward-declared so F.setupCharacter can see it
local SPACING -- forward-declared: the save code above its definition needs it
local INJECTOR_VERSION = "6.2" -- up here so everything can read it
local animTracks = {}
local idleTrack = nil
local connections = {}
local animator = nil
local character = nil
local spacing = nil
local rebindingRow = nil
local guiVisible = true
local idleLooping = false
-- EMOTE CUTTER STATE
local cutterEnabled = false
local cutterAnimId = "138338762910112"
local cutterTrack = nil
local cutterSlices = {
LEFT = { start = 0, stop = 0.5, id = "" },
DOWN = { start = 0.5, stop = 1.0, id = "" },
UP = { start = 1.0, stop = 1.5, id = "" },
RIGHT = { start = 1.5, stop = 2.0, id = "" },
}
local cutterPreviewTrack = nil
-- SOLO MODE STATE
local soloActive = false
local soloKey = "G" -- default keybind
local soloCamDist = 6 -- how close the camera zooms in
local soloCamHeight = 2 -- height offset during solo
local soloCamFOV = 45 -- tight FOV for drama
local soloLetterbox = 0.12 -- letterbox bar size during solo
local soloTransition = 0.6 -- seconds to ease in/out
-- ============================================================
-- SILHOUETTE MODE STATE (rebuilt)
-- ============================================================
function F.silZ(n) return math.clamp(n, 0, 200) end -- hard ZIndex ceiling
local silhouetteActive = false
local silhouetteKey = "H"
local silhouetteBlend = 0
local SilFX = {
strength = 1, -- master intensity 0..1
fadeSpeed = 4, -- how fast it eases in/out
includePlayer = true,
includeOpponent = true,
alwaysOnTop = true, -- cut-outs render through geometry
fillColor = Color3.new(0, 0, 0),
}
-- runtime handles + methods live here so we don't burn top-level locals
local Sil = { cc = nil, blur = nil, conn = nil, highlights = {}, flashGui = nil }
-- ============================================================
-- CHAOS CAMERA STATE
-- Everything lives on one table — the main chunk is near
-- Luau's 200-local ceiling, so this costs a single register.
-- ============================================================
-- ============================================================
-- HEALTH BAR STATE — FNF-style drain with sliding icons
-- ============================================================
local Health = {
enabled = true,
key = "B",
hp = 0.5, -- 0 = dead, 1 = maxed
display = 0.5, -- smoothed value the bar actually draws
gainPerNote = 0.035, -- your note pushes you forward
drainPerPose = 0.055, -- their pose pushes you back
passiveDrain = 0.012, -- per second
smooth = 8,
playerIcon = "", oppIcon = "", -- optional decal IDs
-- drive the real Roblox health bar from the rap battle
lowVignette = true, -- red screen edges as the bar empties
vignetteAt = 0.2, -- starts creeping in below this much health
vig = {},
syncRoblox = true, -- your Humanoid health follows this bar
respectDamage = true, -- real game damage also lowers the rap health
lastWritten = nil, -- what we last set, so we can spot outside damage
gui = nil, fillP = nil, fillO = nil,
iconP = nil, iconO = nil, warn = nil, conn = nil,
}
-- ============================================================
-- PERFECT DOUBLE NOTE STATE — afterimage on a frame-tight double
-- ============================================================
local Ghost = {
enabled = true,
window = 0.02, -- seconds; two presses inside this = perfect
life = 0.45, -- how long the afterimage lasts
distance = 6, -- studs it travels
lastPress = 0, lastDir = nil, lastSpawn = 0,
tint = Color3.fromRGB(120, 240, 255),
}
-- ============================================================
-- RIDE STATE v3 — hard lock to an object
-- ============================================================
local Ride = {
enabled = false, -- true while locked on
key = "R",
walkWith = true, -- walk around on the object
jumpWith = true, -- jump and land back on it, still carried
clampEdges = true, -- can't walk off the edge
edgeMargin = 0.8,
gravityScale = 1,
pickRadius = 14, -- if nothing's under you, grab the nearest part
shake = true, wind = true, speedFov = true, gLean = true,
maxSpeedAt = 120, minSpeed = 4,
shakeScale = 1, windScale = 1, leanScale = 1,
-- live
part = nil,
localPos = Vector3.new(0, 0, 0), -- where you stand, in the object's own space
yaw = 0, -- which way you face, in the object's space
yOff = 0, vy = 0, grounded = true, -- jump arc, also in object space
lastPos = nil, lastVel = Vector3.new(0, 0, 0), lateralG = 0,
speed = 0, smooth = 0,
wasAnchored = false, walkTrack = nil, jumpConn = nil,
stepConn = nil, -- runs just before physics
platVel = Vector3.new(0, 0, 0), -- what the object is doing, world space
walkVel = Vector3.new(0, 0, 0), -- your own walking, world space
savedAutoRotate = nil, savedStates = nil,
-- ping compensation: other players see you one round trip behind the object
pingComp = true, -- send your position ahead of the object by your ping
pingScale = 1, -- 1 = exactly your ping; tune up or down to taste
maxLead = 0.6, -- never lead by more than this many seconds
pingMs = 0, leadStuds = 0, lastPingT = 0,
pingSource = "MINE", -- MINE is the right number; AVERAGE kept as an option
extraMs = 50, -- your screen shows other objects slightly late; this covers that
pingCount = 1, pingOthers = false, warnedOthers = false,
leadTime = 0, -- seconds ahead the replicated pose is predicted
lastCF = nil, -- the object's pose last frame
platAng = Vector3.new(0, 0, 0), -- the object's spin, world space (rad/s)
turnDeg = 0, -- spin in degrees/s, for the readout
-- our own animations while locked (the default falling one would win otherwise)
tracks = nil, currentAnim = nil,
bindName = "FNFRideLock",
gui = nil, streaks = {}, readout = nil,
}
-- ============================================================
-- CUTTER PREVIEW STATE — scrub an animation frame by frame
-- ============================================================
local Cut = {
vp = nil, wm = nil, cam = nil, clone = nil, animator = nil, track = nil,
length = 0, -- seconds, read from the loaded track
playhead = 0,
playing = false,
loopSlice = false, -- when playing, loop only the selected slice
selDir = "LEFT",
yaw = 0, -- preview model rotation
fps = 30, -- frame step for the < > buttons
-- every note can have its own animation; blank means "use the default"
editId = "", -- the animation currently loaded in the preview
tracks = {}, -- [animId] = gameplay track on your own animator
trackOwner = nil, -- the Animator those tracks were loaded on
idBoxes = {},
-- gameplay response
reach = 0.1, -- seconds a slice takes to reach its pose when you press
snap = false, -- skip straight to the pose, no motion at all
sliceGen = 0, -- bumps on every press so stale watchers bow out
-- play the slice back to its start frame after it lands
reverse = "OFF", -- OFF / AUTO / RELEASE
returnPause = 0.08, -- AUTO: how long it sits on the pose before going back
returnTime = 0.25, -- seconds the trip back takes
sliceStart = 0, -- start frame of the slice currently playing
holdAfterReturn = true, -- freeze on the start frame once it's back
conn = nil,
-- ui handles, filled in by the tab builder
bar = nil, head = nil, timeLbl = nil, frameLbl = nil,
sliceFrames = {}, startBoxes = {}, stopBoxes = {}, dirBtns = {},
playBtn = nil, lenLbl = nil, statusLbl = nil,
}
-- ============================================================
-- POSE HOLD STATE
-- ============================================================
local Hold = {
enabled = true,
duration = 5, -- seconds a short pose stays up after you let go
shortAt = 1.5, -- clips this long or shorter count as "short"
holdAll = false, -- ignore the length check, hold everything
cutter = true, -- hold cutter slices too
fadeOut = 0.18, -- blend when the hold expires naturally
untilNext = false, -- ignore the timer: hold until the next pose replaces it
track = nil, label = nil, releaseAt = 0, token = 0, conn = nil,
}
-- ============================================================
-- POSE RESPONSE STATE
-- ============================================================
local Pose = {
reach = 0.06, -- seconds a pose takes to land (was a fixed 0.17)
snap = false, -- land on the final frame instantly, no motion
cache = {}, -- [animId..loopFlag] = track, so a pose never reloads
owner = nil, -- the Animator the cache belongs to
-- plain dodge animation, played from the BINDS tab key
dodgeKey = "Q",
dodgeId = "",
dodgeSpeed = 1,
}
-- ============================================================
-- ARROW HUD STATE — draggable strip + travelling notes
-- ============================================================
local Arrows = {
-- anchor: scale picks the screen corner, offset nudges from it
x = 0.5, y = 1, offX = 0, offY = -82,
size = 64, hitSize = 74, gap = 14,
draggable = true,
noteEnabled = true,
noteDir = "DOWN", -- DOWN or UP
noteSpeed = 420, -- pixels per second
noteLife = 0.55,
trail = true,
beatBob = true, -- strip breathes on the beat
splash = true, -- ring pops out of the arrow on hit
combo = true, -- running hit counter above the strip
tilt = true, -- strip leans toward whichever arrow you hit
container = nil, noteLayer = nil, comboLbl = nil,
comboCount = 0, lastHit = 0, tiltVel = 0, tiltPos = 0,
}
-- ============================================================
-- HOLD STREAK STATE — holding a note key grows a streak
-- ============================================================
local Notes = {
enabled = true,
key = "M",
startDelay = 0.12, -- a hold shorter than this is a tap: no streak
fullTime = 1.0, -- seconds of holding to fill the streak
length = 170, -- how long the streak is when full, in pixels
grow = "UP", -- UP: streak grows away from the arrows. DOWN: toward the screen edge
-- while the streak fills
holdHealPerSec = 0.05, -- health per second
scorePerSec = 250,
-- live
held = {}, -- [dir] = { start, tail }
layer = nil, scoreLbl = nil,
score = 0, best = 0,
conn = nil,
}
-- ============================================================
-- INTRO STATE — loading screen + first-time tour
-- ============================================================
local Intro = {
seen = false, -- has this player finished (or skipped) the tour?
backdropOpacity = 0.5, -- loading screen backdrop, 0.5 = 50%
titleOpacity = 0.5, -- the title slams in at 50%
duration = 2.9, -- seconds the loading screen runs
loadGui = nil, loadDone = false, skip = false,
tourGui = nil, step = 1, pulse = nil,
}
-- ============================================================
-- VIDEO PLAYER STATE — local video files, only you see them
-- ============================================================
local Vid = {
folder = "FNFInjector/videos", -- the drop box, inside your executor's workspace
volume = 0.6,
opacity = 0, -- 0 = solid, 1 = invisible
looped = true,
display = "WINDOW", -- WINDOW / FULLSCREEN / BACKDROP
scale = 1, -- window size multiplier
lastFile = "",
-- black screen fade
boKey = "L",
boFadeIn = 0.35, -- seconds to go black
boHold = 0.4, -- seconds fully black
boFadeOut = 2.5, -- seconds to slowly come back
boToken = 0,
-- video stage: you + your opponent drawn over the video
stage = { on = false, gui = nil, vp = nil, cam = nil, me = nil, opp = nil,
prevDisplay = nil, watch = nil },
-- live
gui = nil, window = nil, frame = nil, bar = nil, timeLbl = nil,
boGui = nil, current = nil, api = {},
}
-- ============================================================
-- RESULT STATE — win / lose, your call
-- ============================================================
local Result = {
winKey = "Y",
loseKey = "U",
-- win
cheerId = "", -- blank = your avatar's own cheer (works anywhere)
cheerLoop = false,
cheerTime = 3, -- seconds to keep cheering when looping
-- lose
sitId = "", -- blank = your avatar's own sit
flingSpeed = 96, -- studs/s (96 ≈ 60 mph)
lift = 45, -- upward kick, studs/s
spin = 12, -- tumble, radians/s
direction = "AWAY", -- AWAY from opponent / BACK / RANDOM
getUpAfter = 2.5, -- seconds before you recover
banner = true, -- big YOU WIN / YOU LOSE text
-- live
gui = nil, bannerLbl = nil, winTrack = nil, sitTrack = nil, token = 0,
}
-- ============================================================
-- MOBILE STATE — FNF-style touch arrows
-- ============================================================
local Mobile = {
mode = "AUTO", -- AUTO (phones/tablets only) / ON / OFF
size = 1, -- arrow size multiplier
spread = 0.2, -- gap between the two pairs, as a share of screen width
pairGap = 14, -- pixels between the two arrows in a pair
bottom = 18, -- pixels up from the bottom edge
idleFade = 0.15, -- how see-through an arrow is when you're not touching it
hideStrip = false, -- hide the top arrow strip while the touch arrows are up
-- live
gui = nil, btns = {}, touches = {}, held = {}, active = false,
}
-- ============================================================
-- SAVE SYSTEM STATE v2
-- ============================================================
local Save = {
folder = "FNFInjector",
slot = 1,
slots = 3,
autoSave = true,
interval = 20, -- seconds between autosave checks
useFolder = false, -- set at boot if the executor can make folders
api = {}, -- resolved file functions, see Save.resolveApi
available = false,
lastJson = nil, -- last thing we wrote, so autosave skips no-op writes
lastSaved = "never",
lastLoaded = "never",
lastError = nil,
autoThread = nil,
}
-- ============================================================
-- KEYBIND REGISTRY + ON-SCREEN CHEAT SHEET
-- ============================================================
local Keys = {
overlayKey = "Backquote", -- the ` key, left of 1
beatKey = "Semicolon", -- toggle the beat zoom
overlayOn = false,
opacity = 0.55, -- panel transparency; higher = more see-through
-- intro transitions
intro = {
{ name = "COUNTDOWN", key = "One", label = "3-2-1-GO!" },
{ name = "WIPE", key = "Two", label = "Diagonal wipe" },
{ name = "PIXELATE", key = "Three", label = "Pixel dissolve" },
{ name = "FLASH", key = "Four", label = "Flash bang" },
{ name = "SPLIT", key = "Five", label = "VS split" },
},
-- cinematic presets, in CAM_PRESET_ORDER order
preset = {
{ name = "Two Shot", key = "Six" },
{ name = "Over Shoulder", key = "Seven" },
{ name = "Shot / Reverse", key = "Eight" },
{ name = "Slow Push", key = "Nine" },
{ name = "Low Hero", key = "Zero" },
{ name = "Crane", key = "Minus" },
{ name = "Directional", key = "Equals" },
{ name = "Snappy", key = "Quote" },
{ name = "First Person", key = "BackSlash" },
},
gui = nil, list = nil, panel = nil,
}
-- ============================================================
-- BOSS BATTLE CUTSCENE STATE
-- Opponent towers over you, camera on the floor looking up.
-- ============================================================
local Boss = {
active = false,
key = "N",
oppScale = 2.6, -- how tall the opponent becomes
playerScale = 0.75, -- how small you become
dist = 7, -- camera distance behind you
height = 1.0, -- camera height (low = looking up)
lookHeight = 7, -- how far up the opponent we frame
bars = 0.17, -- letterbox during the cutscene
blend = 0,
savedOpp = 1, savedPlayer = 1,
scaledOpp = nil, scaledPlayer = nil,
}
-- ============================================================
-- SPLIT SCREEN STATE
-- Roblox renders the world through one CurrentCamera, so a real
-- split is impossible. These are ViewportFrames showing cloned
-- characters with their animations mirrored live — no map.
-- ============================================================
local Split = {
enabled = false,
key = "K",
dist = 7, -- how far each viewport camera sits back
height = 2.5,
rotL = -65, -- your clone's yaw in degrees (negative = turned right)
rotR = 65, -- opponent clone's yaw (positive = turned left)
-- territory: the divider slides toward whoever is losing ground
balance = 0.5, -- 0.5 = even split; higher = you own more screen
targetBalance = 0.5,
claimGain = 0.055, -- how much screen one pose claims
recentre = 0.45, -- how fast territory drifts back to even
slideSpeed = 5, -- divider / pane lerp speed
riseAmount = 0.9, -- studs the claiming character lifts
intro = 0, -- 0..1 fade-in so the divider doesn't pop
loopMirrored = true, -- keep mirrored animations looping instead of ending
gui = nil, L = nil, R = nil, conn = nil,
}
local Chaos = {
enabled = false,
beatSync = true,
-- user-facing intensity (0..2 each)
master = 1,
shake = 1,
zoomPunch = 1,
warp = 1,
glitch = 1,
flash = 1,
readability = 1, -- lower = camera allowed to swing further off the character
-- spring state (punch-in -> overshoot -> snap back)
zoomPos = 0, zoomVel = 0,
dollyPos = 0, dollyVel = 0,
rollPos = 0, rollVel = 0,
-- transient kicks / envelopes
kickX = 0, kickY = 0,
impact = 0, rgbSplit = 0, ccSpike = 0,
warpPhase = 0,
-- tunnel transition
tunnelUntil = 0, tunnelDur = 0.9, tunnelFired = false,
-- glitch bookkeeping
tearUntil = 0,
lastHit = 0, hitCount = 0, beatCount = 0,
beatThread = nil,
seed = math.random() * 1000,
gui = nil, cc = nil, layers = {},
}
-- RP / TARGET STATE
local rpModeEnabled = false -- was true while its button said OFF
local currentTarget = nil
local rpMoveStep = 1.5
local rpMinDistance = 3.5
local rpRetreatStep = 1.2
local rpMaxDistance = 25.0
local targetAnimConn = nil
local targetCharAddedConn = nil
-- PROXIMITY BLOCK + FIGHTING SYSTEM STATE
local proximityBlockEnabled = true
local proximityBlockDist = 3.0 -- studs — invisible wall distance
local proximityBlockConn = nil
local fightModeEnabled = false
local fightState = {
active = false,
playerMomentum = 0,
opponentMomentum = 0,
playerHitTimes = {},
opponentHitTimes = {},
lastPlayerHit = 0,
lastOpponentHit = 0,
lastPlayerKeys = {},
lastPlayerKeyTime = 0,
pushPerHit = 0.8,
pushPerDouble = 2.0,
momentumDecay = 0.92,
momentumCap = 12,
streakWindow = 1.8,
doubleNoteWindow = 0.04,
idleTimeout = 2.5,
}
local fightShakeConn = nil
local fightStatusLbl = nil
local legsLocked = false
local savedWalkSpeed = 16
local savedJumpPower = 50
local faceDecal = nil
local defaultFaceId = nil
local faceIds = { LEFT = "", DOWN = "", UP = "", RIGHT = "" }
local sequence = {}
local seqPlaying = false
local seqTrack = nil
local seqRowFrames = {}
local arrowHudFrames = {}
local idleBPM = 100
local BASE_BPM = 58
local StatusState = {
currentAnimName = "—",
currentAnimId = "",
speedMult = 1,
bpm = idleBPM,
playing = false,
}
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 = {}
local RECENTS_CAP = 8
local playlist = {}
local playlistIndex = 1
local playlistPlaying = false
local previewClone = nil
local previewAnimator = nil
local previewCam = nil
local previewAngle = 0
local previewCloneContainer = nil
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) },
Mono = { accent = Color3.fromRGB(255, 255, 255), accent2 = Color3.fromRGB(190, 190, 200) },
}
local currentThemeName = "Pink"
local themedStrokes = {}
local themedBgs = {}
local themedTexts = {}
local rgbCycleOn = false
local vinylImage = nil
local VINYL_DECAL_ID = ""
local vinylRotation = 0
local vinylBaseRPM = 33.3
local crtEnabled = false
local crtOverlay = nil
local micDeviceInput = nil
local micAnalyzer = nil
local micWire = nil
local micAvailable = false
local specBars = {}
local SPEC_BAR_COUNT = 20
local bpmDetectTaps = {}
local BPM_TAP_WINDOW = 8
local detectedBPM = nil
local beatsPerIdleLoop = 4
local musicRemote = nil
local MUSIC_SOUND_NAME = "FNFMusicPlayer"
local musicStatusLbl = nil
local musicRequestTick = 0
local PRIORITY = Enum.AnimationPriority.Action
local FRAME_TARGET = 10
local BLEND_IN = 0
local BLEND_OUT = 0.05
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 = "►" }
local ARROW_SIZE = 64
local ARROW_HIT_SIZE = 74
local ARROW_GAP = 14
local ARROW_ORDER = { "LEFT", "DOWN", "UP", "RIGHT" }
-- ============================================================
-- LOGGING
-- ============================================================
function F.log(m) print("[FNF] "..tostring(m)) end
-- ============================================================
-- CAMERA SYSTEM
-- ============================================================
local CAM_CFG = {
BaseFOV = 70,
MaxZoomFOV = 54,
Distance = 14,
Height = 5,
LookHeight = 3,
FollowSmooth = 7,
FocusBiasSmooth = 5,
ShakeMaxMagnitude = 0.55,
ShakeMaxRotDeg = 1.6,
LetterboxMaxSize = 0.09,
IntensityWindow = 2.2,
IntensityHitsForMax = 9,
GraceBothActive = 0.18,
ReturnToCenterAfter = 0.9,
BarsEnabled = true, -- cinematic letterbox bars on/off
NudgeStrength = 2.5,
NudgeVertical = 1.2,
NudgeDecay = 3.5,
NudgeTilt = 2.5,
HitFOVPunch = 2.0,
HitFOVRecovery = 8,
}
local CameraSystem = {}
CameraSystem.__index = CameraSystem
function CameraSystem.new()
local self = setmetatable({}, CameraSystem)
self.enabled = false
self.playerChar = nil
self.opponentChar = nil
self.focusBias = 0.5
self.targetBias = 0.5
self.lastPlayerHit = 0
self.lastOpponentHit = 0
self.hitTimestamps = {}
self.intensity = 0
self.targetIntensity = 0
self.sensitivity = 1
self.shakeScale = 1
self.zoomScale = 1
self.currentCFrame = nil
self.currentFOV = CAM_CFG.BaseFOV
self.shakeSeed = math.random() * 1000
self.nudgeOffset = Vector3.new(0, 0, 0)
self.targetNudge = Vector3.new(0, 0, 0)
self.nudgeTilt = 0
self.targetTilt = 0
self.fovPunch = 0
self.camDistance = CAM_CFG.Distance
self.camHeight = CAM_CFG.Height
self.camHorizontal = 0
self.camAngle = 0
self.camFlipped = false
self.soloBlend = 0
-- preset / cinematic upgrades
self.preset = "Classic"
self.orbitSpeed = 0 -- how fast the sweep travels
self.orbitAmp = 0 -- degrees the camera arcs around the pair
self.orbitPhase = 0
self.dutch = 0 -- constant roll, in degrees
self.handheld = 0 -- always-on operator drift
self.lead = 0 -- camera leans toward whoever is singing
self.mode = "STATIC" -- STATIC / OTS / CUTS / PUSH / CRANE / FP / DIR
self.dirBoost = 3.2 -- how hard DIR mode throws the frame per note
self.dirHold = 1.4 -- lower = the kick lingers longer
-- upgraded follow
-- hit reactions (the camera update)
self.snapHits = false -- kicks land instantly, after smoothing
self.nudgeScale = 1 -- how far a note throws the frame
self.tiltScale = 1 -- how much LEFT/RIGHT roll the frame
self.punchScale = 1 -- FOV dip per hit
self.returnSpeed = 1 -- how fast the frame springs home
self.rideShake = 0 -- 0..1, fed by Ride from platform speed
self.rideFov = 0 -- extra FOV at speed
self.rideLean = 0 -- roll, in degrees, under lateral g
self.beatZoom = false -- pulse the lens on every beat
self.beatPower = 14 -- FOV dip per beat
self.beatAccent = 1.7 -- downbeat multiplier (every 4th)
self.bzZoom = 0 -- beat zoom spring
self.bzVel = 0
self.beatThread = nil
self.beatCount = 0
self.dnZoom = 0 -- double-note zoom, in FOV degrees
self.dnZoomVel = 0
self.dnPower = 52 -- how hard a double note dives the lens
self.springFollow = true -- critically damped position spring
self.springStiff = 9
self.camVel = Vector3.new(0, 0, 0)
self.leadTime = 0.18 -- anticipates where the characters are heading
self.autoFrame = true -- widens the shot as they separate
self.pitchBias = 0 -- raises the look target (low-angle hero framing)
self.pushAmt = 0 -- dolly-in progress, 0..1
self.cranePhase = 0
self.lastCutSubject = nil
self.lastCutTime = 0
self.settleEnabled = false
self.settleAfter = 1.6 -- seconds of silence before the camera locks
self.settled = false
self.conn = nil
self.letterTop = nil
self.letterBottom = nil
self.statusCb = nil
return self
end
-- Real shot grammar, not a permanent orbit. Each preset is a technique
-- you'd actually see in a film or a cutscene.
local CAM_PRESETS = {
["Two Shot"] = {
mode = "STATIC", camDistance = 13, camHeight = 4.5, fov = 68,
orbitSpeed = 0, orbitAmp = 0, dutch = 0, handheld = 0,
lead = 0.5, pitchBias = 0, settleAfter = 1.2,
desc = "Locked wide holding both of you. The default coverage shot.",
},
["Over Shoulder"] = {
mode = "OTS", camDistance = 9, camHeight = 3.4, fov = 62,
orbitSpeed = 0, orbitAmp = 0, dutch = 0, handheld = 0.18,
lead = 0.3, pitchBias = 0.4, settleAfter = 1.6,
desc = "Sits behind the listener's shoulder, framed on whoever is singing.",
},
["Shot / Reverse"] = {
mode = "CUTS", camDistance = 6, camHeight = 3.2, fov = 55,
orbitSpeed = 0, orbitAmp = 0, dutch = 0, handheld = 0.22,
lead = 0, pitchBias = 0.5, settleAfter = 1.8,
desc = "Hard cuts between close-ups as the line passes back and forth.",
},
["Slow Push"] = {
mode = "PUSH", camDistance = 14, camHeight = 4.8, fov = 66,
orbitSpeed = 0, orbitAmp = 0, dutch = 0, handheld = 0.1,
lead = 0.7, pitchBias = 0, settleAfter = 2.2,
desc = "Creeps in through the phrase, drifts back out when it ends.",
},
["Low Hero"] = {
mode = "STATIC", camDistance = 9, camHeight = 1.1, fov = 62,
orbitSpeed = 0, orbitAmp = 0, dutch = 2, handheld = 0.12,
lead = 0.6, pitchBias = 2.6, settleAfter = 1.4,
desc = "Camera on the floor looking up. Makes you look enormous.",
},
Crane = {
mode = "CRANE", camDistance = 15, camHeight = 6, fov = 70,
orbitSpeed = 0, orbitAmp = 0, dutch = 0, handheld = 0,
lead = 0.6, pitchBias = 0, settleAfter = 2.5,
desc = "Slow vertical jib. Rises and falls, never spins.",
},
Directional = {
mode = "DIR", camDistance = 11, camHeight = 4, fov = 72,
orbitSpeed = 0, orbitAmp = 0, dutch = 0, handheld = 0.15,
lead = 0.5, pitchBias = 0.3, settleAfter = 1.5,
desc = "The frame kicks the way each note points, then eases home.",
},
Snappy = {
mode = "SNAP", camDistance = 11, camHeight = 4.2, fov = 70,
orbitSpeed = 0, orbitAmp = 0, dutch = 0, handheld = 0.1,
lead = 0.5, pitchBias = 0.2, settleAfter = 1.4,
dirBoost = 2.4, dirHold = 2.6, snapHits = true,
desc = "Every note snaps the frame the way it points, then springs straight back.",
},
["First Person"] = {
mode = "FP", camDistance = 0, camHeight = 0, fov = 82,
orbitSpeed = 0, orbitAmp = 0, dutch = 0, handheld = 0.5,
lead = 0, pitchBias = 0, settleAfter = 6,
desc = "Straight out of your own eyes, locked on your opponent.",
},
}
local CAM_PRESET_ORDER = { "Two Shot", "Over Shoulder", "Shot / Reverse",
"Slow Push", "Low Hero", "Crane", "Directional", "Snappy", "First Person" }
function CameraSystem:ApplyPreset(name)
local p = CAM_PRESETS[name]
if not p then return end
self.preset = name
self.mode = p.mode
self.pitchBias = p.pitchBias
self.pushAmt = 0
self.camDistance = p.camDistance
self.camHeight = p.camHeight
self.orbitSpeed = p.orbitSpeed
self.orbitAmp = p.orbitAmp
self.dutch = p.dutch
self.handheld = p.handheld
self.lead = p.lead
self.settleAfter = p.settleAfter
CAM_CFG.BaseFOV = p.fov
if p.dirBoost then self.dirBoost = p.dirBoost end
if p.dirHold then self.dirHold = p.dirHold end
if p.snapHits ~= nil then self.snapHits = p.snapHits end
self.currentCFrame = nil -- cut cleanly into the new shot
self.settled = false
end
function CameraSystem:BuildLetterbox(playerGui)
local sg = playerGui:FindFirstChild("FNFCameraFX")
if sg then sg:Destroy() end
sg = Instance.new("ScreenGui")
sg.Name = "FNFCameraFX"
sg.IgnoreGuiInset = true
sg.DisplayOrder = 50
sg.ResetOnSpawn = false
sg.Parent = playerGui
local top = Instance.new("Frame")
top.Name = "LetterboxTop"
top.BackgroundColor3 = Color3.new(0, 0, 0)
top.BorderSizePixel = 0
top.Size = UDim2.new(1, 0, 0, 0)
top.Position = UDim2.new(0, 0, 0, 0)
top.ZIndex = 100
top.Parent = sg
local bottom = Instance.new("Frame")
bottom.Name = "LetterboxBottom"
bottom.BackgroundColor3 = Color3.new(0, 0, 0)
bottom.BorderSizePixel = 0
bottom.Size = UDim2.new(1, 0, 0, 0)
bottom.AnchorPoint = Vector2.new(0, 1)
bottom.Position = UDim2.new(0, 0, 1, 0)
bottom.ZIndex = 100
bottom.Parent = sg
self.letterTop, self.letterBottom = top, bottom
end
function CameraSystem:SetPlayerCharacter(char) self.playerChar = char end
function CameraSystem:SetOpponentCharacter(char)
self.opponentChar = char
if not char then self.targetBias = 0 end
end
function CameraSystem:SetSensitivity(mult) self.sensitivity = math.clamp(mult, 0.25, 3) end
function CameraSystem:SetShakeScale(pct01) self.shakeScale = math.clamp(pct01, 0, 1) end
function CameraSystem:SetZoomScale(pct01) self.zoomScale = math.clamp(pct01, 0, 1) end
function CameraSystem:RegisterHit(who)
local now = os.clock()
Split.claim(who)
Health.onHit(who)
table.insert(self.hitTimestamps, now)
if who == "PLAYER" then
self.lastPlayerHit = now
elseif who == "OPPONENT" then
self.lastOpponentHit = now
end
end
function CameraSystem:RegisterDirectionalHit(dir)
if not self.enabled then return end
local boost = (self.mode == "DIR" or self.mode == "SNAP") and self.dirBoost or 1
local s = CAM_CFG.NudgeStrength * boost * self.nudgeScale
local v = CAM_CFG.NudgeVertical * boost * self.nudgeScale
local tilt = CAM_CFG.NudgeTilt * boost * self.tiltScale
if dir == "LEFT" then
self.targetNudge = Vector3.new(-s, 0, 0)
self.targetTilt = math.rad(tilt)
elseif dir == "RIGHT" then
self.targetNudge = Vector3.new(s, 0, 0)
self.targetTilt = math.rad(-tilt)
elseif dir == "UP" then
self.targetNudge = Vector3.new(0, v, 0)
self.targetTilt = 0
elseif dir == "DOWN" then
self.targetNudge = Vector3.new(0, -v, 0)
self.targetTilt = 0
end
self.fovPunch = CAM_CFG.HitFOVPunch * boost * self.punchScale
if self.snapHits then
-- no ease-in: the frame is AT the kick on the same frame you press
self.nudgeOffset = self.targetNudge
self.nudgeTilt = self.targetTilt
end
self.settled = false
Chaos.hit(1, dir)
end
-- Hard lens dive on a double note. Independent of Chaos, so it fires
-- even with the chaos camera switched off.
function CameraSystem:DoubleNoteZoom(power)
if not self.enabled then return end
power = power or 1
self.dnZoomVel = self.dnZoomVel - self.dnPower * power
self.settled = false
end
-- Pulses the lens in time with the current BPM. Reads StatusState.bpm each
-- beat, so changing tempo retimes it without restarting anything.
function CameraSystem:SetBeatZoom(state)
self.beatZoom = state
if state and not self.beatThread then
self.beatThread = task.spawn(function()
while self.beatZoom do
local bpm = math.max(40, StatusState.bpm or 100)
task.wait(60 / bpm)
if not self.beatZoom then break end
self.beatCount = (self.beatCount + 1) % 4
local accent = (self.beatCount == 0) and self.beatAccent or 1
self.bzVel = self.bzVel - self.beatPower * accent
self.settled = false
end
self.beatThread = nil
end)
end
if not state then
self.bzZoom, self.bzVel = 0, 0
end
end
function CameraSystem:_recomputeIntensity(now)
for i = #self.hitTimestamps, 1, -1 do
if now - self.hitTimestamps[i] > CAM_CFG.IntensityWindow then
table.remove(self.hitTimestamps, i)
end
end
local hitsForMax = math.max(2, CAM_CFG.IntensityHitsForMax / self.sensitivity)
self.targetIntensity = math.clamp(#self.hitTimestamps / hitsForMax, 0, 1)
end
function CameraSystem:_updateLetterbox()
if not self.letterTop or not self.letterBottom then return end
local frac = CAM_CFG.BarsEnabled and (CAM_CFG.LetterboxMaxSize * self.intensity) or 0
self.letterTop.Size = UDim2.new(1, 0, frac, 0)
self.letterBottom.Size = UDim2.new(1, 0, frac, 0)
end
function CameraSystem:_update(dt)
if not self.enabled then return end
local cam = Workspace.CurrentCamera
if not cam then return end
local pRoot = self.playerChar and self.playerChar:FindFirstChild("HumanoidRootPart")
if not pRoot then return end
local oRoot = self.opponentChar and self.opponentChar:FindFirstChild("HumanoidRootPart")
local now = os.clock()
local recentP = (now - self.lastPlayerHit) < CAM_CFG.GraceBothActive
local recentO = (now - self.lastOpponentHit) < CAM_CFG.GraceBothActive
local silentFor = math.min(now - self.lastPlayerHit, now - self.lastOpponentHit)
-- Once both sides stop and the shake has died out, freeze exactly where
-- we are. No drift, no noise, no re-framing until someone hits again.
local quietFor = now - math.max(self.lastPlayerHit, self.lastOpponentHit)
if self.settleEnabled and self.currentCFrame and not soloActive and not Boss.active
and quietFor > self.settleAfter and self.intensity < 0.03
and self.rideShake < 0.02 then
if not self.settled then
self.settled = true
self.targetNudge = Vector3.new(0, 0, 0)
self.targetTilt = 0
end
cam.CFrame = self.currentCFrame
cam.FieldOfView = self.currentFOV
if self.letterTop then self.letterTop.Size = UDim2.new(1, 0, 0, 0) end
if self.letterBottom then self.letterBottom.Size = UDim2.new(1, 0, 0, 0) end
if self.statusCb then self.statusCb("SETTLED", 0) end
return
end
self.settled = false
if oRoot then
if recentP and recentO then
self.targetBias = 0.5
elseif recentP then
self.targetBias = 0.0
elseif recentO then
self.targetBias = 1.0
elseif silentFor > CAM_CFG.ReturnToCenterAfter then
self.targetBias = 0.5
end
else
self.targetBias = 0.0
end
local biasAlpha = 1 - math.exp(-CAM_CFG.FocusBiasSmooth * dt)
self.focusBias = self.focusBias + (self.targetBias - self.focusBias) * biasAlpha
local playerPos = pRoot.Position
local oppPos = oRoot and oRoot.Position or playerPos
-- lead the framing by where each character is actually heading, so the
-- camera stops trailing behind fast movement
local leadP, leadO = Vector3.new(), Vector3.new()
if self.leadTime > 0 then
local pv = pRoot.AssemblyLinearVelocity
leadP = Vector3.new(pv.X, 0, pv.Z) * self.leadTime
if leadP.Magnitude > 6 then leadP = leadP.Unit * 6 end
if oRoot then
local ov = oRoot.AssemblyLinearVelocity
leadO = Vector3.new(ov.X, 0, ov.Z) * self.leadTime
if leadO.Magnitude > 6 then leadO = leadO.Unit * 6 end
end
end
local focusPoint = (playerPos + leadP):Lerp(oppPos + leadO, self.focusBias)
+ Vector3.new(0, CAM_CFG.LookHeight, 0)
local axis
if oRoot then
local flat = Vector3.new(oppPos.X - playerPos.X, 0, oppPos.Z - playerPos.Z)
if flat.Magnitude > 1e-3 then
axis = Vector3.new(-flat.Unit.Z, 0, flat.Unit.X)
end
end
if not axis then
axis = pRoot.CFrame.LookVector * -1
end
if self.camFlipped then
axis = -axis
end
if self.camAngle ~= 0 then
local rad = math.rad(self.camAngle)
local cosA, sinA = math.cos(rad), math.sin(rad)
axis = Vector3.new(axis.X * cosA - axis.Z * sinA, 0, axis.X * sinA + axis.Z * cosA).Unit
end
-- orbit sweep: arcs the camera around the pair, faster while it's hot
if self.orbitAmp > 0 then
self.orbitPhase = self.orbitPhase + dt * self.orbitSpeed * (0.4 + self.intensity * 1.7)
local sweep = math.sin(self.orbitPhase) * math.rad(self.orbitAmp)
local cs, sn = math.cos(sweep), math.sin(sweep)
axis = Vector3.new(axis.X * cs - axis.Z * sn, 0, axis.X * sn + axis.Z * cs).Unit
end
-- lead: lean the framing toward whoever is singing
if self.lead > 0 and oRoot then
local leanDir = (oppPos - playerPos)
leanDir = Vector3.new(leanDir.X, 0, leanDir.Z)
if leanDir.Magnitude > 1e-3 then
focusPoint = focusPoint + leanDir.Unit * ((self.focusBias - 0.5) * 2 * self.lead)
end
end
local separation = oRoot and (playerPos - oppPos).Magnitude or 0
local dynamicDistance = self.camDistance + separation * 0.18
local autoHeight = self.camHeight
if self.autoFrame then
-- rise a little as they spread out so both stay comfortably in frame
autoHeight = autoHeight + math.clamp(separation * 0.06, 0, 3)
end
local desiredPos = focusPoint + axis * dynamicDistance + Vector3.new(0, autoHeight, 0)
if self.camHorizontal ~= 0 then
local slideDir = Vector3.new(0, 1, 0):Cross(axis)
if slideDir.Magnitude > 1e-3 then
desiredPos = desiredPos + slideDir.Unit * self.camHorizontal
end
end
-- raise the look target for low-angle framing
focusPoint = focusPoint + Vector3.new(0, self.pitchBias, 0)
-- ---- boss cutscene overrides everything ----
local mode = self.mode
if Boss.active and oRoot then
local toOpp = Vector3.new(oppPos.X - playerPos.X, 0, oppPos.Z - playerPos.Z)
local dir = (toOpp.Magnitude > 1e-3) and toOpp.Unit or pRoot.CFrame.LookVector
desiredPos = playerPos - dir * Boss.dist + Vector3.new(0, Boss.height, 0)
local desiredCF = CFrame.new(desiredPos, oppPos + Vector3.new(0, Boss.lookHeight, 0))
if not self.currentCFrame then self.currentCFrame = desiredCF end
self.currentCFrame = self.currentCFrame:Lerp(desiredCF, 1 - math.exp(-4 * dt))
self.currentFOV = self.currentFOV + (64 - self.currentFOV) * (1 - math.exp(-4 * dt))
if self.letterTop then self.letterTop.Size = UDim2.new(1, 0, Boss.bars, 0) end
if self.letterBottom then self.letterBottom.Size = UDim2.new(1, 0, Boss.bars, 0) end
local bcf, bfov = self.currentCFrame, self.currentFOV
if Chaos.enabled then bcf, bfov = Chaos.apply(bcf, bfov, dt) end
cam.CFrame = bcf; cam.FieldOfView = bfov
if self.statusCb then self.statusCb("BOSS", self.intensity) end
return
end
-- ---- shot grammar ----
local subjectPos, subjectCF, otherPos
if self.focusBias > 0.5 and oRoot then
subjectPos, subjectCF, otherPos = oppPos, oRoot.CFrame, playerPos
else
subjectPos, subjectCF, otherPos = playerPos, pRoot.CFrame, (oRoot and oppPos or playerPos)
end
if mode == "PUSH" then
-- creeps in while someone is singing, eases back out when quiet
local wantPush = (silentFor < 0.7) and 1 or 0
self.pushAmt = self.pushAmt + (wantPush - self.pushAmt) * (1 - math.exp(-1.6 * dt))
dynamicDistance = dynamicDistance * (1 - 0.3 * self.pushAmt)
desiredPos = focusPoint + axis * dynamicDistance + Vector3.new(0, autoHeight, 0)
elseif mode == "CRANE" then
self.cranePhase = self.cranePhase + dt * 0.28
local lift = math.sin(self.cranePhase) * 3.2
desiredPos = focusPoint + axis * dynamicDistance + Vector3.new(0, autoHeight + lift, 0)
elseif mode == "OTS" and oRoot then
-- behind the listener's shoulder, framed past them onto the singer
local otherRoot = (subjectPos == oppPos) and pRoot or oRoot
local ocf = otherRoot.CFrame
desiredPos = ocf.Position
- ocf.LookVector * 2.4
+ ocf.RightVector * 1.7
+ Vector3.new(0, 2.7, 0)
focusPoint = subjectPos + Vector3.new(0, 2.2 + self.pitchBias, 0)
elseif mode == "CUTS" then
-- close-up in front of whoever is singing; hard cut when it flips
local key = (subjectPos == oppPos) and "OPP" or "PLR"
if key ~= self.lastCutSubject and (now - self.lastCutTime) > 0.35 then
self.lastCutSubject = key
self.lastCutTime = now
self.currentCFrame = nil -- nil forces an instant cut, not a pan
self.camVel = Vector3.new(0, 0, 0)
end
desiredPos = subjectPos
+ subjectCF.LookVector * self.camDistance
+ subjectCF.RightVector * 1.4
+ Vector3.new(0, self.camHeight, 0)
focusPoint = subjectPos + Vector3.new(0, 2.3 + self.pitchBias, 0)
elseif mode == "FP" then
local head = self.playerChar and self.playerChar:FindFirstChild("Head")
local eye = head and head.CFrame or (pRoot.CFrame + Vector3.new(0, 1.6, 0))
desiredPos = eye.Position + eye.LookVector * 0.35
focusPoint = oRoot and (oppPos + Vector3.new(0, 2.2 + self.pitchBias, 0))
or (desiredPos + eye.LookVector * 12)
end
local desiredCFrame = CFrame.new(desiredPos, focusPoint)
local soloTarget = soloActive and 1 or 0
local soloAlpha = 1 - math.exp(-3 / math.max(soloTransition, 0.1) * dt)
self.soloBlend = self.soloBlend + (soloTarget - self.soloBlend) * soloAlpha
if self.soloBlend > 0.001 then
local soloFocus = playerPos + Vector3.new(0, CAM_CFG.LookHeight + 0.5, 0)
local soloAxis = pRoot.CFrame.LookVector
local soloPos = soloFocus - soloAxis * soloCamDist + Vector3.new(0, soloCamHeight, 0)
local soloCFrame = CFrame.new(soloPos, soloFocus)
desiredCFrame = desiredCFrame:Lerp(soloCFrame, self.soloBlend)
end
if self.letterTop and self.letterBottom then
local normalBar = CAM_CFG.LetterboxMaxSize * self.intensity
local soloBar = soloLetterbox * self.soloBlend
local barSize = CAM_CFG.BarsEnabled and math.max(normalBar, soloBar) or 0
self.letterTop.Size = UDim2.new(1, 0, barSize, 0)
self.letterBottom.Size = UDim2.new(1, 0, barSize, 0)
end
self:_recomputeIntensity(now)
local intensityAlpha = 1 - math.exp(-3 * dt)
self.intensity = self.intensity + (self.targetIntensity - self.intensity) * intensityAlpha
local shakeMag = CAM_CFG.ShakeMaxMagnitude * self.shakeScale * self.intensity
local shakeRot = CAM_CFG.ShakeMaxRotDeg * self.shakeScale * self.intensity
-- riding a fast platform adds its own rattle on top, at a higher frequency
shakeMag = shakeMag + self.rideShake * 0.9
shakeRot = shakeRot + self.rideShake * 2.2
local t = now * (14 + self.rideShake * 26)
local shakeOffset = Vector3.new(
(math.noise(t, self.shakeSeed) - 0.5) * 2 * shakeMag,
(math.noise(t, self.shakeSeed + 30) - 0.5) * 2 * shakeMag,
0
)
local shakeYaw = math.rad((math.noise(t, self.shakeSeed + 60) - 0.5) * 2 * shakeRot)
local shakePitch = math.rad((math.noise(t, self.shakeSeed + 90) - 0.5) * 2 * shakeRot)
local holdMul = ((self.mode == "DIR" or self.mode == "SNAP") and self.dirHold or 1) * self.returnSpeed
local decayRate = CAM_CFG.NudgeDecay * holdMul
local nudgeAlpha = 1 - math.exp(-decayRate * dt)
if self.snapHits then
-- already at full kick from the hit itself, so just return home
local k = math.exp(-decayRate * 1.6 * dt)
self.nudgeOffset = self.nudgeOffset * k
self.nudgeTilt = self.nudgeTilt * k
self.targetNudge = Vector3.new(0, 0, 0)
self.targetTilt = 0
else
self.nudgeOffset = self.nudgeOffset:Lerp(self.targetNudge, nudgeAlpha)
self.nudgeTilt = self.nudgeTilt + (self.targetTilt - self.nudgeTilt) * nudgeAlpha
self.targetNudge = self.targetNudge:Lerp(Vector3.new(0,0,0), nudgeAlpha * 0.7)
self.targetTilt = self.targetTilt + (0 - self.targetTilt) * nudgeAlpha * 0.7
end
self.fovPunch = self.fovPunch + (0 - self.fovPunch)
* (1 - math.exp(-CAM_CFG.HitFOVRecovery * self.returnSpeed * dt))
local camRight = desiredCFrame.RightVector
local camUp = desiredCFrame.UpVector
local nudgeWorld = camRight * self.nudgeOffset.X + camUp * self.nudgeOffset.Y
-- constant operator drift, independent of hit intensity
if self.handheld > 0 then
local ht = now * 0.85
shakeOffset = shakeOffset + Vector3.new(
math.noise(ht, self.shakeSeed + 200) * self.handheld * 0.55,
math.noise(ht, self.shakeSeed + 260) * self.handheld * 0.4,
0)
shakePitch = shakePitch + math.rad(math.noise(ht, self.shakeSeed + 320) * self.handheld * 1.2)
shakeYaw = shakeYaw + math.rad(math.noise(ht, self.shakeSeed + 380) * self.handheld * 1.2)
end
local dutchRoll = math.rad(self.dutch) * (0.5 + self.intensity * 0.5) + math.rad(self.rideLean)
-- The follow spring below smooths anything added here. That's right for
-- framing, but it's why hits used to feel mushy: the kick got eased in.
-- Snappy hits skip this and are applied after smoothing instead.
local preNudge = self.snapHits and Vector3.new(0, 0, 0) or nudgeWorld
local preTilt = self.snapHits and 0 or self.nudgeTilt
desiredCFrame = desiredCFrame * CFrame.new(shakeOffset + preNudge) * CFrame.Angles(shakePitch, shakeYaw, preTilt + dutchRoll)
if not self.currentCFrame then
self.currentCFrame = desiredCFrame
self.camVel = Vector3.new(0, 0, 0)
end
local followAlpha = 1 - math.exp(-CAM_CFG.FollowSmooth * dt)
if self.springFollow then
-- Critically damped spring on position: catches up fast without the
-- rubber-banding a plain lerp gives you at low frame rates.
local cur = self.currentCFrame.Position
local w = self.springStiff
local accel = (desiredCFrame.Position - cur) * (w * w) - self.camVel * (2 * w)
self.camVel = self.camVel + accel * dt
local newPos = cur + self.camVel * dt
-- rotation still eases, so the aim never jitters
local rot = self.currentCFrame:Lerp(desiredCFrame, followAlpha)
self.currentCFrame = CFrame.new(newPos) * (rot - rot.Position)
else
self.currentCFrame = self.currentCFrame:Lerp(desiredCFrame, followAlpha)
end
local zoomAmount = (CAM_CFG.BaseFOV - CAM_CFG.MaxZoomFOV) * self.zoomScale * self.intensity
local normalFOV = CAM_CFG.BaseFOV - zoomAmount - (self.snapHits and 0 or self.fovPunch)
-- double-note zoom spring: dives in fast, springs back out
self.dnZoomVel = self.dnZoomVel + (-150 * self.dnZoom - 14 * self.dnZoomVel) * dt
self.dnZoom = self.dnZoom + self.dnZoomVel * dt
normalFOV = normalFOV + self.dnZoom
-- beat zoom: softer spring so it breathes instead of snapping
self.bzVel = self.bzVel + (-95 * self.bzZoom - 13 * self.bzVel) * dt
self.bzZoom = self.bzZoom + self.bzVel * dt
normalFOV = normalFOV + self.bzZoom
normalFOV = normalFOV + self.rideFov
if self.autoFrame then
normalFOV = normalFOV + math.clamp(separation * 0.25, 0, 8)
end
local targetFOV = normalFOV + (soloCamFOV - normalFOV) * self.soloBlend
targetFOV = math.clamp(targetFOV, 15, 120)
self.currentFOV = self.currentFOV + (targetFOV - self.currentFOV) * followAlpha
local finalCF, finalFOV = self.currentCFrame, self.currentFOV
if self.snapHits then
-- hits land on top of the smoothed frame, untouched by the follow spring
finalCF = finalCF
* CFrame.new(self.nudgeOffset.X, self.nudgeOffset.Y, 0)
* CFrame.Angles(0, 0, self.nudgeTilt)
finalFOV = finalFOV - self.fovPunch
end
if Chaos.enabled then
finalCF, finalFOV = Chaos.apply(finalCF, finalFOV, dt)
end
cam.CFrame = finalCF
cam.FieldOfView = finalFOV
if self.soloBlend <= 0.001 then
self:_updateLetterbox()
end
if self.statusCb then
local label = self.focusBias < 0.33 and "PLAYER" or (self.focusBias > 0.66 and "OPPONENT" or "CENTER")
self.statusCb(label, self.intensity)
end
end
function CameraSystem:SetEnabled(state)
self.enabled = state
local cam = Workspace.CurrentCamera
if state then
if cam then cam.CameraType = Enum.CameraType.Scriptable end
self.currentCFrame = nil
else
if cam then
cam.CameraType = Enum.CameraType.Custom
cam.FieldOfView = CAM_CFG.BaseFOV
end
if self.letterTop then self.letterTop.Size = UDim2.new(1, 0, 0, 0) end
if self.letterBottom then self.letterBottom.Size = UDim2.new(1, 0, 0, 0) end
end
end
function CameraSystem:Start()
if self.conn then return end
self.conn = RunService.RenderStepped:Connect(function(dt) self:_update(dt) end)
end
function CameraSystem:Destroy()
if self.conn then self.conn:Disconnect(); self.conn = nil end
self:SetEnabled(false)
end
local FNFCamera = CameraSystem.new()
-- ============================================================
-- SILHOUETTE MODE — world washes white, fighters go black
-- (all drawn elements clamped to ZIndex/DisplayOrder 200)
-- ============================================================
function Sil.clearOne(char)
if not char then return end
local h = Sil.highlights[char]
if h then pcall(function() h:Destroy() end); Sil.highlights[char] = nil end
end
function Sil.clearAll()
for char in pairs(Sil.highlights) do Sil.clearOne(char) end
end
function Sil.applyTo(char, alpha)
if not char or not char.Parent then return end
local h = Sil.highlights[char]
if not h or h.Parent ~= char then
if h then pcall(function() h:Destroy() end) end
h = Instance.new("Highlight")
h.Name = "FNFSilhouette"
h.Parent = char
Sil.highlights[char] = h
end
h.FillColor = SilFX.fillColor
h.OutlineColor = SilFX.fillColor
h.DepthMode = SilFX.alwaysOnTop
and Enum.HighlightDepthMode.AlwaysOnTop
or Enum.HighlightDepthMode.Occluded
h.FillTransparency = 1 - alpha * 0.92
h.OutlineTransparency = 1 - alpha
end
-- Quick white punch when the mode snaps on. Capped at ZIndex 200.
function Sil.flash()
if Sil.flashGui then Sil.flashGui:Destroy(); Sil.flashGui = nil end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFSilhouetteFlash"; sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false; sg.DisplayOrder = F.silZ(150)
sg.Parent = PlayerGui
Sil.flashGui = sg
local f = Instance.new("Frame")
f.Size = UDim2.new(1, 0, 1, 0)
f.BackgroundColor3 = Color3.new(1, 1, 1)
f.BackgroundTransparency = 0.3
f.BorderSizePixel = 0
f.ZIndex = F.silZ(198)
f.Parent = sg
local tw = TweenService:Create(f, TweenInfo.new(0.35, Enum.EasingStyle.Quint), { BackgroundTransparency = 1 })
tw:Play()
tw.Completed:Connect(function()
sg:Destroy()
if Sil.flashGui == sg then Sil.flashGui = nil end
end)
end
function Sil.start()
if Sil.conn then return end
Sil.cc = Instance.new("ColorCorrectionEffect")
Sil.cc.Name = "FNFSilhouette"
Sil.cc.Brightness = 0; Sil.cc.Contrast = 0; Sil.cc.Saturation = 0
Sil.cc.Parent = Lighting
Sil.blur = Instance.new("BlurEffect")
Sil.blur.Name = "FNFSilhouetteBlur"; Sil.blur.Size = 0
Sil.blur.Parent = Lighting
Sil.conn = RunService.Heartbeat:Connect(function(dt)
local target = silhouetteActive and 1 or 0
silhouetteBlend = silhouetteBlend + (target - silhouetteBlend) * (1 - math.exp(-SilFX.fadeSpeed * dt))
local a = silhouetteBlend * SilFX.strength
if Sil.cc then
Sil.cc.Brightness = a * 3
Sil.cc.Contrast = a * -0.8
Sil.cc.Saturation = a * -4
end
if Sil.blur then Sil.blur.Size = a * 6 end
local opp = currentTarget and currentTarget.Character or nil
if SilFX.includePlayer and character and a > 0.01 then
Sil.applyTo(character, a)
else
Sil.clearOne(character)
end
if SilFX.includeOpponent and opp and a > 0.01 then
Sil.applyTo(opp, a)
else
Sil.clearOne(opp)
end
-- drop highlights for anyone who respawned / left / got unset
for char in pairs(Sil.highlights) do
if not char.Parent or (char ~= character and char ~= opp) then
Sil.clearOne(char)
end
end
if not silhouetteActive and silhouetteBlend < 0.01 then
Sil.clearAll()
if Sil.cc then Sil.cc:Destroy(); Sil.cc = nil end
if Sil.blur then Sil.blur:Destroy(); Sil.blur = nil end
if Sil.conn then Sil.conn:Disconnect(); Sil.conn = nil end
end
end)
end
function Sil.set(state)
if state == silhouetteActive then return end
silhouetteActive = state
if state then Sil.start(); Sil.flash() end
if F.refreshSilUI then F.refreshSilUI() end
if F.notify then
F.notify(state and "🖤 SILHOUETTE MODE" or "Silhouette off", state and "success" or "info")
end
end
-- ============================================================
-- SPLIT SCREEN — player left, opponent right
-- Both cameras are locked still. The clones are yawed inward so
-- the two characters face each other across the divider.
-- ============================================================
function Split.makeSide(parent, xScale, title, tint, rotKey)
local vp = Instance.new("ViewportFrame")
vp.Size = UDim2.new(0.5, -2, 1, 0)
vp.Position = UDim2.new(xScale, xScale == 0 and 0 or 2, 0, 0)
vp.BackgroundColor3 = Color3.fromRGB(8, 8, 12)
vp.BackgroundTransparency = 0.1
vp.BorderSizePixel = 0
vp.Ambient = Color3.fromRGB(150, 150, 160)
vp.LightColor = Color3.fromRGB(255, 255, 255)
vp.LightDirection = Vector3.new(-0.4, -1, -0.3)
vp.ZIndex = F.silZ(120)
vp.Parent = parent
local wm = Instance.new("WorldModel"); wm.Parent = vp
local cam = Instance.new("Camera"); cam.FieldOfView = 55; cam.Parent = vp
vp.CurrentCamera = cam
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.new(1, 0, 0, 22); lbl.Position = UDim2.new(0, 0, 1, -30)
lbl.BackgroundTransparency = 1; lbl.Text = title
lbl.TextColor3 = tint; lbl.Font = Enum.Font.GothamBold; lbl.TextSize = 13
lbl.ZIndex = F.silZ(126); lbl.Parent = vp
return { vp = vp, wm = wm, cam = cam, label = lbl, rotKey = rotKey,
clone = nil, animator = nil, rise = 0, riseVel = 0 }
end
function Split.build()
local old = PlayerGui:FindFirstChild("FNFSplitScreen")
if old then old:Destroy() end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFSplitScreen"; sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false; sg.DisplayOrder = F.silZ(120)
sg.Enabled = Split.enabled
sg.Parent = PlayerGui
Split.gui = sg
Split.L = Split.makeSide(sg, 0, "YOU", Color3.fromRGB(255, 110, 230), "rotL")
Split.R = Split.makeSide(sg, 0.5, "OPPONENT", Color3.fromRGB(0, 240, 255), "rotR")
local div = Instance.new("Frame")
div.Size = UDim2.new(0, 4, 1, 0); div.Position = UDim2.new(0.5, -2, 0, 0)
div.BackgroundColor3 = Color3.fromRGB(255, 80, 180)
div.BackgroundTransparency = 1 -- fades in, never pops
div.BorderSizePixel = 0; div.ZIndex = F.silZ(125); div.Parent = sg
F.regBg(div)
Split.div = div
-- start even and let the intro ease everything in
Split.balance, Split.targetBalance = 0.5, 0.5
Split.intro = 0
Split.refreshClones()
end
function Split.loadClone(side, char)
if not side then return end
if side.clone then pcall(function() side.clone:Destroy() end) end
side.clone, side.animator = nil, nil
if not char or not char.Parent then return end
local wasArch = char.Archivable
if not wasArch then char.Archivable = true end
local ok, clone = pcall(function() return char:Clone() end)
if not wasArch then char.Archivable = wasArch end
if not ok or not clone then return end
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("Script") or d:IsA("LocalScript") then d:Destroy() end
end
clone.Name = "SplitClone"
clone.Parent = side.wm
local hum = clone:FindFirstChildOfClass("Humanoid")
if hum then
-- Keep the rig together. Without these the humanoid can "die" in the
-- viewport, break its joints, and leave the head floating while the
-- body drops away.
pcall(function() hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None end)
pcall(function() hum.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOff end)
pcall(function() hum.RequiresNeck = false end)
pcall(function() hum.BreakJointsOnDeath = false end)
pcall(function() hum.Health = hum.MaxHealth end)
-- stop the state machine so it can't fall back to its own idle
pcall(function() hum.EvaluateStateMachine = false end)
pcall(function() hum:ChangeState(Enum.HumanoidStateType.Physics) end)
side.animator = hum:FindFirstChildOfClass("Animator") or Instance.new("Animator", hum)
end
-- Anchor ONLY the root. Anchoring every part would freeze the limbs and
-- kill animation playback; leaving the root loose lets the rig drift.
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("BasePart") then
d.CanCollide = false
d.Massless = true
d.Anchored = false
elseif d:IsA("BodyMover") or d:IsA("BodyGyro") or d:IsA("BodyPosition")
or d:IsA("AlignPosition") or d:IsA("AlignOrientation") or d:IsA("VectorForce") then
d:Destroy()
end
end
local root = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
if root then
root.Anchored = true
clone.PrimaryPart = root
end
-- yaw from this pane's own rotation bar
local yaw = math.rad(Split[side.rotKey] or 0)
pcall(function() clone:PivotTo(CFrame.new(0, 3, 0) * CFrame.Angles(0, yaw, 0)) end)
side.clone = clone
side.lastAnimId = nil
-- a floor so the character reads as standing on something
if not side.floor or not side.floor.Parent then
local floor = Instance.new("Part")
floor.Name = "SplitFloor"
floor.Size = Vector3.new(14, 1, 14)
floor.Anchored = true; floor.CanCollide = false
floor.Color = Color3.fromRGB(22, 20, 30)
floor.Material = Enum.Material.SmoothPlastic
floor.Parent = side.wm
side.floor = floor
end
-- align the floor to the clone's actual feet
local okB, cf, size = pcall(function()
local a, b = clone:GetBoundingBox()
return a, b
end)
local feetY = 3 - 3
if okB and cf and size then feetY = cf.Y - size.Y / 2 end
side.floor.CFrame = CFrame.new(0, feetY - 0.5, 0)
end
-- Re-yaw the clones without rebuilding them (driven by the rotation bars).
function Split.reface()
for _, side in ipairs({ Split.L, Split.R }) do
if side and side.clone then
local yaw = math.rad(Split[side.rotKey] or 0)
pcall(function() side.clone:PivotTo(CFrame.new(0, 3, 0) * CFrame.Angles(0, yaw, 0)) end)
end
end
end
function Split.refreshClones()
if not Split.L then return end
Split.loadClone(Split.L, character)
Split.loadClone(Split.R, currentTarget and currentTarget.Character or nil)
if Split.R then
Split.R.label.Text = currentTarget and currentTarget.Name or "NO OPPONENT"
end
end
-- Mirror a live animation onto the matching clone.
function Split.play(who, animId)
if not Split.enabled or not animId or animId == "" then return end
local side = (who == "OPPONENT") and Split.R or Split.L
if not side or not side.animator then return end
if side.lastAnimId == animId and side.track and side.track.IsPlaying then return end
local ok, track = pcall(function()
local a = Instance.new("Animation")
a.AnimationId = "rbxassetid://" .. animId
local t = side.animator:LoadAnimation(a)
t.Priority = Enum.AnimationPriority.Action
t.Looped = Split.loopMirrored -- otherwise it ends and the rig goes limp
return t
end)
if ok and track then
for _, t in ipairs(side.animator:GetPlayingAnimationTracks()) do t:Stop(0) end
track:Play(0.05)
side.track = track
side.lastAnimId = animId
end
end
-- Re-fires the last animation if a track ended, so a pane never sits limp.
function Split.holdAnimations()
for _, side in ipairs({ Split.L, Split.R }) do
if side and side.animator and side.lastAnimId and Split.loopMirrored then
if not side.track or not side.track.IsPlaying then
local id = side.lastAnimId
side.lastAnimId = nil
Split.play(side == Split.R and "OPPONENT" or "PLAYER", id)
end
end
end
end
-- One pose claims screen space from the other side.
function Split.claim(who)
if not Split.enabled then return end
local side, other
if who == "PLAYER" then
Split.targetBalance = Split.targetBalance + Split.claimGain
side, other = Split.L, Split.R
else
Split.targetBalance = Split.targetBalance - Split.claimGain
side, other = Split.R, Split.L
end
Split.targetBalance = math.clamp(Split.targetBalance, 0.18, 0.82)
-- the claiming character lifts, the other settles
if side then side.riseVel = side.riseVel + 7 end
if other then other.riseVel = other.riseVel - 2.5 end
end
function Split.start()
if Split.conn then return end
Split.conn = RunService.RenderStepped:Connect(function(dt)
if not Split.enabled then return end
dt = math.clamp(dt, 0, 1/20)
Split.holdAnimations()
-- territory eases back toward even, then the divider lerps to it
Split.targetBalance = 0.5 + (Split.targetBalance - 0.5) * math.exp(-Split.recentre * dt)
local a = 1 - math.exp(-Split.slideSpeed * dt)
Split.balance = Split.balance + (Split.targetBalance - Split.balance) * a
Split.intro = Split.intro + (1 - Split.intro) * (1 - math.exp(-4 * dt))
local b = math.clamp(Split.balance, 0.05, 0.95)
if Split.L then
Split.L.vp.Size = UDim2.new(b, -2, 1, 0)
Split.L.vp.Position = UDim2.new(0, 0, 0, 0)
end
if Split.R then
Split.R.vp.Size = UDim2.new(1 - b, -2, 1, 0)
Split.R.vp.Position = UDim2.new(b, 2, 0, 0)
end
if Split.div then
Split.div.Position = UDim2.new(b, -2, 0, 0)
Split.div.BackgroundTransparency = 1 - Split.intro
end
-- cameras stay locked; only the characters breathe
for _, side in ipairs({ Split.L, Split.R }) do
if side and side.cam then
side.cam.CFrame = CFrame.new(
Vector3.new(0, Split.height + 3, Split.dist),
Vector3.new(0, 3, 0))
side.cam.FieldOfView = 55
-- sprung rise so gaining ground reads as stepping up
side.riseVel = side.riseVel + (-70 * side.rise - 11 * side.riseVel) * dt
side.rise = side.rise + side.riseVel * dt
if side.clone then
local yaw = math.rad(Split[side.rotKey] or 0)
local y = 3 + side.rise * Split.riseAmount
pcall(function()
side.clone:PivotTo(CFrame.new(0, y, 0) * CFrame.Angles(0, yaw, 0))
end)
end
end
end
end)
end
function Split.setEnabled(state)
Split.enabled = state
if state then
if not Split.gui or not Split.gui.Parent then Split.build() else Split.refreshClones() end
Split.gui.Enabled = true
Split.start()
elseif Split.gui then
Split.gui.Enabled = false
end
if Split.refreshUI then Split.refreshUI() end
F.notify(state and "Split screen ON — clones only, no map" or "Split screen OFF",
state and "success" or "info")
end
-- ============================================================
-- HEALTH BAR — FNF-style, with icons that ride the boundary.
-- Your notes push you forward into their side; their poses
-- push you back. Where your icon sits tells you how close
-- you are to losing the exchange.
-- ============================================================
function Health.build()
local old = PlayerGui:FindFirstChild("FNFHealthBar")
if old then old:Destroy() end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFHealthBar"; sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false; sg.DisplayOrder = F.silZ(140)
sg.Enabled = Health.enabled; sg.Parent = PlayerGui
Health.gui = sg
-- red vignette: four edges that crush inward as health runs out
Health.vig = {}
local edges = {
{ UDim2.new(1,0,0.32,0), UDim2.new(0,0,0,0), 90 },
{ UDim2.new(1,0,0.32,0), UDim2.new(0,0,0.68,0), 270 },
{ UDim2.new(0.26,0,1,0), UDim2.new(0,0,0,0), 0 },
{ UDim2.new(0.26,0,1,0), UDim2.new(0.74,0,0,0), 180 },
}
for i, e in ipairs(edges) do
local f = Instance.new("Frame")
f.Size, f.Position = e[1], e[2]
f.BackgroundColor3 = Color3.fromRGB(190, 0, 18)
f.BackgroundTransparency = 1
f.BorderSizePixel = 0
f.ZIndex = F.silZ(130)
f.Parent = sg
local g = Instance.new("UIGradient")
g.Rotation = e[3]
g.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 1),
})
g.Parent = f
Health.vig[i] = f
end
-- faint red wash over the whole screen at the very bottom
local wash = Instance.new("Frame")
wash.Size = UDim2.new(1, 0, 1, 0)
wash.BackgroundColor3 = Color3.fromRGB(120, 0, 10)
wash.BackgroundTransparency = 1
wash.BorderSizePixel = 0
wash.ZIndex = F.silZ(129)
wash.Parent = sg
Health.vig.wash = wash
local holder = Instance.new("Frame")
holder.Size = UDim2.new(0.55, 0, 0, 22)
holder.Position = UDim2.new(0.5, 0, 1, -132)
holder.AnchorPoint = Vector2.new(0.5, 0)
holder.BackgroundColor3 = Color3.fromRGB(10, 10, 14)
holder.BorderSizePixel = 0
holder.ZIndex = F.silZ(140); holder.Parent = sg
F.uiCorner(holder, UDim.new(0, 6))
F.uiStroke(holder, Color3.fromRGB(0, 0, 0), 3).Transparency = 0
-- opponent owns the left portion
local fillO = Instance.new("Frame")
fillO.Size = UDim2.new(0.5, 0, 1, 0); fillO.Position = UDim2.new(0, 0, 0, 0)
fillO.BackgroundColor3 = Color3.fromRGB(255, 60, 70)
fillO.BorderSizePixel = 0; fillO.ZIndex = F.silZ(141); fillO.Parent = holder
F.uiCorner(fillO, UDim.new(0, 5))
Health.fillO = fillO
-- you own the right portion
local fillP = Instance.new("Frame")
fillP.Size = UDim2.new(0.5, 0, 1, 0); fillP.Position = UDim2.new(0.5, 0, 0, 0)
fillP.BackgroundColor3 = Color3.fromRGB(60, 230, 120)
fillP.BorderSizePixel = 0; fillP.ZIndex = F.silZ(141); fillP.Parent = holder
F.uiCorner(fillP, UDim.new(0, 5))
Health.fillP = fillP
local function makeIcon(color, anchorX)
local ic = Instance.new("ImageLabel")
ic.Size = UDim2.new(0, 46, 0, 46)
ic.AnchorPoint = Vector2.new(anchorX, 0.5)
ic.Position = UDim2.new(0.5, 0, 0.5, 0)
ic.BackgroundColor3 = color
ic.BackgroundTransparency = 0
ic.Image = ""
ic.BorderSizePixel = 0
ic.ZIndex = F.silZ(143); ic.Parent = holder
F.uiCorner(ic, UDim.new(0, 8))
F.uiStroke(ic, Color3.fromRGB(0, 0, 0), 2).Transparency = 0
return ic
end
Health.iconO = makeIcon(Color3.fromRGB(200, 50, 60), 1)
Health.iconP = makeIcon(Color3.fromRGB(50, 190, 100), 0)
local warn = Instance.new("TextLabel")
warn.Size = UDim2.new(1, 0, 0, 18); warn.Position = UDim2.new(0, 0, 0, -22)
warn.BackgroundTransparency = 1; warn.Text = ""
warn.TextColor3 = Color3.fromRGB(255, 70, 70)
warn.Font = Enum.Font.GothamBlack; warn.TextSize = 15
warn.ZIndex = F.silZ(144); warn.Parent = holder
Health.warn = warn
end
function Health.setIcons(playerId, oppId)
Health.playerIcon, Health.oppIcon = playerId or "", oppId or ""
if Health.iconP then
Health.iconP.Image = Health.playerIcon ~= "" and ("rbxassetid://" .. Health.playerIcon) or ""
Health.iconP.BackgroundTransparency = Health.playerIcon ~= "" and 1 or 0
end
if Health.iconO then
Health.iconO.Image = Health.oppIcon ~= "" and ("rbxassetid://" .. Health.oppIcon) or ""
Health.iconO.BackgroundTransparency = Health.oppIcon ~= "" and 1 or 0
end
end
function Health.onHit(who)
if not Health.enabled then return end
if who == "PLAYER" then
Health.hp = math.min(1, Health.hp + Health.gainPerNote)
else
Health.hp = math.max(0, Health.hp - Health.drainPerPose)
end
end
function Health.reset()
Health.hp, Health.display = 0.5, 0.5
Health.lastWritten = nil
end
function Health.start()
if Health.conn then return end
Health.conn = RunService.RenderStepped:Connect(function(dt)
if not Health.enabled or not Health.gui then return end
dt = math.clamp(dt, 0, 1/20)
-- passive bleed only while an opponent is actually set
if currentTarget then
Health.hp = math.clamp(Health.hp - Health.passiveDrain * dt, 0, 1)
end
Health.display = Health.display + (Health.hp - Health.display) * (1 - math.exp(-Health.smooth * dt))
Health.syncStep()
Health.drawVignette()
local d = math.clamp(Health.display, 0.02, 0.98)
-- boundary sits at (1 - hp): more health pushes it left, into their side
local boundary = 1 - d
if Health.fillO then Health.fillO.Size = UDim2.new(boundary, 0, 1, 0) end
if Health.fillP then
Health.fillP.Position = UDim2.new(boundary, 0, 0, 0)
Health.fillP.Size = UDim2.new(d, 0, 1, 0)
end
-- your icon rides forward, theirs gets pushed back
if Health.iconO then Health.iconO.Position = UDim2.new(boundary, -4, 0.5, 0) end
if Health.iconP then Health.iconP.Position = UDim2.new(boundary, 4, 0.5, 0) end
if Health.warn then
if d < 0.22 then
Health.warn.Text = "LOSING GROUND"
Health.warn.TextTransparency = 0.15 + math.abs(math.sin(os.clock() * 9)) * 0.55
else
Health.warn.Text = ""
end
end
end)
end
-- Red edges that grow and throb as health runs out. Nothing above the
-- threshold; full strength, pulsing fastest, when the bar is empty.
function Health.drawVignette()
local v = Health.vig
if not v or not v[1] then return end
local level = 0
if Health.lowVignette and Health.vignetteAt > 0 then
level = math.clamp((Health.vignetteAt - Health.display) / Health.vignetteAt, 0, 1)
end
-- heartbeat pulse that speeds up the closer you are to empty
local rate = 4 + level * 6
local pulse = 0.72 + 0.28 * math.abs(math.sin(os.clock() * rate))
local strength = level * pulse
for i = 1, 4 do
v[i].BackgroundTransparency = 1 - strength * 0.92
end
if v.wash then
v.wash.BackgroundTransparency = 1 - math.max(0, level - 0.6) * 0.45 * pulse
end
end
-- Push the rap health onto your real Roblox health bar.
-- Client-side, so the drop shows on YOUR screen only; the server still
-- has your real health. Rap drain never kills: 1 HP is the floor.
function Health.syncStep()
if not Health.syncRoblox then return end
local hum = character and character:FindFirstChildOfClass("Humanoid")
if not hum or hum.Health <= 0 then return end
-- something other than us lowered your health (a fall, a weapon, the
-- game itself) — count it against the rap health too, so the two agree
if Health.respectDamage and Health.lastWritten and hum.Health < Health.lastWritten - 0.5 then
local lost = (Health.lastWritten - hum.Health) / math.max(hum.MaxHealth, 1)
Health.hp = math.clamp(Health.hp - lost, 0, 1)
Health.display = math.clamp(Health.display - lost, 0, 1)
end
local target = math.max(1, Health.display * hum.MaxHealth)
hum.Health = target
Health.lastWritten = target
end
-- Hand your real health back untouched.
function Health.releaseRoblox()
local hum = character and character:FindFirstChildOfClass("Humanoid")
if hum and hum.Health > 0 then hum.Health = hum.MaxHealth end
Health.lastWritten = nil
end
function Health.setEnabled(state)
Health.enabled = state
if state then
if not Health.gui or not Health.gui.Parent then Health.build() end
Health.gui.Enabled = true
Health.start()
elseif Health.gui then
Health.gui.Enabled = false
end
-- bar off means the battle's off: give the real health back
if not state and Health.syncRoblox then Health.releaseRoblox() end
if Health.refreshUI then Health.refreshUI() end
end
-- ============================================================
-- PERFECT DOUBLE NOTE — afterimage streak
-- Two different notes inside the perfect window spawn a ghost
-- copy of you that flies off in the direction of the note.
-- Note: input events are frame-bound, so the tightest window
-- that can actually fire is roughly one frame (~16ms at 60fps).
-- ============================================================
function Ghost.spawn(dir)
local char = character
if not char then return end
local root = char:FindFirstChild("HumanoidRootPart")
if not root then return end
local wasArch = char.Archivable
if not wasArch then char.Archivable = true end
local ok, clone = pcall(function() return char:Clone() end)
if not wasArch then char.Archivable = wasArch end
if not ok or not clone then return end
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("Script") or d:IsA("LocalScript") then d:Destroy() end
end
local hum = clone:FindFirstChildOfClass("Humanoid")
if hum then
hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
hum.EvaluateStateMachine = false
end
local parts = {}
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("BasePart") then
d.Anchored = true
d.CanCollide = false -- nothing bumps into it
d.CanTouch = false -- no Touched events fire off it
d.CanQuery = false -- invisible to raycasts and region checks
d.Massless = true
d.Transparency = math.max(d.Transparency, 0.45)
table.insert(parts, d)
elseif d:IsA("Decal") or d:IsA("Texture") then
d.Transparency = 0.45
elseif d:IsA("TouchTransmitter") or d:IsA("ClickDetector")
or d:IsA("ProximityPrompt") or d:IsA("Fire") or d:IsA("Smoke") then
d:Destroy()
end
end
-- strip the humanoid so it can't walk, collide or be targeted
local ghostHum = clone:FindFirstChildOfClass("Humanoid")
if ghostHum then
pcall(function() ghostHum.EvaluateStateMachine = false end)
pcall(function() ghostHum.PlatformStand = true end)
pcall(function() ghostHum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None end)
end
local hl = Instance.new("Highlight")
hl.FillColor = Ghost.tint; hl.OutlineColor = Ghost.tint
hl.FillTransparency = 0.55; hl.OutlineTransparency = 0.2
hl.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop
hl.Parent = clone
clone.Name = "FNFAfterimage"
clone.Parent = Workspace
-- travel direction, relative to how the character is facing
local cf = root.CFrame
local offset
-- horizontal notes read as depth: LEFT throws the ghost forward,
-- RIGHT throws it backward. Vertical notes stay vertical.
if dir == "LEFT" then offset = cf.LookVector
elseif dir == "RIGHT" then offset = -cf.LookVector
elseif dir == "UP" then offset = Vector3.new(0, 1, 0)
elseif dir == "DOWN" then offset = Vector3.new(0, -1, 0)
else offset = -cf.LookVector end
offset = offset * Ghost.distance
local startCF = cf
local life = Ghost.life
for _, p in ipairs(parts) do
TweenService:Create(p, TweenInfo.new(life, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{ Transparency = 1 }):Play()
end
TweenService:Create(hl, TweenInfo.new(life, Enum.EasingStyle.Quad),
{ FillTransparency = 1, OutlineTransparency = 1 }):Play()
task.spawn(function()
local t0 = os.clock()
while os.clock() - t0 < life do
local a = (os.clock() - t0) / life
local eased = 1 - (1 - a) * (1 - a)
pcall(function() clone:PivotTo(startCF + offset * eased) end)
RunService.Heartbeat:Wait()
end
pcall(function() clone:Destroy() end)
end)
end
-- Called on every note press. Returns true if it was a perfect double.
function Ghost.tryPerfect(dir)
if not Ghost.enabled then
Ghost.lastPress, Ghost.lastDir = os.clock(), dir
return false
end
local now = os.clock()
local perfect = (now - Ghost.lastPress) <= Ghost.window
and Ghost.lastDir ~= nil and Ghost.lastDir ~= dir
and (now - Ghost.lastSpawn) > 0.15
Ghost.lastPress, Ghost.lastDir = now, dir
if not perfect then return false end
Ghost.lastSpawn = now
Ghost.spawn(dir)
F.notify("PERFECT DOUBLE", "success")
FNFCamera:DoubleNoteZoom(1.6) -- perfect doubles hit hardest
if Chaos.enabled then Chaos.bigImpact(1.5) end
return true
end
-- ============================================================
-- BOSS BATTLE CUTSCENE
-- Scales the opponent up and you down, then frames it from the
-- floor. Note: Model:ScaleTo is applied on your client only, so
-- the opponent sees themselves at normal size — this is a local
-- visual effect, not a replicated one.
-- ============================================================
function Boss.scaleModel(model, scale)
if not model then return nil end
local ok = pcall(function() model:ScaleTo(scale) end)
return ok and model or nil
end
function Boss.enter()
if Boss.active then return end
local opp = currentTarget and currentTarget.Character or nil
if not opp then
F.notify("Set an opponent first — boss view needs a target", "warn")
return
end
Boss.active = true
-- remember current scale so we can put everything back
pcall(function() Boss.savedOpp = opp:GetScale() end)
pcall(function() Boss.savedPlayer = character and character:GetScale() or 1 end)
Boss.scaledOpp = Boss.scaleModel(opp, Boss.savedOpp * Boss.oppScale)
Boss.scaledPlayer = Boss.scaleModel(character, Boss.savedPlayer * Boss.playerScale)
if not Boss.scaledOpp then
F.notify("This rig can't be scaled — framing only", "warn")
end
FNFCamera:SetEnabled(true)
FNFCamera.currentCFrame = nil -- cut straight into the shot
F.notify("BOSS BATTLE", "success")
if Chaos.enabled then Chaos.bigImpact(1.6) end
end
function Boss.exit()
if not Boss.active then return end
Boss.active = false
if Boss.scaledOpp then pcall(function() Boss.scaledOpp:ScaleTo(Boss.savedOpp) end) end
if Boss.scaledPlayer then pcall(function() Boss.scaledPlayer:ScaleTo(Boss.savedPlayer) end) end
Boss.scaledOpp, Boss.scaledPlayer = nil, nil
FNFCamera.currentCFrame = nil
F.notify("Boss view ended", "info")
end
function Boss.toggle()
if Boss.active then Boss.exit() else Boss.enter() end
if Boss.refreshUI then Boss.refreshUI() end
end
-- Rescale live if the sliders move while the cutscene is running.
function Boss.restyle()
if not Boss.active then return end
if Boss.scaledOpp then pcall(function() Boss.scaledOpp:ScaleTo(Boss.savedOpp * Boss.oppScale) end) end
if Boss.scaledPlayer then pcall(function() Boss.scaledPlayer:ScaleTo(Boss.savedPlayer * Boss.playerScale) end) end
end
-- ============================================================
-- KEYBIND CHEAT SHEET — translucent overlay you can read
-- while playing, without opening the panel.
-- ============================================================
function Keys.rows()
local out = {}
local function add(k, d)
table.insert(out, { key = (k == nil or k == "" or k == "None") and "--none--" or k, desc = d })
end
add("--", "NOTES")
for _, row in ipairs(CurrentBindings) do
add(row.key, row.dir)
end
add("--", "SECTIONS")
add(sectionPrevKey, "Previous section")
add(sectionNextKey, "Next section")
add("--", "MODES")
add(soloKey, "Solo mode")
add(silhouetteKey, "Silhouette mode")
add(Split.key, "Split screen")
add(Health.key, "Health bar")
add(Boss.key, "Boss battle view")
add(Ride.key, "Ride lock")
add(Keys.overlayKey, "This list")
add(Keys.beatKey, "Beat zoom")
add(Pose.dodgeKey, "Dodge animation")
add(Notes.key, "Hold streaks")
add(Vid.boKey, "Black screen fade")
add(Result.winKey, "Win")
add(Result.loseKey, "Lose")
add("--", "INTRO TRANSITIONS")
for _, e in ipairs(Keys.intro) do add(e.key, e.label) end
add("--", "CINEMATIC PRESETS")
for _, e in ipairs(Keys.preset) do add(e.key, e.name) end
return out
end
function Keys.build()
local old = PlayerGui:FindFirstChild("FNFKeybindList")
if old then old:Destroy() end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFKeybindList"; sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false; sg.DisplayOrder = F.silZ(135)
sg.Enabled = Keys.overlayOn; sg.Parent = PlayerGui
Keys.gui = sg
local panel = Instance.new("Frame")
panel.Size = UDim2.new(0, 208, 0, 40)
panel.Position = UDim2.new(0, 14, 0.5, 0)
panel.AnchorPoint = Vector2.new(0, 0.5)
panel.BackgroundColor3 = Color3.fromRGB(10, 9, 15)
panel.BackgroundTransparency = Keys.opacity
panel.BorderSizePixel = 0
panel.AutomaticSize = Enum.AutomaticSize.Y
panel.ZIndex = F.silZ(135); panel.Parent = sg
F.uiCorner(panel, UDim.new(0, 10))
local edge = F.uiStroke(panel, Color3.fromRGB(255, 255, 255), 1)
edge.Transparency = math.clamp(Keys.opacity + 0.25, 0, 1)
Keys.panel = panel
local pad = Instance.new("UIPadding")
pad.PaddingTop = UDim.new(0, 8); pad.PaddingBottom = UDim.new(0, 8)
pad.PaddingLeft = UDim.new(0, 10); pad.PaddingRight = UDim.new(0, 10)
pad.Parent = panel
local layout = Instance.new("UIListLayout")
layout.Padding = UDim.new(0, 2)
layout.SortOrder = Enum.SortOrder.LayoutOrder
layout.Parent = panel
Keys.list = panel
Keys.refresh()
end
function Keys.refresh()
local panel = Keys.list
if not panel then return end
for _, c in ipairs(panel:GetChildren()) do
if c:IsA("Frame") or c:IsA("TextLabel") then c:Destroy() end
end
local order = 0
for _, r in ipairs(Keys.rows()) do
order = order + 1
if r.key == "--" then
local h = Instance.new("TextLabel")
h.Size = UDim2.new(1, 0, 0, 16)
h.BackgroundTransparency = 1
h.Text = r.desc
h.TextColor3 = THEMES[currentThemeName].accent
h.Font = Enum.Font.GothamBold; h.TextSize = 9
h.TextXAlignment = Enum.TextXAlignment.Left
h.LayoutOrder = order
h.ZIndex = F.silZ(137); h.Parent = panel
F.regText(h)
else
local row = Instance.new("Frame")
row.Size = UDim2.new(1, 0, 0, 15)
row.BackgroundTransparency = 1
row.LayoutOrder = order
row.ZIndex = F.silZ(136); row.Parent = panel
local chip = Instance.new("TextLabel")
chip.Size = UDim2.new(0, 62, 1, 0)
chip.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
chip.BackgroundTransparency = math.clamp(Keys.opacity + 0.25, 0, 1)
chip.Text = r.key
chip.TextColor3 = Color3.fromRGB(245, 245, 255)
chip.Font = Enum.Font.GothamBold; chip.TextSize = 9
chip.BorderSizePixel = 0
chip.ZIndex = F.silZ(137); chip.Parent = row
F.uiCorner(chip, UDim.new(0, 4))
local desc = Instance.new("TextLabel")
desc.Size = UDim2.new(1, -68, 1, 0); desc.Position = UDim2.new(0, 68, 0, 0)
desc.BackgroundTransparency = 1
desc.Text = r.desc
desc.TextColor3 = Color3.fromRGB(215, 215, 230)
desc.Font = Enum.Font.Gotham; desc.TextSize = 9
desc.TextXAlignment = Enum.TextXAlignment.Left
desc.ZIndex = F.silZ(137); desc.Parent = row
end
end
end
function Keys.applyOpacity()
if Keys.panel then
Keys.panel.BackgroundTransparency = Keys.opacity
for _, s in ipairs(Keys.panel:GetChildren()) do
if s:IsA("UIStroke") then s.Transparency = math.clamp(Keys.opacity + 0.25, 0, 1) end
end
end
Keys.refresh()
end
function Keys.setOverlay(state)
Keys.overlayOn = state
if state then
if not Keys.gui or not Keys.gui.Parent then Keys.build() else Keys.refresh() end
Keys.gui.Enabled = true
elseif Keys.gui then
Keys.gui.Enabled = false
end
if Keys.refreshUI then Keys.refreshUI() end
end
-- Handles the intro / preset / overlay / beat hotkeys.
function Keys.handle(kn)
if not kn or kn == "None" then return false end
if kn == Keys.overlayKey then
Keys.setOverlay(not Keys.overlayOn)
return true
end
if kn == Keys.beatKey then
FNFCamera:SetBeatZoom(not FNFCamera.beatZoom)
if Keys.onBeatZoom then Keys.onBeatZoom() end
F.notify(FNFCamera.beatZoom
and ("Beat zoom on — " .. math.floor(StatusState.bpm or 100) .. " BPM")
or "Beat zoom off", FNFCamera.beatZoom and "success" or "info")
return true
end
for _, e in ipairs(Keys.intro) do
if e.key ~= "None" and kn == e.key then
F.playIntroTransition(e.name)
F.notify("Intro: " .. e.label, "info")
return true
end
end
for _, e in ipairs(Keys.preset) do
if e.key ~= "None" and kn == e.key then
FNFCamera:ApplyPreset(e.name)
if Keys.onPreset then Keys.onPreset(e.name) end
F.notify("Camera preset: " .. e.name, "success")
return true
end
end
return false
end
-- ============================================================
-- SAVE SYSTEM v2
--
-- What changed from v1, and why v1 never worked:
-- * v1's startup call was accidentally nested inside another function,
-- so it never ran. Boot is now an explicit step in the launch sequence.
-- * v1 required writefile, readfile AND isfile. Executors differ, so each
-- function is found separately and only write + read are mandatory.
-- * v1 applied everything in one block — one bad field aborted the rest,
-- and at launch that could crash startup. Every section is isolated now.
-- * v1 loaded after the GUI was built, so the panel showed stale values.
-- Config now loads BEFORE the GUI, so it's built from your settings.
-- * Three slots, plus clipboard export/import as a fallback for
-- executors with no file access at all.
-- ============================================================
local HttpService = game:GetService("HttpService")
-- Find an executor function by trying the direct global, then getgenv().
local function findFn(name, direct)
if typeof(direct) == "function" then return direct end
local f
pcall(function() f = getgenv()[name] end)
if typeof(f) == "function" then return f end
return nil
end
function Save.resolveApi()
Save.api = {
write = findFn("writefile", writefile),
read = findFn("readfile", readfile),
isfile = findFn("isfile", isfile),
makefolder = findFn("makefolder", makefolder),
isfolder = findFn("isfolder", isfolder),
delfile = findFn("delfile", delfile),
clip = findFn("setclipboard", setclipboard) or findFn("toclipboard", toclipboard),
}
Save.available = (Save.api.write ~= nil and Save.api.read ~= nil)
-- prefer a folder when we can make one; fall back to a flat file name
Save.useFolder = false
if Save.available and Save.api.makefolder then
local ok = pcall(function()
local exists = Save.api.isfolder and Save.api.isfolder(Save.folder)
if not exists then Save.api.makefolder(Save.folder) end
end)
Save.useFolder = ok
end
return Save.available
end
function Save.path(slot)
slot = slot or Save.slot
if Save.useFolder then return Save.folder .. "/slot" .. slot .. ".json" end
return Save.folder .. "_slot" .. slot .. ".json"
end
function Save.exists(slot)
if not Save.available then return false end
local p = Save.path(slot)
if Save.api.isfile then
local ok, r = pcall(Save.api.isfile, p)
if ok then return r == true end
end
-- no isfile: a read that succeeds means it's there
local ok, r = pcall(Save.api.read, p)
return ok and type(r) == "string" and #r > 0
end
-- ---- JSON-safe conversion ---------------------------------------------
-- JSONEncode chokes on NaN/inf, Roblox datatypes and mixed tables, so we
-- normalise everything first.
function Save.encodeValue(v, depth)
depth = depth or 0
if depth > 14 then return nil end
local t = typeof(v)
if t == "number" then
if v ~= v or v == math.huge or v == -math.huge then return nil end
return v
elseif t == "string" or t == "boolean" then
return v
elseif t == "Vector3" then
return { __t = "V3", x = v.X, y = v.Y, z = v.Z }
elseif t == "Color3" then
return { __t = "C3", r = v.R, g = v.G, b = v.B }
elseif t == "table" then
local out = {}
for k, x in pairs(v) do
local kt = type(k)
if kt == "string" or kt == "number" then
local e = Save.encodeValue(x, depth + 1)
if e ~= nil then out[k] = e end
end
end
return out
end
return nil -- functions, Instances, anything else is skipped
end
function Save.decodeValue(v)
if type(v) ~= "table" then return v end
if v.__t == "V3" then return Vector3.new(v.x or 0, v.y or 0, v.z or 0) end
if v.__t == "C3" then return Color3.new(v.r or 0, v.g or 0, v.b or 0) end
local out = {}
for k, x in pairs(v) do out[k] = Save.decodeValue(x) end
return out
end
function Save.serialize()
local ok, res = pcall(function()
local data = Save.collect()
data.version = INJECTOR_VERSION
data.savedAt = os.time()
return HttpService:JSONEncode(Save.encodeValue(data))
end)
if ok then return res end
Save.lastError = "encode: " .. tostring(res)
return nil
end
function Save.deserialize(json)
if type(json) ~= "string" or #json == 0 then return nil end
local ok, res = pcall(function() return HttpService:JSONDecode(json) end)
if not ok or type(res) ~= "table" then
Save.lastError = "decode: " .. tostring(res)
return nil
end
return Save.decodeValue(res)
end
function Save.collect()
local binds = {}
for _, r in ipairs(CurrentBindings) do
local alts = {}
for _, a in ipairs(r.alts or {}) do
table.insert(alts, { id = a.id or "", chance = a.chance or 0 })
end
table.insert(binds, {
dir = r.dir, key = r.key, id = r.id,
speedMult = r.speedMult, holdLoop = r.holdLoop, alts = alts,
mode = r.mode or "CYCLE",
})
end
local secs = {}
for _, s in ipairs(sections) do
table.insert(secs, { name = s.name, ids = {
LEFT = s.ids.LEFT, DOWN = s.ids.DOWN, UP = s.ids.UP, RIGHT = s.ids.RIGHT } })
end
local favs = {}
for _, f in ipairs(favorites) do table.insert(favs, { name = f.name, id = f.id }) end
local list = {}
for _, p in ipairs(playlist) do table.insert(list, { name = p.name, id = p.id, delay = p.delay }) end
local introKeys, presetKeys = {}, {}
for _, e in ipairs(Keys.intro) do table.insert(introKeys, { name = e.name, key = e.key }) end
for _, e in ipairs(Keys.preset) do table.insert(presetKeys, { name = e.name, key = e.key }) end
return {
version = INJECTOR_VERSION,
defaultsVersion = 3,
tourSeen = Intro.seen,
binds = binds,
sections = secs,
currentSection = currentSectionIdx,
favorites = favs,
playlist = list,
faceIds = { LEFT = faceIds.LEFT, DOWN = faceIds.DOWN, UP = faceIds.UP, RIGHT = faceIds.RIGHT },
theme = currentThemeName,
idleBPM = idleBPM, baseBPM = BASE_BPM,
legsLocked = legsLocked,
vinyl = { id = VINYL_DECAL_ID, rpm = vinylBaseRPM },
cutterResponse = { reach = Cut.reach, snap = Cut.snap, reverse = Cut.reverse,
returnPause = Cut.returnPause, returnTime = Cut.returnTime,
holdAfterReturn = Cut.holdAfterReturn },
cutter = { id = cutterAnimId, slices = {
LEFT = cutterSlices.LEFT, DOWN = cutterSlices.DOWN,
UP = cutterSlices.UP, RIGHT = cutterSlices.RIGHT } },
keys = {
solo = soloKey, silhouette = silhouetteKey,
sectionPrev = sectionPrevKey, sectionNext = sectionNextKey,
split = Split.key, health = Health.key, boss = Boss.key,
overlay = Keys.overlayKey, beat = Keys.beatKey, opacity = Keys.opacity,
intro = introKeys, preset = presetKeys,
},
camera = {
preset = FNFCamera.preset,
distance = FNFCamera.camDistance, height = FNFCamera.camHeight,
horizontal = FNFCamera.camHorizontal, angle = FNFCamera.camAngle,
flipped = FNFCamera.camFlipped, fov = CAM_CFG.BaseFOV,
bars = CAM_CFG.BarsEnabled,
settleEnabled = FNFCamera.settleEnabled, settleAfter = FNFCamera.settleAfter,
orbitSpeed = FNFCamera.orbitSpeed, orbitAmp = FNFCamera.orbitAmp,
dutch = FNFCamera.dutch, handheld = FNFCamera.handheld, lead = FNFCamera.lead,
beatZoom = FNFCamera.beatZoom, beatPower = FNFCamera.beatPower,
snapHits = FNFCamera.snapHits, nudgeScale = FNFCamera.nudgeScale,
tiltScale = FNFCamera.tiltScale, punchScale = FNFCamera.punchScale,
returnSpeed = FNFCamera.returnSpeed,
beatAccent = FNFCamera.beatAccent,
},
chaos = {
master = Chaos.master, shake = Chaos.shake, zoomPunch = Chaos.zoomPunch,
warp = Chaos.warp, glitch = Chaos.glitch, flash = Chaos.flash,
readability = Chaos.readability, beatSync = Chaos.beatSync,
},
sil = {
strength = SilFX.strength, fadeSpeed = SilFX.fadeSpeed,
includePlayer = SilFX.includePlayer, includeOpponent = SilFX.includeOpponent,
alwaysOnTop = SilFX.alwaysOnTop,
},
split = {
dist = Split.dist, height = Split.height,
rotL = Split.rotL, rotR = Split.rotR,
claimGain = Split.claimGain, slideSpeed = Split.slideSpeed,
loopMirrored = Split.loopMirrored,
},
health = {
syncRoblox = Health.syncRoblox, respectDamage = Health.respectDamage,
lowVignette = Health.lowVignette, vignetteAt = Health.vignetteAt,
gainPerNote = Health.gainPerNote, drainPerPose = Health.drainPerPose,
passiveDrain = Health.passiveDrain, smooth = Health.smooth,
playerIcon = Health.playerIcon, oppIcon = Health.oppIcon,
},
arrows = {
x = Arrows.x, y = Arrows.y, offX = Arrows.offX, offY = Arrows.offY,
size = Arrows.size, gap = Arrows.gap,
draggable = Arrows.draggable, noteEnabled = Arrows.noteEnabled,
noteDir = Arrows.noteDir, noteSpeed = Arrows.noteSpeed, noteLife = Arrows.noteLife,
trail = Arrows.trail, beatBob = Arrows.beatBob,
splash = Arrows.splash, combo = Arrows.combo, tilt = Arrows.tilt,
},
ride = { key = Ride.key, pingComp = Ride.pingComp, pingScale = Ride.pingScale,
pingSource = Ride.pingSource, extraMs = Ride.extraMs,
walkWith = Ride.walkWith, jumpWith = Ride.jumpWith,
clampEdges = Ride.clampEdges, edgeMargin = Ride.edgeMargin,
gravityScale = Ride.gravityScale, pickRadius = Ride.pickRadius,
shake = Ride.shake, wind = Ride.wind, speedFov = Ride.speedFov,
gLean = Ride.gLean, maxSpeedAt = Ride.maxSpeedAt, minSpeed = Ride.minSpeed,
shakeScale = Ride.shakeScale, windScale = Ride.windScale,
leanScale = Ride.leanScale },
mobile = { mode = Mobile.mode, size = Mobile.size, spread = Mobile.spread,
pairGap = Mobile.pairGap, bottom = Mobile.bottom, idleFade = Mobile.idleFade,
hideStrip = Mobile.hideStrip },
result = { winKey = Result.winKey, loseKey = Result.loseKey, cheerId = Result.cheerId,
cheerLoop = Result.cheerLoop, cheerTime = Result.cheerTime, sitId = Result.sitId,
flingSpeed = Result.flingSpeed, lift = Result.lift, spin = Result.spin,
direction = Result.direction, getUpAfter = Result.getUpAfter, banner = Result.banner },
video = { volume = Vid.volume, opacity = Vid.opacity, looped = Vid.looped,
display = Vid.display, scale = Vid.scale, lastFile = Vid.lastFile,
boKey = Vid.boKey, boFadeIn = Vid.boFadeIn, boHold = Vid.boHold,
boFadeOut = Vid.boFadeOut },
notes = { enabled = Notes.enabled, key = Notes.key, grow = Notes.grow,
fullTime = Notes.fullTime, startDelay = Notes.startDelay, length = Notes.length,
holdHealPerSec = Notes.holdHealPerSec, best = Notes.best },
pose = { reach = Pose.reach, snap = Pose.snap,
dodgeKey = Pose.dodgeKey, dodgeId = Pose.dodgeId, dodgeSpeed = Pose.dodgeSpeed },
hold = { enabled = Hold.enabled, duration = Hold.duration, shortAt = Hold.shortAt,
holdAll = Hold.holdAll, cutter = Hold.cutter, fadeOut = Hold.fadeOut,
untilNext = Hold.untilNext },
spacing = {
enabled = SPACING.enabled, stepIn = SPACING.stepIn, bothBonus = SPACING.bothBonus,
stepTime = SPACING.stepTime, returnDelay = SPACING.returnDelay,
returnTime = SPACING.returnTime, minGap = SPACING.minGap,
stepEase = SPACING.stepEase, returnEase = SPACING.returnEase,
},
ghost = { window = Ghost.window, life = Ghost.life, distance = Ghost.distance,
zoom = FNFCamera.dnPower },
boss = {
oppScale = Boss.oppScale, playerScale = Boss.playerScale,
dist = Boss.dist, height = Boss.height,
lookHeight = Boss.lookHeight, bars = Boss.bars,
},
fight = {
pushPerHit = fightState.pushPerHit, pushPerDouble = fightState.pushPerDouble,
momentumCap = fightState.momentumCap,
proximityDist = proximityBlockDist, proximityOn = proximityBlockEnabled,
rpMode = rpModeEnabled,
},
}
end
local function pick(v, fallback) if v == nil then return fallback end return v end
-- Every section runs on its own, so one bad or outdated field can't stop
-- the rest of your settings loading. Returns (applied, failedSections).
function Save.apply(d, silentUi)
if type(d) ~= "table" then return false, {} end
local failed = {}
local function sec(name, fn)
local ok, err = pcall(fn)
if not ok then
table.insert(failed, name)
F.log("save/" .. name .. " failed: " .. tostring(err))
end
end
local function num(v, fallback) return type(v) == "number" and v or fallback end
local function str(v, fallback) return (type(v) == "string" and v ~= "") and v or fallback end
sec("binds", function()
if type(d.binds) ~= "table" then return end
for i, b in ipairs(d.binds) do
local r = CurrentBindings[i]
if r and type(b) == "table" then
r.key = str(b.key, r.key)
r.id = str(b.id, r.id)
r.speedMult = num(b.speedMult, r.speedMult)
r.holdLoop = pick(b.holdLoop, r.holdLoop)
r.alts = {}
for _, a in ipairs(b.alts or {}) do
if type(a) == "table" then
table.insert(r.alts, { id = tostring(a.id or ""), chance = num(a.chance, 0) })
end
end
if b.mode == "CYCLE" or b.mode == "RANDOM" or b.mode == "WEIGHTED" then
r.mode = b.mode
end
r.cycleIdx = 0
if r.keyBtn then r.keyBtn.Text = r.key end
end
end
end)
sec("sections", function()
if type(d.sections) ~= "table" or #d.sections == 0 then return end
local new = {}
for _, s in ipairs(d.sections) do
if type(s) == "table" and type(s.ids) == "table" then
table.insert(new, { name = str(s.name, "Section"), ids = {
LEFT = str(s.ids.LEFT, "0"), DOWN = str(s.ids.DOWN, "0"),
UP = str(s.ids.UP, "0"), RIGHT = str(s.ids.RIGHT, "0") } })
end
end
if #new > 0 then
sections = new
currentSectionIdx = math.clamp(num(d.currentSection, 1), 1, #sections)
end
end)
sec("favorites", function()
if type(d.favorites) ~= "table" then return end
favorites = {}
for _, f in ipairs(d.favorites) do
if type(f) == "table" and f.id then
table.insert(favorites, { name = str(f.name, "Fav"), id = tostring(f.id) })
end
end
end)
sec("playlist", function()
if type(d.playlist) ~= "table" then return end
playlist = {}
for _, p in ipairs(d.playlist) do
if type(p) == "table" and p.id then
table.insert(playlist, { name = str(p.name, "Track"), id = tostring(p.id), delay = num(p.delay, 1) })
end
end
end)
sec("faces", function()
if type(d.faceIds) ~= "table" then return end
for _, k in ipairs({ "LEFT", "DOWN", "UP", "RIGHT" }) do
if type(d.faceIds[k]) == "string" then faceIds[k] = d.faceIds[k] end
end
end)
sec("theme", function()
if type(d.theme) == "string" and THEMES[d.theme] then
currentThemeName = d.theme
rgbCycleOn = (d.theme == "RGB")
end
end)
sec("bpm", function()
idleBPM = num(d.idleBPM, idleBPM)
BASE_BPM = num(d.baseBPM, BASE_BPM)
StatusState.bpm = idleBPM
end)
sec("vinyl", function()
if type(d.vinyl) ~= "table" then return end
VINYL_DECAL_ID = type(d.vinyl.id) == "string" and d.vinyl.id or VINYL_DECAL_ID
vinylBaseRPM = num(d.vinyl.rpm, vinylBaseRPM)
end)
sec("cutter", function()
if type(d.cutterResponse) == "table" then
Cut.reach = num(d.cutterResponse.reach, Cut.reach)
if type(d.cutterResponse.snap) == "boolean" then Cut.snap = d.cutterResponse.snap end
local rv = d.cutterResponse.reverse
if rv == "OFF" or rv == "AUTO" or rv == "RELEASE" then Cut.reverse = rv end
Cut.returnPause = num(d.cutterResponse.returnPause, Cut.returnPause)
Cut.returnTime = num(d.cutterResponse.returnTime, Cut.returnTime)
if type(d.cutterResponse.holdAfterReturn) == "boolean" then
Cut.holdAfterReturn = d.cutterResponse.holdAfterReturn
end
end
if type(d.cutter) == "table" then
-- an empty saved id keeps the default instead of wiping it
cutterAnimId = str(d.cutter.id, cutterAnimId)
for _, k in ipairs({ "LEFT", "DOWN", "UP", "RIGHT" }) do
local s = d.cutter.slices and d.cutter.slices[k]
if type(s) == "table" then
cutterSlices[k] = {
start = num(s.start, 0), stop = num(s.stop, 0),
id = type(s.id) == "string" and s.id or "",
}
end
end
end
end)
sec("keys", function()
local k = d.keys
if type(k) ~= "table" then return end
soloKey = str(k.solo, soloKey)
silhouetteKey = str(k.silhouette, silhouetteKey)
sectionPrevKey = str(k.sectionPrev, sectionPrevKey)
sectionNextKey = str(k.sectionNext, sectionNextKey)
Split.key = str(k.split, Split.key)
Health.key = str(k.health, Health.key)
Boss.key = str(k.boss, Boss.key)
Keys.overlayKey = str(k.overlay, Keys.overlayKey)
Keys.beatKey = str(k.beat, Keys.beatKey)
Keys.opacity = num(k.opacity, Keys.opacity)
for _, e in ipairs(k.intro or {}) do
for _, t in ipairs(Keys.intro) do
if type(e) == "table" and t.name == e.name then t.key = str(e.key, t.key) end
end
end
for _, e in ipairs(k.preset or {}) do
for _, t in ipairs(Keys.preset) do
if type(e) == "table" and t.name == e.name then t.key = str(e.key, t.key) end
end
end
end)
-- plain config tables: copy only keys that already exist, and only when
-- the type matches, so an old save can never inject junk into a module
local function copyInto(tbl, data, skip)
if type(data) ~= "table" then return end
for key, val in pairs(data) do
if not (skip and skip[key]) and tbl[key] ~= nil and type(tbl[key]) == type(val) then
tbl[key] = val
end
end
end
sec("ride", function() copyInto(Ride, d.ride, { enabled = true }) end)
sec("hold", function() copyInto(Hold, d.hold) end)
sec("pose", function() copyInto(Pose, d.pose, { cache = true, owner = true }) end)
sec("notes", function() copyInto(Notes, d.notes, { held = true }) end)
sec("video", function() copyInto(Vid, d.video) end)
sec("result", function() copyInto(Result, d.result) end)
sec("mobile", function() copyInto(Mobile, d.mobile, { active = true }) end)
sec("spacing", function() copyInto(SPACING, d.spacing) end)
sec("chaos", function() copyInto(Chaos, d.chaos, { enabled = true }) end)
sec("sil", function() copyInto(SilFX, d.sil) end)
sec("split", function() copyInto(Split, d.split, { enabled = true }) end)
sec("health", function() copyInto(Health, d.health, { enabled = true, hp = true, display = true }) end)
sec("ghost", function()
copyInto(Ghost, d.ghost)
if type(d.ghost) == "table" then FNFCamera.dnPower = num(d.ghost.zoom, FNFCamera.dnPower) end
end)
sec("boss", function() copyInto(Boss, d.boss, { active = true }) end)
sec("arrows", function()
copyInto(Arrows, d.arrows)
Arrows.hitSize = Arrows.size * 1.16
end)
sec("camera", function()
local c = d.camera
if type(c) ~= "table" then return end
if type(c.preset) == "string" and CAM_PRESETS[c.preset] then FNFCamera:ApplyPreset(c.preset) end
FNFCamera.camDistance = num(c.distance, FNFCamera.camDistance)
FNFCamera.camHeight = num(c.height, FNFCamera.camHeight)
FNFCamera.camHorizontal = num(c.horizontal, FNFCamera.camHorizontal)
FNFCamera.camAngle = num(c.angle, FNFCamera.camAngle)
FNFCamera.camFlipped = pick(c.flipped, FNFCamera.camFlipped)
CAM_CFG.BaseFOV = num(c.fov, CAM_CFG.BaseFOV)
CAM_CFG.BarsEnabled = pick(c.bars, CAM_CFG.BarsEnabled)
FNFCamera.settleEnabled = pick(c.settleEnabled, FNFCamera.settleEnabled)
FNFCamera.settleAfter = num(c.settleAfter, FNFCamera.settleAfter)
FNFCamera.orbitSpeed = num(c.orbitSpeed, FNFCamera.orbitSpeed)
FNFCamera.orbitAmp = num(c.orbitAmp, FNFCamera.orbitAmp)
FNFCamera.dutch = num(c.dutch, FNFCamera.dutch)
FNFCamera.handheld = num(c.handheld, FNFCamera.handheld)
FNFCamera.lead = num(c.lead, FNFCamera.lead)
FNFCamera.beatPower = num(c.beatPower, FNFCamera.beatPower)
FNFCamera.snapHits = pick(c.snapHits, FNFCamera.snapHits)
FNFCamera.nudgeScale = num(c.nudgeScale, FNFCamera.nudgeScale)
FNFCamera.tiltScale = num(c.tiltScale, FNFCamera.tiltScale)
FNFCamera.punchScale = num(c.punchScale, FNFCamera.punchScale)
FNFCamera.returnSpeed = num(c.returnSpeed, FNFCamera.returnSpeed)
FNFCamera.beatAccent = num(c.beatAccent, FNFCamera.beatAccent)
if c.beatZoom == true then FNFCamera:SetBeatZoom(true) end
end)
sec("fight", function()
local f = d.fight
if type(f) ~= "table" then return end
fightState.pushPerHit = num(f.pushPerHit, fightState.pushPerHit)
fightState.pushPerDouble = num(f.pushPerDouble, fightState.pushPerDouble)
fightState.momentumCap = num(f.momentumCap, fightState.momentumCap)
proximityBlockDist = num(f.proximityDist, proximityBlockDist)
proximityBlockEnabled = pick(f.proximityOn, proximityBlockEnabled)
if type(f.rpMode) == "boolean" then rpModeEnabled = f.rpMode end
end)
-- anyone with a save file from before the tour existed isn't new
if d.tourSeen == true or d.tourSeen == nil then Intro.seen = true end
-- ride lead: AVERAGE turned out to be the wrong number; saves from before
-- defaults v3 move to MINE (plus the new screen-delay allowance) once
if (tonumber(d.defaultsVersion) or 1) < 3 then
Ride.pingSource = "MINE"
if (Ride.extraMs or 0) < 50 then Ride.extraMs = 50 end
end
-- saves from before defaults v2 had these switched on without you asking
if (tonumber(d.defaultsVersion) or 1) < 2 then
FNFCamera.snapHits = false
FNFCamera.settleEnabled = false
SPACING.enabled = false
rpModeEnabled = false
end
return true, failed
end
-- ---- file operations ----------------------------------------------------
function Save.write(slot, quiet)
slot = slot or Save.slot
if not Save.available then
if not quiet then F.notify("No file access here — use EXPORT instead", "error") end
return false
end
local json = Save.serialize()
if not json then
if not quiet then F.notify("Couldn't encode settings: " .. tostring(Save.lastError), "error") end
return false
end
local ok, err = pcall(Save.api.write, Save.path(slot), json)
if not ok then
Save.lastError = "write: " .. tostring(err)
if not quiet then F.notify("Save failed: " .. tostring(err), "error") end
return false
end
Save.lastJson = json
Save.lastSaved = os.date("%H:%M:%S")
Save.lastError = nil
if Save.refreshUI then Save.refreshUI() end
if not quiet then F.notify("Saved to slot " .. slot, "success") end
return true
end
function Save.read(slot)
slot = slot or Save.slot
if not Save.available then return nil, "no file access" end
local ok, json = pcall(Save.api.read, Save.path(slot))
if not ok or type(json) ~= "string" or #json == 0 then return nil, "no save in slot " .. slot end
local data = Save.deserialize(json)
if not data then return nil, "save file is corrupt" end
return data, json
end
-- Runtime load: apply, then rebuild the panel so every field shows it.
function Save.load(slot)
slot = slot or Save.slot
local data, info = Save.read(slot)
if not data then F.notify("Load failed — " .. tostring(info), "error"); return false end
local _, failed = Save.apply(data)
Save.lastJson = info
Save.lastLoaded = os.date("%H:%M:%S")
Save.slot = slot
Save.rebuildAfterLoad()
if #failed > 0 then
F.notify("Loaded slot " .. slot .. " — skipped: " .. table.concat(failed, ", "), "warn")
else
F.notify("Loaded slot " .. slot, "success")
end
return true
end
-- Rebuild everything that captured values at build time.
function Save.rebuildAfterLoad()
pcall(function() F.buildArrowHud() end)
pcall(function() Health.build(); Health.setIcons(Health.playerIcon, Health.oppIcon) end)
pcall(function() Keys.build() end)
pcall(function() F.setIdleBPM(idleBPM) end)
pcall(function() F.buildGUI() end)
pcall(function() F.setTheme(currentThemeName) end)
end
function Save.wipe(slot)
slot = slot or Save.slot
if not Save.available then return end
if Save.api.delfile then
pcall(Save.api.delfile, Save.path(slot))
else
pcall(Save.api.write, Save.path(slot), "") -- no delfile: blank it
end
if slot == Save.slot then Save.lastJson = nil; Save.lastSaved = "never" end
if Save.refreshUI then Save.refreshUI() end
F.notify("Cleared slot " .. slot, "info")
end
-- Clipboard fallback: works even when the executor has no file access.
function Save.export()
local json = Save.serialize()
if not json then F.notify("Couldn't encode settings", "error"); return end
if Save.api.clip then
pcall(Save.api.clip, json)
F.notify("Settings copied to clipboard", "success")
else
F.notify("No clipboard access — the text is in the box instead", "warn")
end
return json
end
function Save.import(json)
local data = Save.deserialize(json)
if not data then F.notify("That text isn't a valid settings export", "error"); return false end
local _, failed = Save.apply(data)
Save.rebuildAfterLoad()
if #failed > 0 then
F.notify("Imported — skipped: " .. table.concat(failed, ", "), "warn")
else
F.notify("Settings imported", "success")
end
return true
end
-- Called in the launch sequence BEFORE the GUI is built, so the panel is
-- constructed straight from your saved settings.
function Save.boot()
Save.resolveApi()
if not Save.available then
F.log("no writefile/readfile — saving disabled, clipboard export still works")
return
end
local data, info = Save.read(Save.slot)
if data then
local _, failed = Save.apply(data)
Save.lastJson = info
Save.lastLoaded = os.date("%H:%M:%S")
if #failed > 0 then F.log("boot load skipped: " .. table.concat(failed, ", ")) end
end
end
function Save.startAuto()
if Save.autoThread then return end
Save.autoThread = task.spawn(function()
while true do
task.wait(Save.interval)
if Save.autoSave and Save.available then
-- only touch the disk if something actually changed
local json = Save.serialize()
if json and json ~= Save.lastJson then Save.write(Save.slot, true) end
end
end
end)
end
-- ============================================================
-- RIDE v3 — hard lock
--
-- Stores your position in the object's own coordinate space and
-- rebuilds your world CFrame from it every frame:
--
-- root.CFrame = object.CFrame * localOffset
--
-- Nothing accumulates, so there's no drift and no error that grows
-- with speed. Walking and jumping both happen in object space too,
-- which is why a jump lands you back on the same spot even at speed.
-- ============================================================
function Ride.buildWind()
local old = PlayerGui:FindFirstChild("FNFWind")
if old then old:Destroy() end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFWind"; sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false; sg.DisplayOrder = F.silZ(55)
sg.Enabled = false; sg.Parent = PlayerGui
Ride.gui = sg
Ride.streaks = {}
for i = 1, 26 do
local s = Instance.new("Frame")
s.AnchorPoint = Vector2.new(0.5, 0.5)
s.Size = UDim2.new(0, 2, 0, 70)
s.BackgroundColor3 = Color3.new(1, 1, 1)
s.BackgroundTransparency = 1
s.BorderSizePixel = 0
s.ZIndex = F.silZ(55); s.Parent = sg
F.uiCorner(s, UDim.new(1, 0))
Ride.streaks[i] = { frame = s, ang = math.random() * math.pi * 2,
dist = math.random() * 0.5 + 0.2,
speed = 0.4 + math.random() * 0.8 }
end
Ride.readout = F.makeLabel(sg, "", UDim2.new(0.5, -130, 1, -172),
UDim2.new(0, 260, 0, 20), Color3.fromRGB(235,235,245), Enum.TextXAlignment.Center, 12)
Ride.readout.Font = Enum.Font.GothamBold
Ride.readout.TextStrokeTransparency = 0.5
Ride.readout.ZIndex = F.silZ(56)
end
-- Is this part part of somebody's character?
local function isCharacterPart(p)
local m = p:FindFirstAncestorOfClass("Model")
return m and m:FindFirstChildOfClass("Humanoid") ~= nil
end
-- Pick the object to lock to: what's under you first, then the nearest part.
function Ride.pick()
local root = character and character:FindFirstChild("HumanoidRootPart")
if not root then return nil end
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
local hit = Workspace:Raycast(root.Position, Vector3.new(0, -12, 0), params)
if hit and hit.Instance and not isCharacterPart(hit.Instance) then
return hit.Instance
end
local op = OverlapParams.new()
op.FilterType = Enum.RaycastFilterType.Exclude
op.FilterDescendantsInstances = { character }
local ok, parts = pcall(function()
return Workspace:GetPartBoundsInRadius(root.Position, Ride.pickRadius, op)
end)
if not ok or not parts then return nil end
local best, bestD = nil, math.huge
for _, p in ipairs(parts) do
if p:IsA("BasePart") and not isCharacterPart(p) and p.Transparency < 1 then
local d = (p.Position - root.Position).Magnitude
if d < bestD then best, bestD = p, d end
end
end
return best
end
function Ride.jumpVelocity(hum)
local g = Workspace.Gravity * Ride.gravityScale
local ok, useJP = pcall(function() return hum.UseJumpPower end)
if ok and useJP then return hum.JumpPower end
local okH, h = pcall(function() return hum.JumpHeight end)
return math.sqrt(2 * g * ((okH and h) or 7.2))
end
-- Borrow the character's own idle, walk, jump and fall animations from its
-- Animate script and play them ourselves, at a priority above the default
-- ones. While you're locked on, the object is moving under you, so the
-- humanoid can decide you're falling and play its falling animation. Ours
-- outrank that, so you look planted on the object instead.
function Ride.stopAnims(fade)
for _, t in pairs(Ride.tracks or {}) do
pcall(function() if t.IsPlaying then t:Stop(fade or 0.15) end end)
end
Ride.currentAnim = nil
end
function Ride.loadAnims(hum)
Ride.stopAnims(0)
Ride.tracks, Ride.currentAnim = {}, nil
local animate = character and character:FindFirstChild("Animate")
local animr = hum and hum:FindFirstChildOfClass("Animator")
if not animate or not animr then return end
local function load(folderName, preferred)
local folder = animate:FindFirstChild(folderName)
if not folder then return nil end
local anim = folder:FindFirstChild(preferred)
if not (anim and anim:IsA("Animation")) then anim = folder:FindFirstChildOfClass("Animation") end
if not anim then return nil end
local ok, t = pcall(function() return animr:LoadAnimation(anim) end)
if not ok or not t then return nil end
t.Priority = Enum.AnimationPriority.Movement
t.Looped = (folderName ~= "jump")
return t
end
Ride.tracks = {
idle = load("idle", "Animation1"),
walk = load("walk", "WalkAnim"),
jump = load("jump", "JumpAnim"),
fall = load("fall", "FallAnim"),
}
end
-- Idle when standing on it, walk when moving, jump then fall in the air.
function Ride.updateAnim(hum, moving)
local tracks = Ride.tracks
if not tracks then return end
local want = "idle"
if not Ride.grounded then want = (Ride.vy > 0) and "jump" or "fall"
elseif moving then want = "walk" end
if not tracks[want] then want = "idle" end
local t = tracks[want]
if not t then return end
if Ride.currentAnim ~= want then
local old = tracks[Ride.currentAnim]
if old and old ~= t and old.IsPlaying then old:Stop(0.12) end
Ride.currentAnim = want
end
if not t.IsPlaying then t:Play(0.12) end
if want == "walk" then t:AdjustSpeed(math.max(hum.WalkSpeed / 16, 0.1)) end
end
-- Ping in milliseconds. MINE reads yours from the network stats Roblox shows
-- on its own performance overlay. AVERAGE also tries to read every other
-- player's ping and averages them all. If other players' pings can't be read
-- here it quietly falls back to yours and reports that, so you always know
-- which one you're getting.
-- Returns: ms, how many players went into it, whether any other player's ping was readable.
function Ride.measurePing()
local mine
local ok, v = pcall(function()
return game:GetService("Stats").Network.ServerStatsItem["Data Ping"]:GetValue()
end)
if ok and type(v) == "number" and v > 0 then mine = v end
if not mine then
local ok2, p = pcall(function() return LocalPlayer:GetNetworkPing() end) -- seconds
if ok2 and type(p) == "number" and p > 0 then mine = p * 1000 end
end
if Ride.pingSource ~= "AVERAGE" then return mine, 1, false end
local sum, n, others = 0, 0, 0
if mine then sum, n = mine, 1 end
for _, plr in ipairs(Players:GetPlayers()) do
if plr ~= LocalPlayer then
local okp, p = pcall(function() return plr:GetNetworkPing() end) -- seconds
if okp and type(p) == "number" and p > 0 and p < 5 then
sum = sum + p * 1000
n = n + 1
others = others + 1
end
end
end
if n == 0 then return nil, 0, false end
return sum / n, n, others > 0
end
-- If YOU are simulating the object (you're driving it), your screen and
-- everyone else's agree on where it is, so there's nothing to compensate for.
function Ride.locallyOwned(part)
if part.Anchored then return false end
local ok, age = pcall(function() return part.ReceiveAge end)
return ok and age == 0
end
-- Writes your position AND velocity from the object-space state.
--
-- The root is NOT anchored. An anchored part moved by a client script never
-- replicates, so other players saw you frozen where you locked on while the
-- object drove away. Unanchored, you keep network ownership and this position
-- is what gets sent to everyone. Velocity is written too, so other players
-- interpolate you smoothly instead of seeing you jump between updates.
--
-- Runs twice a frame: once just before physics (Stepped), so physics starts
-- from the right place, and once just before the camera, so it never sees a
-- stale frame. It only writes state, so running twice does no harm.
function Ride.apply(forReplication)
if not Ride.enabled then return end
local part = Ride.part
local root = character and character:FindFirstChild("HumanoidRootPart")
if not part or not part.Parent or not root then return end
local pcf = part.CFrame
-- Other players see you about one round trip (plus your screen's own
-- delay on the object) behind where the object is. So the pose sent out
-- -- the write right before physics -- is where the object WILL be by
-- the time it arrives: its position AND rotation, extrapolated with its
-- speed and spin. Rotation matters: leading in a straight line alone is
-- what threw you off the side on every turn.
-- The write right before the camera uses the object as it is now, so on
-- your own screen you stay glued to it.
local base = pcf
if forReplication and Ride.leadTime > 0 then
local t = Ride.leadTime
local rot = pcf.Rotation
local w = Ride.platAng
local a = w.Magnitude * t
if a > 1e-5 then rot = CFrame.fromAxisAngle(w.Unit, a) * rot end
base = CFrame.new(pcf.Position + Ride.platVel * t) * rot
end
local cf = base
* CFrame.new(Ride.localPos + Vector3.new(0, Ride.yOff, 0))
* CFrame.Angles(0, Ride.yaw, 0)
root.CFrame = cf
-- Velocity of the exact spot you're standing on (spin included), so the
-- smoothing on other screens carries you around a turn between updates
-- instead of sliding you off in a straight line.
local r = cf.Position - base.Position
root.AssemblyLinearVelocity = Ride.platVel + Ride.platAng:Cross(r) + Ride.walkVel + base.UpVector * Ride.vy
root.AssemblyAngularVelocity = Ride.platAng
end
function Ride.step(dt)
local part = Ride.part
local char = character
local root = char and char:FindFirstChild("HumanoidRootPart")
local hum = char and char:FindFirstChildOfClass("Humanoid")
if not part or not part.Parent or not root or not hum then
Ride.unlock("lost the object")
return
end
dt = math.clamp(dt, 1/240, 1/20)
local pcf = part.CFrame
-- ---- walking, done in the object's own space ----
local moving = false
Ride.walkVel = Vector3.new(0, 0, 0)
local md = hum.MoveDirection
if Ride.walkWith and md.Magnitude > 0.05 then
local lm = pcf:VectorToObjectSpace(md)
lm = Vector3.new(lm.X, 0, lm.Z)
if lm.Magnitude > 1e-3 then
local speed = hum.WalkSpeed * math.min(md.Magnitude, 1)
Ride.localPos = Ride.localPos + lm.Unit * speed * dt
Ride.yaw = math.atan2(-lm.X, -lm.Z)
Ride.walkVel = pcf:VectorToWorldSpace(lm.Unit * speed)
moving = true
end
end
-- ---- jump arc, also in object space ----
if not Ride.grounded then
Ride.vy = Ride.vy - Workspace.Gravity * Ride.gravityScale * dt
Ride.yOff = Ride.yOff + Ride.vy * dt
if Ride.yOff <= 0 then
Ride.yOff, Ride.vy, Ride.grounded = 0, 0, true
end
end
-- ---- keep you on the surface ----
if Ride.clampEdges then
local half = part.Size / 2
local mx = math.max(0, half.X - Ride.edgeMargin)
local mz = math.max(0, half.Z - Ride.edgeMargin)
Ride.localPos = Vector3.new(
math.clamp(Ride.localPos.X, -mx, mx),
Ride.localPos.Y,
math.clamp(Ride.localPos.Z, -mz, mz))
end
-- (position is written by Ride.apply() below, once the object's speed is known)
-- ---- animation: idle on it, walk when moving, jump / fall in the air ----
Ride.updateAnim(hum, moving)
-- ---- object speed, for the effects ----
local vel, ang = Vector3.new(0, 0, 0), Vector3.new(0, 0, 0)
if Ride.lastCF then
vel = (pcf.Position - Ride.lastCF.Position) / dt
local okA, axis, angle = pcall(function()
return (pcf.Rotation * Ride.lastCF.Rotation:Inverse()):ToAxisAngle()
end)
if okA and angle == angle and math.abs(angle) > 1e-6 then ang = axis * (angle / dt) end
end
Ride.lastCF = pcf
Ride.lastPos = pcf.Position
if vel.Magnitude > 2000 or ang.Magnitude > 60 then -- a teleport, not motion
vel, ang = Vector3.new(0, 0, 0), Vector3.new(0, 0, 0)
end
-- smoothed, so noisy per-frame speed doesn't shake what gets replicated
Ride.platVel = Ride.platVel:Lerp(vel, 1 - math.exp(-14 * dt))
Ride.platAng = Ride.platAng:Lerp(ang, 1 - math.exp(-14 * dt))
Ride.turnDeg = math.deg(Ride.platAng.Magnitude)
-- ping compensation: sample a few times a second, smooth it, then lead
local clk = os.clock()
if clk - Ride.lastPingT > 0.25 then
Ride.lastPingT = clk
local ms, cnt, others = Ride.measurePing()
if ms then
Ride.pingMs = (Ride.pingMs <= 0) and ms or (Ride.pingMs * 0.7 + ms * 0.3)
Ride.pingCount, Ride.pingOthers = cnt, others
end
if Ride.pingSource == "AVERAGE" and not others and not Ride.warnedOthers
and #Players:GetPlayers() > 1 then
Ride.warnedOthers = true
F.notify("Can't read other players' pings in this game — using yours only", "warn")
end
end
local lead = 0
if Ride.pingComp and not Ride.locallyOwned(part) then
lead = math.min((Ride.pingMs + Ride.extraMs) / 1000 * Ride.pingScale, Ride.maxLead)
end
Ride.leadTime = lead
Ride.leadStuds = (Ride.platVel * lead).Magnitude
Ride.apply(false)
local accel = (vel - Ride.lastVel) / dt
Ride.lastVel = vel
Ride.lateralG = Ride.lateralG + ((accel:Dot(root.CFrame.RightVector) / 60) - Ride.lateralG)
* (1 - math.exp(-8 * dt))
Ride.speed = vel.Magnitude
Ride.smooth = Ride.smooth + (Ride.speed - Ride.smooth) * (1 - math.exp(-6 * dt))
local t = math.clamp((Ride.smooth - Ride.minSpeed) / math.max(1, Ride.maxSpeedAt - Ride.minSpeed), 0, 1)
FNFCamera.rideShake = Ride.shake and (t * Ride.shakeScale) or 0
FNFCamera.rideFov = Ride.speedFov and (t * 14) or 0
FNFCamera.rideLean = Ride.gLean and math.clamp(Ride.lateralG * 6 * Ride.leanScale, -9, 9) or 0
if Ride.gui then
local show = Ride.wind and t > 0.02
Ride.gui.Enabled = true
for _, st in ipairs(Ride.streaks) do
if show then
st.dist = st.dist + dt * st.speed * (0.6 + t * 4.5)
if st.dist > 1.15 then
st.dist = 0.05 + math.random() * 0.1
st.ang = math.random() * math.pi * 2
end
local f = st.frame
f.Position = UDim2.new(0.5 + math.cos(st.ang) * st.dist * 0.75, 0,
0.5 + math.sin(st.ang) * st.dist * 0.75, 0)
f.Rotation = math.deg(st.ang) + 90
f.Size = UDim2.new(0, 2, 0, 30 + t * 130 * st.speed)
f.BackgroundTransparency = math.clamp(1 - t * 0.85 * Ride.windScale, 0, 1)
else
st.frame.BackgroundTransparency = 1
end
end
if Ride.readout then
Ride.readout.Text = string.format("LOCKED %.0f studs/s", Ride.smooth)
end
end
end
function Ride.lock()
local root = character and character:FindFirstChild("HumanoidRootPart")
local hum = character and character:FindFirstChildOfClass("Humanoid")
if not root or not hum then return false end
local part = Ride.pick()
if not part then
F.notify("Nothing to lock to — stand on it or get close", "warn")
return false
end
-- capture where we are relative to it, right now
local rel = part.CFrame:ToObjectSpace(root.CFrame)
Ride.part = part
Ride.localPos = rel.Position
local look = rel.LookVector
Ride.yaw = math.atan2(-look.X, -look.Z)
Ride.yOff, Ride.vy, Ride.grounded = 0, 0, true
Ride.lastPos, Ride.lastVel = part.Position, Vector3.new(0, 0, 0)
Ride.speed, Ride.smooth, Ride.lateralG = 0, 0, 0
-- Never anchor: it would stop your position replicating (other players
-- would see you floating where you locked on). Instead we rewrite position
-- and velocity every frame before physics, and stop the humanoid from
-- turning you or tripping over at speed.
Ride.wasAnchored = root.Anchored
root.Anchored = false
Ride.savedAutoRotate = hum.AutoRotate
hum.AutoRotate = false
Ride.savedStates = {}
for _, st in ipairs({ Enum.HumanoidStateType.FallingDown, Enum.HumanoidStateType.Ragdoll,
Enum.HumanoidStateType.Freefall }) do
local ok, on = pcall(function() return hum:GetStateEnabled(st) end)
Ride.savedStates[st] = (not ok) or on
pcall(function() hum:SetStateEnabled(st, false) end)
end
Ride.platVel, Ride.walkVel = Vector3.new(0, 0, 0), Vector3.new(0, 0, 0)
pcall(function() hum:ChangeState(Enum.HumanoidStateType.Running) end) -- drop any falling pose
Ride.leadTime, Ride.lastCF = 0, part.CFrame
Ride.platAng, Ride.turnDeg = Vector3.new(0, 0, 0), 0
Ride.warnedOthers, Ride.lastPingT = false, 0
Ride.loadAnims(hum)
if Ride.jumpConn then Ride.jumpConn:Disconnect() end
Ride.jumpConn = UserInputService.JumpRequest:Connect(function()
if Ride.enabled and Ride.jumpWith and Ride.grounded then
local h = character and character:FindFirstChildOfClass("Humanoid")
if h then
Ride.vy = Ride.jumpVelocity(h)
Ride.grounded = false
end
end
end)
if not Ride.gui or not Ride.gui.Parent then Ride.buildWind() end
-- run just before the camera, so the camera always sees our final position
pcall(function() RunService:UnbindFromRenderStep(Ride.bindName) end)
RunService:BindToRenderStep(Ride.bindName, Enum.RenderPriority.Camera.Value - 1,
function(dt) Ride.step(dt) end)
-- and again right before physics, which is what actually gets replicated
if Ride.stepConn then Ride.stepConn:Disconnect() end
Ride.stepConn = RunService.Stepped:Connect(function() Ride.apply(true) end)
Ride.enabled = true
if Ride.refreshUI then Ride.refreshUI() end
F.notify("Locked to " .. part.Name, "success")
return true
end
function Ride.unlock(reason)
if not Ride.enabled then return end
Ride.enabled = false
pcall(function() RunService:UnbindFromRenderStep(Ride.bindName) end)
if Ride.jumpConn then Ride.jumpConn:Disconnect(); Ride.jumpConn = nil end
if Ride.stepConn then Ride.stepConn:Disconnect(); Ride.stepConn = nil end
local root = character and character:FindFirstChild("HumanoidRootPart")
if root then
root.Anchored = Ride.wasAnchored
-- leave with the object's velocity so letting go doesn't stop you dead
if not root.Anchored then
pcall(function() root.AssemblyLinearVelocity = Ride.lastVel end)
end
end
local hum = character and character:FindFirstChildOfClass("Humanoid")
if hum then
if Ride.savedAutoRotate ~= nil then hum.AutoRotate = Ride.savedAutoRotate end
for st, on in pairs(Ride.savedStates or {}) do
pcall(function() hum:SetStateEnabled(st, on) end)
end
end
Ride.savedAutoRotate, Ride.savedStates = nil, nil
Ride.platVel, Ride.walkVel = Vector3.new(0, 0, 0), Vector3.new(0, 0, 0)
Ride.stopAnims(0.2)
Ride.tracks = nil
Ride.leadTime, Ride.leadStuds, Ride.lastCF = 0, 0, nil
Ride.platAng, Ride.turnDeg = Vector3.new(0, 0, 0), 0
if Ride.gui then Ride.gui.Enabled = false end
FNFCamera.rideShake, FNFCamera.rideFov, FNFCamera.rideLean = 0, 0, 0
Ride.part = nil
if Ride.refreshUI then Ride.refreshUI() end
F.notify(reason and ("Unlocked — " .. reason) or "Unlocked", "info")
end
-- Kept so the keybind and older call sites keep working.
function Ride.setEnabled(state)
if state then Ride.lock() else Ride.unlock() end
end
-- ============================================================
-- POSE HOLD
-- When you let go of a short pose, it stays frozen on its final
-- frame for Hold.duration seconds instead of dropping back to
-- idle. Another real animation (Action priority and up, like an
-- emote or your next note) cuts the hold immediately. Idle,
-- Core and Movement tracks don't count, so standing around or
-- walking won't break it.
-- ============================================================
function Hold.qualifies(track)
if not Hold.enabled or not track then return false end
if track.Looped then return false end -- hold-loop binds manage themselves
if Hold.holdAll then return true end
local len = track.Length
return len > 0 and len <= Hold.shortAt
end
-- Drop our reference and kill any pending timer WITHOUT stopping the track.
-- Used when the same track is about to be reused, e.g. the cutter's one
-- shared track moving on to the next slice.
function Hold.detach()
Hold.track = nil
Hold.token = Hold.token + 1
end
function Hold.start(track, label)
-- The cutter plays every slice on one shared track. Clearing a previous
-- hold on that same track would stop the pose we're about to hold, which
-- is what dropped you back to idle on every release after the first.
if Hold.track and Hold.track ~= track then
Hold.clear(false)
else
Hold.detach()
end
Hold.track = track
Hold.label = label or "pose"
Hold.token = Hold.token + 1
Hold.releaseAt = os.clock() + Hold.duration
if Hold.untilNext then return end -- no timer: the next pose ends it
local tok = Hold.token
task.delay(Hold.duration, function()
if Hold.token == tok then Hold.clear(true) end
end)
end
-- natural = true when the timer ran out; false when something interrupted it
function Hold.clear(natural)
local t = Hold.track
Hold.track = nil
Hold.token = Hold.token + 1
if t and t.IsPlaying then
pcall(function() t:Stop(natural and Hold.fadeOut or 0.05) end)
end
if natural then
StatusState.playing = false
if F.refreshStatusUI then F.refreshStatusUI() end
end
end
function Hold.remaining()
if not Hold.track then return 0 end
if Hold.untilNext then return math.huge end
return math.max(0, Hold.releaseAt - os.clock())
end
-- Listen on the player's animator for anything that should break a hold.
function Hold.watch(animr)
if Hold.conn then Hold.conn:Disconnect(); Hold.conn = nil end
if not animr then return end
Hold.conn = animr.AnimationPlayed:Connect(function(tr)
if not Hold.track or tr == Hold.track then return end
local p = tr.Priority
if p == Enum.AnimationPriority.Idle
or p == Enum.AnimationPriority.Core
or p == Enum.AnimationPriority.Movement then
return
end
Hold.clear(false)
end)
end
-- ============================================================
-- HOLD STREAK
-- Hold a note key and a streak grows out of that arrow; let go
-- and it pulls back in. Health and score trickle in while it
-- fills. Taps never make a streak, and a full streak stops
-- paying, so sitting on a key can't farm health.
-- ============================================================
function Notes.strumY() return Arrows.size / 2 end
function Notes.laneX(dir)
local f = arrowHudFrames[dir]
return f and (f.slot + Arrows.size / 2) or 0
end
-- Lives inside the arrow strip so streaks move, lean and bob with it.
-- The strip gets rebuilt sometimes, so re-home when it does.
function Notes.ensureLayer()
local holder = Arrows.container
if not holder or not holder.Parent then return nil end
if Notes.layer and Notes.layer.Parent == holder then return Notes.layer end
for _, h in pairs(Notes.held) do if h.tail then h.tail:Destroy() end end
Notes.held = {}
local layer = Instance.new("Frame")
layer.Name = "HoldStreaks"
layer.Size = UDim2.new(1, 0, 1, 0)
layer.BackgroundTransparency = 1
layer.ClipsDescendants = false
layer.ZIndex = F.silZ(61)
layer.Parent = holder
Notes.layer = layer
local score = Instance.new("TextLabel")
score.Name = "Score"
score.Size = UDim2.new(1, 0, 0, 20)
score.Position = UDim2.new(0, 0, 1, 6)
score.BackgroundTransparency = 1
score.TextColor3 = Color3.fromRGB(235, 235, 245)
score.Font = Enum.Font.GothamBold
score.TextSize = 14
score.TextStrokeTransparency = 0.5
score.ZIndex = F.silZ(66)
score.Parent = layer
Notes.scoreLbl = score
return layer
end
function Notes.press(dir)
if not Notes.enabled then return end
local old = Notes.held[dir]
if old and old.tail then old.tail:Destroy() end
Notes.held[dir] = { start = os.clock(), tail = nil }
end
function Notes.release(dir)
local h = Notes.held[dir]
Notes.held[dir] = nil
if not h or not h.tail then return end
-- pull the streak back into the arrow and fade it
local tail = h.tail
TweenService:Create(tail, TweenInfo.new(0.16, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
Size = UDim2.new(0, Arrows.size * 0.3, 0, 0),
Position = UDim2.new(0, Notes.laneX(dir), 0, Notes.strumY()),
BackgroundTransparency = 1,
}):Play()
task.delay(0.18, function() if tail then tail:Destroy() end end)
end
function Notes.step(dt)
if not Notes.enabled then return end
local layer = Notes.ensureLayer()
if not layer then return end
dt = math.clamp(dt, 0, 1/20)
local now = os.clock()
local sign = (Notes.grow == "UP") and -1 or 1
local sY = Notes.strumY()
for dir, h in pairs(Notes.held) do
local elapsed = now - h.start - Notes.startDelay
if elapsed > 0 then
if not h.tail then
local tail = Instance.new("Frame")
tail.AnchorPoint = Vector2.new(0.5, 0)
tail.BackgroundColor3 = ARROW_COLORS[dir].hit
tail.BackgroundTransparency = 0.2
tail.BorderSizePixel = 0
tail.ZIndex = F.silZ(62)
tail.Parent = layer
F.uiCorner(tail, UDim.new(0, 6))
h.tail = tail
end
local frac = math.clamp(elapsed / math.max(Notes.fullTime, 0.05), 0, 1)
local len = Notes.length * frac
local top = (sign < 0) and (sY - len) or sY
h.tail.Position = UDim2.new(0, Notes.laneX(dir), 0, top)
h.tail.Size = UDim2.new(0, Arrows.size * 0.3, 0, len)
-- trickle only while it's still filling
if frac < 1 then
Health.hp = math.clamp(Health.hp + Notes.holdHealPerSec * dt, 0, 1)
Notes.score = Notes.score + Notes.scorePerSec * dt
if Notes.score > Notes.best then Notes.best = Notes.score end
end
end
end
if Notes.scoreLbl then
Notes.scoreLbl.Text = (Notes.score > 0) and string.format("SCORE %d", math.floor(Notes.score)) or ""
end
end
function Notes.clear()
for _, h in pairs(Notes.held) do if h.tail then h.tail:Destroy() end end
Notes.held = {}
end
function Notes.start()
if Notes.conn then return end
Notes.conn = RunService.RenderStepped:Connect(function(dt) Notes.step(dt) end)
end
function Notes.setEnabled(state)
Notes.enabled = state
if state then
Notes.ensureLayer()
Notes.start()
else
Notes.clear()
end
if Notes.refreshUI then Notes.refreshUI() end
F.notify(state and "Hold streaks ON" or "Hold streaks OFF", "info")
end
function Notes.resetScore()
Notes.score = 0
end
-- ============================================================
-- LOADING SCREEN
-- Arrows pop in one by one, then the title slams in out of
-- nowhere. It checks for a saved setup and tells you whether
-- you're new or back. Click or press any key to skip.
-- ============================================================
function Intro.isNew()
if Intro.seen then return false end
-- same session re-inject with no file access: remember via getgenv
local g
pcall(function() g = getgenv().FNFInjectorTourSeen end)
return not g
end
function Intro.markSeen()
Intro.seen = true
pcall(function() getgenv().FNFInjectorTourSeen = true end)
if Save.available then pcall(function() Save.write(Save.slot, true) end) end
end
function Intro.startLoading()
local old = PlayerGui:FindFirstChild("FNFLoading")
if old then old:Destroy() end
Intro.loadDone, Intro.skip = false, false
local sg = Instance.new("ScreenGui")
sg.Name = "FNFLoading"
sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false
sg.DisplayOrder = F.silZ(195)
sg.Parent = PlayerGui
Intro.loadGui = sg
local back = Instance.new("Frame")
back.Size = UDim2.new(1, 0, 1, 0)
back.BackgroundColor3 = Color3.fromRGB(6, 5, 12)
back.BackgroundTransparency = 1
back.BorderSizePixel = 0
back.Active = true
back.ZIndex = F.silZ(190)
back.Parent = sg
-- the arrows, centred, all hidden to start
local arrows = {}
local ARW, GAP = 78, 18
local totalW = #ARROW_ORDER * ARW + (#ARROW_ORDER - 1) * GAP
for i, dir in ipairs(ARROW_ORDER) do
local a = Instance.new("Frame")
a.AnchorPoint = Vector2.new(0.5, 0.5)
a.Position = UDim2.new(0.5, -totalW / 2 + (i - 1) * (ARW + GAP) + ARW / 2, 0.5, 40)
a.Size = UDim2.new(0, 0, 0, 0)
a.Rotation = -25
a.BackgroundColor3 = ARROW_COLORS[dir].hit
a.BorderSizePixel = 0
a.ZIndex = F.silZ(192)
a.Parent = back
F.uiCorner(a, UDim.new(0, 16))
F.uiStroke(a, Color3.fromRGB(255, 255, 255), 2).Transparency = 0.3
local s = Instance.new("TextLabel")
s.Size = UDim2.new(1, 0, 1, 0)
s.BackgroundTransparency = 1
s.Text = ARROW_SYMBOLS[dir]
s.TextColor3 = Color3.fromRGB(255, 255, 255)
s.Font = Enum.Font.GothamBlack
s.TextScaled = true
s.ZIndex = F.silZ(193)
s.Parent = a
local pad = Instance.new("UIPadding")
pad.PaddingTop = UDim.new(0.2, 0); pad.PaddingBottom = UDim.new(0.2, 0)
pad.Parent = s
arrows[i] = a
end
-- the title, invisible until it slams in
local title = Instance.new("TextLabel")
title.AnchorPoint = Vector2.new(0.5, 0.5)
title.Position = UDim2.new(0.5, 0, 0.5, -60)
title.Size = UDim2.new(1, 0, 0, 80)
title.BackgroundTransparency = 1
title.Text = "FNF ANIM INJECTOR"
title.TextColor3 = Color3.fromRGB(255, 255, 255)
title.Font = Enum.Font.GothamBlack
title.TextSize = 120
title.TextTransparency = 1
title.TextStrokeColor3 = THEMES[currentThemeName].accent
title.TextStrokeTransparency = 1
title.ZIndex = F.silZ(193)
title.Parent = back
local flash = Instance.new("Frame")
flash.Size = UDim2.new(1, 0, 1, 0)
flash.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
flash.BackgroundTransparency = 1
flash.BorderSizePixel = 0
flash.ZIndex = F.silZ(194)
flash.Parent = back
local status = F.makeLabel(back, "", UDim2.new(0, 0, 0.5, 100), UDim2.new(1, 0, 0, 20),
Color3.fromRGB(220, 220, 235), Enum.TextXAlignment.Center, 13)
status.Font = Enum.Font.GothamMedium
status.ZIndex = F.silZ(193)
local barBg = Instance.new("Frame")
barBg.AnchorPoint = Vector2.new(0.5, 0)
barBg.Position = UDim2.new(0.5, 0, 0.5, 128)
barBg.Size = UDim2.new(0, 260, 0, 4)
barBg.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
barBg.BackgroundTransparency = 0.85
barBg.BorderSizePixel = 0
barBg.ZIndex = F.silZ(193)
barBg.Parent = back
F.uiCorner(barBg, UDim.new(1, 0))
local barFill = Instance.new("Frame")
barFill.Size = UDim2.new(0, 0, 1, 0)
barFill.BackgroundColor3 = THEMES[currentThemeName].accent
barFill.BorderSizePixel = 0
barFill.ZIndex = F.silZ(194)
barFill.Parent = barBg
F.uiCorner(barFill, UDim.new(1, 0))
local hint = F.makeLabel(back, "click or press any key to skip", UDim2.new(0, 0, 1, -40),
UDim2.new(1, 0, 0, 16), Color3.fromRGB(150, 150, 170), Enum.TextXAlignment.Center, 10)
hint.ZIndex = F.silZ(193)
-- skip on click or any key
local skipConns = {}
local function doSkip() Intro.skip = true end
table.insert(skipConns, back.InputBegan:Connect(function(i)
if i.UserInputType == Enum.UserInputType.MouseButton1
or i.UserInputType == Enum.UserInputType.Touch then doSkip() end
end))
table.insert(skipConns, UserInputService.InputBegan:Connect(function(i)
if i.UserInputType == Enum.UserInputType.Keyboard then doSkip() end
end))
-- a wait that ends early if you skip
local function wait(sec)
local t0 = os.clock()
while os.clock() - t0 < sec do
if Intro.skip then return end
RunService.Heartbeat:Wait()
end
end
task.spawn(function()
-- backdrop in
TweenService:Create(back, TweenInfo.new(0.25), { BackgroundTransparency = Intro.backdropOpacity }):Play()
wait(0.2)
-- arrows pop one after another
for _, a in ipairs(arrows) do
if Intro.skip then break end
TweenService:Create(a, TweenInfo.new(0.32, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
Size = UDim2.new(0, ARW, 0, ARW), Rotation = 0,
}):Play()
wait(0.11)
end
wait(0.35)
-- title out of nowhere: no fade, it's just there, with a flash
title.TextTransparency = Intro.titleOpacity
title.TextStrokeTransparency = 0.4
TweenService:Create(title, TweenInfo.new(0.2, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
{ TextSize = 58 }):Play()
flash.BackgroundTransparency = 0.55
TweenService:Create(flash, TweenInfo.new(0.3), { BackgroundTransparency = 1 }):Play()
for _, a in ipairs(arrows) do
TweenService:Create(a, TweenInfo.new(0.08), { Size = UDim2.new(0, ARW * 1.12, 0, ARW * 1.12) }):Play()
end
wait(0.1)
for _, a in ipairs(arrows) do
TweenService:Create(a, TweenInfo.new(0.2, Enum.EasingStyle.Back),
{ Size = UDim2.new(0, ARW, 0, ARW) }):Play()
end
-- scan for a saved setup
status.Text = "scanning for your setup..."
TweenService:Create(barFill, TweenInfo.new(math.max(0.2, Intro.duration - 1.6), Enum.EasingStyle.Quad),
{ Size = UDim2.new(1, 0, 1, 0) }):Play()
wait(0.7)
if Intro.isNew() then
status.Text = "new here? quick tour coming up"
status.TextColor3 = Color3.fromRGB(120, 230, 255)
else
status.Text = "welcome back — loading your setup"
status.TextColor3 = Color3.fromRGB(120, 255, 150)
end
wait(math.max(0.3, Intro.duration - 1.9))
-- out
for _, c in ipairs(skipConns) do c:Disconnect() end
local out = TweenInfo.new(0.35, Enum.EasingStyle.Quad)
TweenService:Create(back, out, { BackgroundTransparency = 1 }):Play()
for _, d in ipairs(back:GetDescendants()) do
if d:IsA("TextLabel") then
TweenService:Create(d, out, { TextTransparency = 1, TextStrokeTransparency = 1 }):Play()
elseif d:IsA("Frame") then
TweenService:Create(d, out, { BackgroundTransparency = 1 }):Play()
elseif d:IsA("UIStroke") then
TweenService:Create(d, out, { Transparency = 1 }):Play()
end
end
task.wait(0.38)
sg:Destroy()
Intro.loadDone = true
end)
end
-- ============================================================
-- FIRST-TIME TOUR
-- Five cards, each switching the panel to the tab it explains
-- and pulsing that tab's button.
-- ============================================================
local TOUR = {
{
tab = "📋 BINDS", title = "1 · YOUR BINDS",
body = "These are your four notes: Z X C V play LEFT, DOWN, UP and RIGHT. "
.. "Click a key to change it, paste an animation ID into the box to set that note's pose, "
.. "and hold TEST to preview. Start here — set up your four poses first.",
chips = { "Z", "X", "C", "V" },
},
{
tab = "🎥 CAM", title = "2 · THE CAMERA",
body = "Turn on the cinematic camera, then type your opponent's name under SET OPPONENT "
.. "and it frames the two of you. FIND A MATCH scans the server for other players "
.. "who are running this script, so you can battle someone who has it too.",
},
{
tab = "✂ CUTTER", title = "3 · EMOTE CUTTER",
body = "Cuts one emote into four poses. Give each note an animation, scrub the timeline "
.. "to the frame you want, and mark it. Switch CUTTER ON and your note keys play "
.. "your cuts instead of the normal binds.",
},
{
tab = "🎬 MAKER", title = "4 · EVERYTHING ELSE",
body = "MAKER builds animation sequences and song sections. PREVIEW shows your character "
.. "live. FAVES and LIST store animations you like. FX has the music player and visual "
.. "extras. Press ` any time for a list of every keybind.",
},
{
tab = "⚙ SETTINGS", title = "5 · SETTINGS",
body = "Themes, the arrow HUD, save slots and autosave all live here. Your setup saves "
.. "on its own, so it'll be waiting next time. You can replay this tour from here "
.. "whenever you like.",
},
}
function Intro.pulseTab(name)
if Intro.pulse then Intro.pulse:Destroy(); Intro.pulse = nil end
local btn = F.tabButtons and F.tabButtons[name]
if not btn then return end
local s = Instance.new("UIStroke")
s.Name = "TourPulse"
s.Color = Color3.fromRGB(255, 220, 90)
s.Thickness = 2
s.Parent = btn
Intro.pulse = s
task.spawn(function()
while s.Parent do
s.Transparency = 0.1 + 0.6 * math.abs(math.sin(os.clock() * 5))
RunService.Heartbeat:Wait()
end
end)
end
function Intro.endTour()
if Intro.pulse then Intro.pulse:Destroy(); Intro.pulse = nil end
if Intro.tourGui then Intro.tourGui:Destroy(); Intro.tourGui = nil end
if F.selectTab then F.selectTab("📋 BINDS") end
Intro.markSeen()
end
function Intro.startTour()
if Intro.tourGui then Intro.tourGui:Destroy() end
Intro.step = 1
local sg = Instance.new("ScreenGui")
sg.Name = "FNFTour"
sg.ResetOnSpawn = false
sg.DisplayOrder = F.silZ(180)
sg.Parent = PlayerGui
Intro.tourGui = sg
local card = Instance.new("Frame")
card.Size = UDim2.new(0, 340, 0, 250)
card.Position = UDim2.new(0, 520, 0, 80)
card.BackgroundColor3 = Color3.fromRGB(16, 15, 24)
card.BorderSizePixel = 0
card.Active = true
card.Draggable = true
card.Parent = sg
F.uiCorner(card, UDim.new(0, 14))
F.uiStroke(card, Color3.fromRGB(255, 220, 90), 1.5).Transparency = 0.3
local tag = F.makeLabel(card, "QUICK TOUR", UDim2.new(0, 16, 0, 12), UDim2.new(0, 160, 0, 14),
Color3.fromRGB(255, 220, 90), Enum.TextXAlignment.Left, 10)
tag.Font = Enum.Font.GothamBold
local count = F.makeLabel(card, "", UDim2.new(1, -96, 0, 12), UDim2.new(0, 80, 0, 14),
Color3.fromRGB(150, 150, 170), Enum.TextXAlignment.Right, 10)
local title = F.makeLabel(card, "", UDim2.new(0, 16, 0, 30), UDim2.new(1, -32, 0, 24),
Color3.fromRGB(255, 255, 255), Enum.TextXAlignment.Left, 16)
title.Font = Enum.Font.GothamBlack
local body = F.makeLabel(card, "", UDim2.new(0, 16, 0, 60), UDim2.new(1, -32, 0, 104),
Color3.fromRGB(210, 210, 225), Enum.TextXAlignment.Left, 11)
body.TextWrapped = true
body.TextYAlignment = Enum.TextYAlignment.Top
-- key chips, used on the binds card
local chipRow = Instance.new("Frame")
chipRow.Size = UDim2.new(1, -32, 0, 30)
chipRow.Position = UDim2.new(0, 16, 0, 160)
chipRow.BackgroundTransparency = 1
chipRow.Parent = card
local chipLayout = Instance.new("UIListLayout")
chipLayout.FillDirection = Enum.FillDirection.Horizontal
chipLayout.Padding = UDim.new(0, 8)
chipLayout.Parent = chipRow
-- progress dots
local dots = {}
for i = 1, #TOUR do
local d = Instance.new("Frame")
d.Size = UDim2.new(0, 8, 0, 8)
d.Position = UDim2.new(0, 16 + (i - 1) * 14, 1, -46)
d.BorderSizePixel = 0
d.Parent = card
F.uiCorner(d, UDim.new(1, 0))
dots[i] = d
end
local skip = F.makeBtn(card, "SKIP", UDim2.new(0, 16, 1, -32), UDim2.new(0, 70, 0, 24),
Color3.fromRGB(45, 40, 60), nil, 10)
local back = F.makeBtn(card, "BACK", UDim2.new(1, -176, 1, -32), UDim2.new(0, 70, 0, 24),
Color3.fromRGB(45, 40, 60), nil, 10)
local nextB = F.makeBtn(card, "NEXT", UDim2.new(1, -100, 1, -32), UDim2.new(0, 84, 0, 24),
Color3.fromRGB(255, 190, 60), Color3.fromRGB(20, 20, 25), 11)
local function show()
local s = TOUR[Intro.step]
count.Text = Intro.step .. " / " .. #TOUR
title.Text = s.title
body.Text = s.body
for _, c in ipairs(chipRow:GetChildren()) do
if c:IsA("TextLabel") then c:Destroy() end
end
-- binds card: show the player's real keys, not just the defaults
if s.chips then
for i, row in ipairs(CurrentBindings) do
local chip = Instance.new("TextLabel")
chip.Size = UDim2.new(0, 56, 0, 28)
chip.BackgroundColor3 = ARROW_COLORS[row.dir].hit
chip.Text = row.key .. " " .. ARROW_SYMBOLS[row.dir]
chip.TextColor3 = Color3.fromRGB(15, 15, 20)
chip.Font = Enum.Font.GothamBlack
chip.TextSize = 12
chip.LayoutOrder = i
chip.Parent = chipRow
F.uiCorner(chip, UDim.new(0, 7))
end
end
for i, d in ipairs(dots) do
d.BackgroundColor3 = (i == Intro.step) and Color3.fromRGB(255, 220, 90) or Color3.fromRGB(60, 58, 75)
end
back.Visible = Intro.step > 1
nextB.Text = (Intro.step == #TOUR) and "LET'S GO" or "NEXT"
-- take them to the tab this card is about
if F.selectTab then F.selectTab(s.tab) end
Intro.pulseTab(s.tab)
local main = PlayerGui:FindFirstChild("FNFInjectorGui")
main = main and main:FindFirstChild("Main")
if main then main.Visible = true end
end
nextB.MouseButton1Click:Connect(function()
if Intro.step >= #TOUR then Intro.endTour() return end
Intro.step = Intro.step + 1
show()
end)
back.MouseButton1Click:Connect(function()
Intro.step = math.max(1, Intro.step - 1)
show()
end)
skip.MouseButton1Click:Connect(function() Intro.endTour() end)
show()
end
-- Called at the end of launch: wait out the loading screen, show the
-- panel, then start the tour for new players.
function Intro.finishBoot()
local gui = PlayerGui:FindFirstChild("FNFInjectorGui")
local main = gui and gui:FindFirstChild("Main")
if main then main.Visible = false end
task.spawn(function()
local deadline = os.clock() + 12
while not Intro.loadDone and os.clock() < deadline do task.wait(0.05) end
if main then
main.Visible = true
local target = main.Position
main.Position = target + UDim2.new(0, 0, 0, 24)
TweenService:Create(main, TweenInfo.new(0.35, Enum.EasingStyle.Quint, Enum.EasingDirection.Out),
{ Position = target }):Play()
end
if Intro.isNew() then
task.wait(0.3)
Intro.startTour()
end
end)
end
-- ============================================================
-- VIDEO PLAYER
-- Plays a video file from your own computer, locally. Roblox
-- can't accept files dragged in from Windows, so a folder in
-- your executor's workspace is the drop box: put a .webm in
-- there, refresh, click it. getcustomasset turns that file into
-- something Roblox can load on your client only, so nobody
-- else sees it.
-- ============================================================
function Vid.resolveApi()
Vid.api = {
asset = findFn("getcustomasset", getcustomasset) or findFn("getsynasset", getsynasset),
list = findFn("listfiles", listfiles),
isfile = findFn("isfile", isfile),
isfolder = findFn("isfolder", isfolder),
makefolder = findFn("makefolder", makefolder),
}
if Vid.api.makefolder then
pcall(function()
if not (Vid.api.isfolder and Vid.api.isfolder("FNFInjector")) then Vid.api.makefolder("FNFInjector") end
if not (Vid.api.isfolder and Vid.api.isfolder(Vid.folder)) then Vid.api.makefolder(Vid.folder) end
end)
end
end
local function baseName(path)
return (tostring(path):match("[^/\\]+$")) or tostring(path)
end
-- What's in the drop box? Returns { {path, name, ext}, ... }
function Vid.listFiles()
local out = {}
if not Vid.api.list then return out end
local ok, files = pcall(Vid.api.list, Vid.folder)
if not ok or type(files) ~= "table" then return out end
for _, p in ipairs(files) do
local name = baseName(p)
local ext = (name:match("%.([%w]+)$") or ""):lower()
if ext ~= "" then
-- normalise to a path relative to the workspace folder
table.insert(out, { path = Vid.folder .. "/" .. name, name = name, ext = ext })
end
end
table.sort(out, function(a, b) return a.name:lower() < b.name:lower() end)
return out
end
function Vid.buildPlayer()
local old = PlayerGui:FindFirstChild("FNFVideo")
if old then old:Destroy() end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFVideo"
sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false
sg.DisplayOrder = F.silZ(45)
sg.Enabled = false
sg.Parent = PlayerGui
Vid.gui = sg
local win = Instance.new("Frame")
win.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
win.BorderSizePixel = 0
win.Active = true
win.Draggable = true
win.ClipsDescendants = true
win.Parent = sg
F.uiCorner(win, UDim.new(0, 10))
Vid.window = win
local vf = Instance.new("VideoFrame")
vf.Size = UDim2.new(1, 0, 1, 0)
vf.BackgroundTransparency = 1
vf.Looped = Vid.looped
vf.Volume = Vid.volume
vf.Parent = win
Vid.frame = vf
-- slim progress bar along the bottom of the window
local barBg = Instance.new("Frame")
barBg.AnchorPoint = Vector2.new(0, 1)
barBg.Position = UDim2.new(0, 0, 1, 0)
barBg.Size = UDim2.new(1, 0, 0, 3)
barBg.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
barBg.BackgroundTransparency = 0.8
barBg.BorderSizePixel = 0
barBg.ZIndex = 3
barBg.Parent = win
local bar = Instance.new("Frame")
bar.Size = UDim2.new(0, 0, 1, 0)
bar.BackgroundColor3 = THEMES[currentThemeName].accent
bar.BorderSizePixel = 0
bar.ZIndex = 4
bar.Parent = barBg
Vid.bar = bar
local close = Instance.new("TextButton")
close.Size = UDim2.new(0, 22, 0, 22)
close.Position = UDim2.new(1, -28, 0, 6)
close.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
close.BackgroundTransparency = 0.4
close.Text = "X"
close.TextColor3 = Color3.fromRGB(255, 255, 255)
close.Font = Enum.Font.GothamBold
close.TextSize = 11
close.ZIndex = 5
close.Parent = win
F.uiCorner(close, UDim.new(1, 0))
close.MouseButton1Click:Connect(function() Vid.stop() end)
RunService.RenderStepped:Connect(function()
if not Vid.frame or not Vid.gui.Enabled then return end
local len = Vid.frame.TimeLength
if len and len > 0 then
Vid.bar.Size = UDim2.new(math.clamp(Vid.frame.TimePosition / len, 0, 1), 0, 1, 0)
if Vid.timeLbl then
Vid.timeLbl.Text = string.format("%d:%02d / %d:%02d",
math.floor(Vid.frame.TimePosition / 60), math.floor(Vid.frame.TimePosition % 60),
math.floor(len / 60), math.floor(len % 60))
end
end
end)
Vid.applyLayout()
end
-- Window, fullscreen, or a dim backdrop behind everything.
function Vid.applyLayout()
local win = Vid.window
if not win then return end
-- The stage draws the characters one layer above the video. Any mode
-- that lifts the video higher would cover them, so while the stage is
-- on the video is pinned to the bottom, whatever mode is selected.
local mode = Vid.stage.on and "BACKDROP" or Vid.display
if mode == "FULLSCREEN" then
win.AnchorPoint = Vector2.new(0, 0)
win.Position = UDim2.new(0, 0, 0, 0)
win.Size = UDim2.new(1, 0, 1, 0)
win.Draggable = false
Vid.gui.DisplayOrder = F.silZ(150)
elseif mode == "BACKDROP" then
win.AnchorPoint = Vector2.new(0, 0)
win.Position = UDim2.new(0, 0, 0, 0)
win.Size = UDim2.new(1, 0, 1, 0)
win.Draggable = false
Vid.gui.DisplayOrder = F.silZ(1) -- underneath every other panel
else
win.AnchorPoint = Vector2.new(1, 1)
win.Position = UDim2.new(1, -20, 1, -150)
win.Size = UDim2.new(0, 480 * Vid.scale, 0, 270 * Vid.scale)
win.Draggable = true
Vid.gui.DisplayOrder = F.silZ(45)
end
win.BackgroundTransparency = Vid.stage.on and 0 or Vid.opacity
if Vid.frame then Vid.frame.ImageTransparency = Vid.opacity end
-- and the character layer always sits directly above the video
if Vid.stage.gui then Vid.stage.gui.DisplayOrder = Vid.gui.DisplayOrder + 1 end
if Vid.syncModes then Vid.syncModes() end
end
function Vid.setStatus(text, color)
if Vid.statusLbl then
Vid.statusLbl.Text = text
Vid.statusLbl.TextColor3 = color or Color3.fromRGB(160, 160, 180)
end
end
-- Load and play. `source` is a file path in the drop box, or a Roblox video ID.
function Vid.play(source, isAssetId)
if not Vid.gui or not Vid.gui.Parent then Vid.buildPlayer() end
local content
if isAssetId then
content = "rbxassetid://" .. source
else
if not Vid.api.asset then
Vid.setStatus("Your executor has no getcustomasset, so local files can't load.", Color3.fromRGB(255, 120, 90))
F.notify("Local videos need getcustomasset", "error")
return
end
local ext = (tostring(source):match("%.([%w]+)$") or ""):lower()
if ext ~= "webm" then
Vid.setStatus("That's a ." .. ext .. " file. Roblox only plays .webm — convert it first.",
Color3.fromRGB(255, 170, 80))
F.notify("Convert to .webm first", "warn")
return
end
local ok, res = pcall(Vid.api.asset, source)
if not ok or not res then
Vid.setStatus("getcustomasset failed: " .. tostring(res), Color3.fromRGB(255, 120, 90))
return
end
content = res
Vid.lastFile = source
end
local vf = Vid.frame
pcall(function() vf:Pause() end)
vf.Video = content
vf.Looped = Vid.looped
vf.Volume = Vid.volume
Vid.current = source
Vid.gui.Enabled = true
Vid.applyLayout()
Vid.setStatus("loading " .. baseName(source) .. "...", Color3.fromRGB(0, 200, 255))
-- give it a few seconds to load before calling it a failure
task.spawn(function()
local deadline = os.clock() + 6
while os.clock() < deadline and vf.Video == content and not vf.IsLoaded do task.wait(0.1) end
if vf.Video ~= content then return end
if vf.IsLoaded then
pcall(function() vf:Play() end)
Vid.setStatus("playing " .. baseName(source), Color3.fromRGB(120, 230, 150))
else
Vid.setStatus("Roblox couldn't load it. Try re-exporting as VP9 .webm, or a shorter clip.",
Color3.fromRGB(255, 120, 90))
end
end)
end
function Vid.togglePause()
local vf = Vid.frame
if not vf or not Vid.current then return end
if vf.Playing then pcall(function() vf:Pause() end) else pcall(function() vf:Play() end) end
end
function Vid.restart()
local vf = Vid.frame
if not vf or not Vid.current then return end
vf.TimePosition = 0
pcall(function() vf:Play() end)
end
function Vid.stop()
if Vid.frame then pcall(function() Vid.frame:Pause() end) end
if Vid.gui and not Vid.stage.on then Vid.gui.Enabled = false end
if Vid.stage.on and Vid.frame then Vid.frame.Video = "" end -- stage stays, background goes black
Vid.current = nil
Vid.setStatus("stopped")
end
-- ============================================================
-- BLACK SCREEN FADE
-- Smooth to black, a beat of darkness, then a slow fade back.
-- Pressing it again mid-fade drops you straight back to black.
-- ============================================================
function Vid.blackout()
if not Vid.boGui or not Vid.boGui.Parent then
local sg = Instance.new("ScreenGui")
sg.Name = "FNFBlackout"
sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false
sg.DisplayOrder = F.silZ(185)
sg.Parent = PlayerGui
local f = Instance.new("Frame")
f.Name = "Black"
f.Size = UDim2.new(1, 0, 1, 0)
f.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
f.BackgroundTransparency = 1
f.BorderSizePixel = 0
f.Parent = sg
Vid.boGui = sg
end
local f = Vid.boGui.Black
Vid.boToken = Vid.boToken + 1
local tok = Vid.boToken
TweenService:Create(f, TweenInfo.new(math.max(Vid.boFadeIn, 0.01), Enum.EasingStyle.Sine, Enum.EasingDirection.Out),
{ BackgroundTransparency = 0 }):Play()
task.delay(Vid.boFadeIn + Vid.boHold, function()
if Vid.boToken ~= tok then return end
TweenService:Create(f, TweenInfo.new(math.max(Vid.boFadeOut, 0.05), Enum.EasingStyle.Sine, Enum.EasingDirection.InOut),
{ BackgroundTransparency = 1 }):Play()
end)
end
-- ============================================================
-- VIDEO STAGE
-- The video plays full screen and hides the whole world. You
-- and your opponent are redrawn on top of it: exact copies in
-- a full-screen viewport, using your real camera, with every
-- body part copying its real position every frame. Normal
-- colours, no black fill. Nobody else is drawn at all.
-- ============================================================
local function relPath(inst, root)
local parts = {}
local cur = inst
while cur and cur ~= root do
table.insert(parts, 1, cur.Name)
cur = cur.Parent
end
return table.concat(parts, "/")
end
local function countParts(char)
local n = 0
for _, d in ipairs(char:GetDescendants()) do
if d:IsA("BasePart") then n = n + 1 end
end
return n
end
-- A still copy of a character whose parts get placed by hand every frame.
function Vid.stageClone(char)
if not char or not char.Parent or not Vid.stage.vp then return nil end
local was = char.Archivable
char.Archivable = true
local ok, clone = pcall(function() return char:Clone() end)
char.Archivable = was
if not ok or not clone then return nil end
-- Joints would drag whole limbs around when we set one part's CFrame,
-- so every part is freed and positioned on its own instead.
local strip = {}
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("LuaSourceContainer") or d:IsA("JointInstance") or d:IsA("WeldConstraint")
or d:IsA("Constraint") or d:IsA("Sound") then
table.insert(strip, d)
end
end
for _, d in ipairs(strip) do d:Destroy() end
local hum = clone:FindFirstChildOfClass("Humanoid")
if hum then
pcall(function() hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None end)
pcall(function() hum.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOff end)
pcall(function() hum.RequiresNeck = false end)
pcall(function() hum.BreakJointsOnDeath = false end)
end
-- pair every clone part with the real part at the same place in the tree
local real = {}
for _, p in ipairs(char:GetDescendants()) do
if p:IsA("BasePart") then real[relPath(p, char)] = p end
end
local map = {}
for _, p in ipairs(clone:GetDescendants()) do
if p:IsA("BasePart") then
p.Anchored = true
p.CanCollide = false
local r = real[relPath(p, clone)]
if r then table.insert(map, { r, p }) end
end
end
clone.Name = "StageCopy"
clone.Parent = Vid.stage.vp
return { char = char, clone = clone, map = map, count = countParts(char) }
end
function Vid.stageDrop(entry)
if entry and entry.clone then pcall(function() entry.clone:Destroy() end) end
end
-- Keep the copies pointed at the right characters: respawns, a new
-- opponent, or an outfit change all trigger a fresh copy.
function Vid.stageRefresh()
local st = Vid.stage
local wantMe = character
local wantOpp = currentTarget and currentTarget.Character or nil
for _, key in ipairs({ "me", "opp" }) do
local want = (key == "me") and wantMe or wantOpp
local e = st[key]
local stale = e and (e.char ~= want or not e.char.Parent
or countParts(e.char) ~= e.count or #e.map == 0)
if stale or (not e and want) then
Vid.stageDrop(e)
st[key] = want and Vid.stageClone(want) or nil
end
end
end
function Vid.stageStep()
local st = Vid.stage
if not st.on or not st.vp then return end
-- same camera as the real one, so the copies line up exactly
local cam = Workspace.CurrentCamera
if cam and st.cam then
st.cam.CFrame = cam.CFrame
st.cam.FieldOfView = cam.FieldOfView
end
for _, key in ipairs({ "me", "opp" }) do
local e = st[key]
if e then
for _, pair in ipairs(e.map) do
local r, c = pair[1], pair[2]
if r.Parent then
c.CFrame = r.CFrame
end
end
end
end
end
function Vid.startStage()
local st = Vid.stage
if st.on then return end
st.on = true
st.prevDisplay = Vid.display
-- the video layer: full screen, behind the panels
if not Vid.gui or not Vid.gui.Parent then Vid.buildPlayer() end
Vid.display = "BACKDROP"
Vid.gui.Enabled = true
Vid.applyLayout()
if Vid.window then Vid.window.BackgroundTransparency = 0 end -- solid black if nothing's playing
if not Vid.current then F.notify("Stage on — play a video from the list for the background", "info") end
-- the character layer, one step above the video
local old = PlayerGui:FindFirstChild("FNFVideoStage")
if old then old:Destroy() end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFVideoStage"
sg.IgnoreGuiInset = true -- must match the real camera's full screen
sg.ResetOnSpawn = false
sg.DisplayOrder = (Vid.gui and Vid.gui.DisplayOrder or 1) + 1
sg.Parent = PlayerGui
st.gui = sg
local vp = Instance.new("ViewportFrame")
vp.Size = UDim2.new(1, 0, 1, 0)
vp.BackgroundTransparency = 1
vp.Ambient = Color3.fromRGB(150, 150, 160)
vp.LightColor = Color3.fromRGB(255, 255, 255)
local okSun, sun = pcall(function() return game:GetService("Lighting"):GetSunDirection() end)
vp.LightDirection = okSun and -sun or Vector3.new(-0.5, -1, -0.4)
vp.Parent = sg
st.vp = vp
local cam = Instance.new("Camera")
cam.Parent = vp
vp.CurrentCamera = cam
st.cam = cam
Vid.stageRefresh()
Vid.applyLayout()
-- run right after the camera finishes, so nothing lags a frame
pcall(function() RunService:UnbindFromRenderStep("FNFVideoStage") end)
RunService:BindToRenderStep("FNFVideoStage", Enum.RenderPriority.Camera.Value + 1, Vid.stageStep)
st.watch = task.spawn(function()
while st.on do
task.wait(0.5)
if st.on then
pcall(Vid.stageRefresh)
-- real fades only (like the invisible root part), never the
-- camera's zoomed-in hiding, which would erase your own copy
for _, key in ipairs({ "me", "opp" }) do
local e = st[key]
if e then
for _, pair in ipairs(e.map) do
if pair[1].Parent then pair[2].Transparency = pair[1].Transparency end
end
end
end
if Vid.refreshStageUI then Vid.refreshStageUI() end
end
end
end)
if Vid.refreshStageUI then Vid.refreshStageUI() end
if not currentTarget then F.notify("No opponent set — only you are shown. Pick one in the CAM tab.", "warn") end
end
function Vid.stopStage()
local st = Vid.stage
if not st.on then return end
st.on = false
pcall(function() RunService:UnbindFromRenderStep("FNFVideoStage") end)
Vid.stageDrop(st.me); Vid.stageDrop(st.opp)
st.me, st.opp = nil, nil
if st.gui then st.gui:Destroy() end
st.gui, st.vp, st.cam = nil, nil, nil
Vid.display = st.prevDisplay or "WINDOW"
if Vid.window then Vid.window.BackgroundTransparency = Vid.opacity end
if not Vid.current and Vid.gui then Vid.gui.Enabled = false end
Vid.applyLayout()
if Vid.refreshStageUI then Vid.refreshStageUI() end
end
-- ============================================================
-- RESULT — win / lose
-- WIN plays a cheer. LOSE sits you down and launches you. Both
-- are manual: you decide the outcome with a key or a button.
-- ============================================================
function Result.mph(studsPerSec)
return studsPerSec * 0.28 * 2.23694 -- ~0.28 m per stud
end
-- Your avatar's own built-in animation (cheer, sit...), which plays
-- in any game because Roblox owns it.
function Result.defaultAnim(slot)
local animate = character and character:FindFirstChild("Animate")
local folder = animate and animate:FindFirstChild(slot)
local anim = folder and folder:FindFirstChildOfClass("Animation")
return anim and anim.AnimationId or nil
end
function Result.loadTrack(customId, slot)
if not animator then return nil end
local id
if customId and customId ~= "" then
id = "rbxassetid://" .. customId
else
id = Result.defaultAnim(slot)
end
if not id then return nil end
local ok, t = pcall(function()
local a = Instance.new("Animation")
a.AnimationId = id
return animator:LoadAnimation(a)
end)
if ok and t then
t.Priority = Enum.AnimationPriority.Action4 -- over every pose and emote
return t
end
return nil
end
-- anything of ours currently posing you gets out of the way
function Result.clearPoses()
if Hold.track then Hold.clear(false) end
for _, t in pairs(animTracks) do pcall(function() t:Stop(0.1) end) end
if cutterTrack and cutterTrack.IsPlaying then pcall(function() cutterTrack:Stop(0.1) end) end
end
function Result.showBanner(text, color)
if not Result.banner then return end
if not Result.gui or not Result.gui.Parent then
local sg = Instance.new("ScreenGui")
sg.Name = "FNFResult"
sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false
sg.DisplayOrder = F.silZ(170)
sg.Parent = PlayerGui
local l = Instance.new("TextLabel")
l.AnchorPoint = Vector2.new(0.5, 0.5)
l.Position = UDim2.new(0.5, 0, 0.32, 0)
l.Size = UDim2.new(1, 0, 0, 90)
l.BackgroundTransparency = 1
l.Font = Enum.Font.GothamBlack
l.TextSize = 72
l.TextStrokeTransparency = 0.2
l.TextTransparency = 1
l.Parent = sg
Result.gui, Result.bannerLbl = sg, l
end
local l = Result.bannerLbl
l.Text = text
l.TextColor3 = color
l.TextTransparency = 0
l.TextStrokeTransparency = 0.2
l.TextSize = 120
l.Rotation = math.random(-6, 6)
TweenService:Create(l, TweenInfo.new(0.35, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
{ TextSize = 72, Rotation = 0 }):Play()
task.delay(2, function()
if l.Text == text then
TweenService:Create(l, TweenInfo.new(0.5), { TextTransparency = 1, TextStrokeTransparency = 1 }):Play()
end
end)
end
function Result.win()
if not character or not animator then return end
Result.token = Result.token + 1
local tok = Result.token
Result.clearPoses()
if Result.winTrack then pcall(function() Result.winTrack:Stop(0.1) end) end
local t = Result.loadTrack(Result.cheerId, "cheer")
if not t then
F.notify("No cheer found — paste a cheer animation ID in the RESULT tab", "warn")
else
t.Looped = Result.cheerLoop
t:Play(0.1)
Result.winTrack = t
if Result.cheerLoop then
task.delay(Result.cheerTime, function()
if Result.token == tok and t.IsPlaying then t:Stop(0.3) end
end)
end
end
Result.showBanner("YOU WIN!", Color3.fromRGB(255, 215, 70))
FNFCamera:DoubleNoteZoom(0.8)
if Chaos.enabled then Chaos.hit(1.2) end
end
function Result.lose()
local char = character
local hum = char and char:FindFirstChildOfClass("Humanoid")
local root = char and char:FindFirstChild("HumanoidRootPart")
if not hum or not root or hum.Health <= 0 then return end
Result.token = Result.token + 1
local tok = Result.token
Result.clearPoses()
-- anything of ours that pins your body has to let go first
if Ride.enabled then Ride.unlock("lost the battle") end
root.Anchored = false
-- sitting pose
if Result.sitTrack then pcall(function() Result.sitTrack:Stop(0) end) end
local sit = Result.loadTrack(Result.sitId, "sit")
if sit then
sit.Looped = true
sit:Play(0.05)
Result.sitTrack = sit
end
-- hand the body to physics so the humanoid doesn't cancel the launch
hum.PlatformStand = true
-- which way you go flying
local dir
local oppRoot = currentTarget and currentTarget.Character
and currentTarget.Character:FindFirstChild("HumanoidRootPart")
if Result.direction == "AWAY" and oppRoot then
dir = root.Position - oppRoot.Position
elseif Result.direction == "RANDOM" then
local a = math.random() * math.pi * 2
dir = Vector3.new(math.cos(a), 0, math.sin(a))
else
dir = -root.CFrame.LookVector
end
dir = Vector3.new(dir.X, 0, dir.Z)
dir = (dir.Magnitude > 1e-3) and dir.Unit or -root.CFrame.LookVector
local vel = dir * Result.flingSpeed + Vector3.new(0, Result.lift, 0)
root.AssemblyLinearVelocity = vel
local axis = Vector3.new(math.random() - 0.5, math.random() - 0.5, math.random() - 0.5)
if axis.Magnitude < 1e-3 then axis = Vector3.new(1, 0, 0) end
root.AssemblyAngularVelocity = axis.Unit * Result.spin
-- keep pushing for a split second so ground friction can't eat the launch
local att = Instance.new("Attachment")
att.Parent = root
local lv = Instance.new("LinearVelocity")
lv.Attachment0 = att
lv.RelativeTo = Enum.ActuatorRelativeTo.World
lv.VectorVelocity = vel
lv.MaxForce = math.huge
lv.Parent = root
task.delay(0.12, function() lv:Destroy(); att:Destroy() end)
Result.showBanner("YOU LOSE", Color3.fromRGB(255, 70, 70))
if Chaos.enabled then Chaos.bigImpact(1.2) end
-- recover
task.delay(Result.getUpAfter, function()
if Result.token ~= tok then return end
if sit then pcall(function() sit:Stop(0.25) end) end
if hum.Parent and hum.Health > 0 then
hum.PlatformStand = false
pcall(function() hum:ChangeState(Enum.HumanoidStateType.GettingUp) end)
end
end)
end
-- ============================================================
-- NOTE PRESS / RELEASE
-- One path for every input: keyboard, touch arrows, anything
-- later. So every feature behaves the same however you play.
-- ============================================================
function F.notePress(row)
if cutterEnabled and (F.cutterSliceId(row.dir) or "") ~= "" then
F.cutterPlaySlice(row.dir)
else
if Hold.track then Hold.clear(false) end
for _, track in pairs(animTracks) do
if track.IsPlaying then track:Stop(0) end
end
local chosenId = F.rollBindingAnimId(row)
F.playAnim(row.key, chosenId, row.speedMult, row.holdLoop)
end
F.hitArrow(row.dir)
FNFCamera:RegisterHit("PLAYER")
FNFCamera:RegisterDirectionalHit(row.dir)
F.fightPlayerHit(row.dir)
Ghost.tryPerfect(row.dir)
F.swapFace(row.dir)
F.triggerRPMovement()
if spacing then spacing:OnSingStart(character) end
Notes.press(row.dir)
F.registerBpmTap()
if F.refreshBpmDetectorUI then F.refreshBpmDetectorUI() end
end
function F.noteRelease(row)
if cutterEnabled then
F.cutterStopSlice()
else
F.stopAnim(row.key)
end
F.releaseArrow(row.dir)
F.restoreFace()
if spacing then spacing:OnSingStop(character) end
Notes.release(row.dir)
end
function F.rowForDir(dir)
for _, row in ipairs(CurrentBindings) do
if row.dir == dir then return row end
end
return nil
end
-- ============================================================
-- MOBILE TOUCH ARROWS
-- FNF's mobile layout: two pairs along the bottom, ← ↓ on the
-- left and ↑ → on the right, with a gap in the middle for the
-- stage. Grey when idle, the note's colour when pressed. Every
-- finger is tracked on its own, so chords and holds work.
-- ============================================================
local STRUM_GREY = Color3.fromRGB(135, 163, 173)
function Mobile.shouldShow()
if Mobile.mode == "ON" then return true end
if Mobile.mode == "OFF" then return false end
return UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
end
function Mobile.layout()
if not Mobile.gui then return end
local cam = Workspace.CurrentCamera
local vs = cam and cam.ViewportSize or Vector2.new(1280, 720)
local W, H = vs.X, vs.Y
local s = math.clamp(math.min(W * 0.1, H * 0.2), 58, 150) * Mobile.size
local step = s + Mobile.pairGap
local cx = W / 2
local inner = W * Mobile.spread / 2
local xs = {
LEFT = cx - inner - step,
DOWN = cx - inner,
UP = cx + inner,
RIGHT = cx + inner + step,
}
for dir, b in pairs(Mobile.btns) do
b.frame.Size = UDim2.new(0, s, 0, s)
b.frame.Position = UDim2.new(0, xs[dir], 1, -Mobile.bottom - s / 2)
b.base = s
end
end
function Mobile.paint(dir, down)
local b = Mobile.btns[dir]
if not b then return end
local s = b.base or 80
if down then
b.glyph.TextColor3 = ARROW_COLORS[dir].hit
b.glyph.TextTransparency = 0
b.stroke.Color = Color3.fromRGB(255, 255, 255)
b.frame.Size = UDim2.new(0, s * 1.12, 0, s * 1.12)
TweenService:Create(b.frame, TweenInfo.new(0.12, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
{ Size = UDim2.new(0, s * 0.94, 0, s * 0.94) }):Play()
else
b.glyph.TextColor3 = STRUM_GREY
b.glyph.TextTransparency = Mobile.idleFade
b.stroke.Color = Color3.fromRGB(28, 32, 40)
TweenService:Create(b.frame, TweenInfo.new(0.14, Enum.EasingStyle.Quad),
{ Size = UDim2.new(0, s, 0, s) }):Play()
end
end
function Mobile.press(dir)
Mobile.held[dir] = (Mobile.held[dir] or 0) + 1
if Mobile.held[dir] > 1 then return end -- a second finger on the same arrow
Mobile.paint(dir, true)
local row = F.rowForDir(dir)
if row then F.notePress(row) end
end
function Mobile.release(dir)
Mobile.held[dir] = math.max(0, (Mobile.held[dir] or 0) - 1)
if Mobile.held[dir] > 0 then return end -- another finger is still on it
Mobile.paint(dir, false)
local row = F.rowForDir(dir)
if row then F.noteRelease(row) end
end
function Mobile.build()
local old = PlayerGui:FindFirstChild("FNFMobileNotes")
if old then old:Destroy() end
Mobile.btns, Mobile.touches, Mobile.held = {}, {}, {}
local sg = Instance.new("ScreenGui")
sg.Name = "FNFMobileNotes"
sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false
sg.DisplayOrder = F.silZ(90) -- under the main panel, over the game
sg.Parent = PlayerGui
Mobile.gui = sg
for _, dir in ipairs(ARROW_ORDER) do
-- the frame is the touch area; it's a little bigger than the arrow
local frame = Instance.new("Frame")
frame.AnchorPoint = Vector2.new(0.5, 0.5)
frame.BackgroundTransparency = 1
frame.Active = true
frame.Parent = sg
local glyph = Instance.new("TextLabel")
glyph.Size = UDim2.new(1, 0, 1, 0)
glyph.BackgroundTransparency = 1
glyph.Text = ARROW_SYMBOLS[dir]
glyph.TextColor3 = STRUM_GREY
glyph.TextTransparency = Mobile.idleFade
glyph.Font = Enum.Font.GothamBlack
glyph.TextScaled = true
glyph.Parent = frame
local stroke = Instance.new("UIStroke")
stroke.Thickness = 3
stroke.Color = Color3.fromRGB(28, 32, 40)
stroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Contextual
stroke.Parent = glyph
Mobile.btns[dir] = { frame = frame, glyph = glyph, stroke = stroke }
frame.InputBegan:Connect(function(input)
local t = input.UserInputType
if t ~= Enum.UserInputType.Touch and t ~= Enum.UserInputType.MouseButton1 then return end
if Mobile.touches[input] then return end
Mobile.touches[input] = dir
Mobile.press(dir)
end)
end
Mobile.layout()
end
-- A finger lifting anywhere counts, even if it slid off the arrow first,
-- so a note can never get stuck held down.
local function mobileEnded(input)
local dir = Mobile.touches[input]
if dir then
Mobile.touches[input] = nil
Mobile.release(dir)
end
end
UserInputService.InputEnded:Connect(mobileEnded)
UserInputService.TouchEnded:Connect(function(touch) mobileEnded(touch) end)
function Mobile.refresh()
local show = Mobile.shouldShow()
Mobile.active = show
if show then
if not Mobile.gui or not Mobile.gui.Parent then Mobile.build() end
Mobile.gui.Enabled = true
Mobile.layout()
elseif Mobile.gui then
-- let go of anything still held before hiding
for dir, n in pairs(Mobile.held) do
if n > 0 then Mobile.held[dir] = 1; Mobile.release(dir) end
end
Mobile.touches = {}
Mobile.gui.Enabled = false
end
if Arrows.container then
Arrows.container.Visible = not (show and Mobile.hideStrip)
end
if Mobile.refreshUI then Mobile.refreshUI() end
end
function Mobile.start()
Mobile.refresh()
local cam = Workspace.CurrentCamera
if cam then
cam:GetPropertyChangedSignal("ViewportSize"):Connect(Mobile.layout)
end
-- plugging in a keyboard (or unplugging it) re-checks AUTO
UserInputService:GetPropertyChangedSignal("KeyboardEnabled"):Connect(Mobile.refresh)
end
-- ============================================================
-- CHAOS CAMERA FX — unstable, aggressive, corrupted camera
--
-- Roblox exposes no post-process shader hooks, so true barrel
-- distortion / screen warping of the rendered frame is not
-- possible. Everything below approximates that look with the
-- tools that DO exist: violent FOV pumping, camera roll, dolly
-- along the view axis, ColorCorrection spikes, and screen-space
-- GUI overlays. All overlays are ZIndex-capped at 200.
-- ============================================================
function Chaos.build()
local old = PlayerGui:FindFirstChild("FNFChaosFX")
if old then old:Destroy() end
local sg = Instance.new("ScreenGui")
sg.Name = "FNFChaosFX"; sg.IgnoreGuiInset = true
sg.ResetOnSpawn = false; sg.DisplayOrder = F.silZ(160)
sg.Enabled = Chaos.enabled
sg.Parent = PlayerGui
Chaos.gui = sg
Chaos.layers = {}
-- chromatic / RGB split: three tinted sheets we slide apart
Chaos.layers.rgb = {}
local tints = { Color3.fromRGB(255,0,60), Color3.fromRGB(0,255,120), Color3.fromRGB(60,80,255) }
for i, c in ipairs(tints) do
local f = Instance.new("Frame")
f.Size = UDim2.new(1, 60, 1, 60)
f.Position = UDim2.new(0, -30, 0, -30)
f.BackgroundColor3 = c
f.BackgroundTransparency = 1
f.BorderSizePixel = 0
f.ZIndex = F.silZ(150 + i)
f.Parent = sg
Chaos.layers.rgb[i] = f
end
-- lens-squeeze vignette: four gradient edges that crush inward
Chaos.layers.vig = {}
local edges = {
{ UDim2.new(1,0,0,120), UDim2.new(0,0,0,0), Vector2.new(0,0), 90 },
{ UDim2.new(1,0,0,120), UDim2.new(0,0,1,0), Vector2.new(0,1), 270 },
{ UDim2.new(0,120,1,0), UDim2.new(0,0,0,0), Vector2.new(0,0), 0 },
{ UDim2.new(0,120,1,0), UDim2.new(1,0,0,0), Vector2.new(1,0), 180 },
}
for i, e in ipairs(edges) do
local f = Instance.new("Frame")
f.Size = e[1]; f.Position = e[2]; f.AnchorPoint = e[3]
f.BackgroundColor3 = Color3.new(0,0,0)
f.BackgroundTransparency = 1
f.BorderSizePixel = 0
f.ZIndex = F.silZ(160)
f.Parent = sg
local g = Instance.new("UIGradient")
g.Rotation = e[4]
g.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 1),
})
g.Parent = f
Chaos.layers.vig[i] = f
end
-- scanline sheet (only shown during glitch spikes)
local scan = Instance.new("Frame")
scan.Size = UDim2.new(1, 0, 1, 12); scan.BackgroundTransparency = 1
scan.BorderSizePixel = 0; scan.ClipsDescendants = true
scan.ZIndex = F.silZ(170); scan.Visible = false; scan.Parent = sg
for y = 0, 1300, 3 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.new(0,0,0)
line.BackgroundTransparency = 0.45
line.BorderSizePixel = 0; line.ZIndex = F.silZ(170); line.Parent = scan
end
Chaos.layers.scan = scan
-- torn-block container
local tear = Instance.new("Frame")
tear.Size = UDim2.new(1,0,1,0); tear.BackgroundTransparency = 1
tear.BorderSizePixel = 0; tear.ClipsDescendants = true
tear.ZIndex = F.silZ(180); tear.Parent = sg
Chaos.layers.tear = tear
-- flash sheet
local flash = Instance.new("Frame")
flash.Size = UDim2.new(1,0,1,0); flash.BackgroundColor3 = Color3.new(1,1,1)
flash.BackgroundTransparency = 1; flash.BorderSizePixel = 0
flash.ZIndex = F.silZ(190); flash.Parent = sg
Chaos.layers.flash = flash
-- colour grade driver for red/white spikes
local old2 = Lighting:FindFirstChild("FNFChaosCC")
if old2 then old2:Destroy() end
local cc = Instance.new("ColorCorrectionEffect")
cc.Name = "FNFChaosCC"
cc.Brightness = 0; cc.Contrast = 0; cc.Saturation = 0
cc.Parent = Lighting
Chaos.cc = cc
end
-- Short red/white blowout. Kept brief on purpose.
function Chaos.flashPulse(color, strength)
local f = Chaos.layers and Chaos.layers.flash
strength = (strength or 0.8) * Chaos.flash
if f then
f.BackgroundColor3 = color or Color3.new(1,1,1)
f.BackgroundTransparency = math.clamp(1 - strength, 0, 1)
TweenService:Create(f, TweenInfo.new(0.15, Enum.EasingStyle.Quint), { BackgroundTransparency = 1 }):Play()
end
Chaos.ccSpike = math.max(Chaos.ccSpike, strength)
Chaos.rgbSplit = math.max(Chaos.rgbSplit, strength * 0.9)
end
-- Frame tears into offset blocks and shifts sideways.
function Chaos.glitchBurst(dur)
if not Chaos.enabled then return end
local tear = Chaos.layers and Chaos.layers.tear
if not tear then return end
dur = (dur or 0.2) * Chaos.glitch
local wasRunning = os.clock() < Chaos.tearUntil
Chaos.tearUntil = math.max(Chaos.tearUntil, os.clock() + dur)
if wasRunning then return end
task.spawn(function()
while os.clock() < Chaos.tearUntil do
for _, c in ipairs(tear:GetChildren()) do c:Destroy() end
for i = 1, math.random(3, 7) do
local blk = Instance.new("Frame")
blk.Size = UDim2.new(1, math.random(30, 140), 0, math.random(6, 58))
blk.Position = UDim2.new(0, math.random(-100, 100), math.random(), 0)
if math.random() < 0.35 then
blk.BackgroundColor3 = Color3.fromRGB(math.random(180,255), math.random(0,80), math.random(120,255))
blk.BackgroundTransparency = 0.45 + math.random() * 0.3
else
blk.BackgroundColor3 = Color3.fromRGB(math.random(0,35), math.random(0,35), math.random(0,55))
blk.BackgroundTransparency = 0.3 + math.random() * 0.4
end
blk.BorderSizePixel = 0
blk.ZIndex = F.silZ(180)
blk.Parent = tear
end
-- hard lateral snap of the real frame, so the tear reads as motion
Chaos.kickX = Chaos.kickX + (math.random() - 0.5) * 3.6 * Chaos.glitch
Chaos.rgbSplit = math.max(Chaos.rgbSplit, 0.85)
task.wait(0.03 + math.random() * 0.04)
end
for _, c in ipairs(tear:GetChildren()) do c:Destroy() end
end)
end
-- Camera gets sucked toward the scene centre, then fires outward.
function Chaos.tunnelPull(dur)
if not Chaos.enabled then return end
Chaos.tunnelDur = dur or 0.85
Chaos.tunnelUntil = os.clock() + Chaos.tunnelDur
Chaos.tunnelFired = false
Chaos.glitchBurst(0.12)
end
-- Every note press routes through here. power ~0.3 (light) to ~2 (huge).
function Chaos.hit(power, dir)
if not Chaos.enabled then return end
power = (power or 1) * Chaos.master
local now = os.clock()
-- punch in hard; the spring overshoots past neutral then snaps back
Chaos.zoomVel = Chaos.zoomVel - 46 * power * Chaos.zoomPunch
Chaos.dollyVel = Chaos.dollyVel - 28 * power * Chaos.zoomPunch
local s = 2.5 * power * Chaos.shake
if dir == "LEFT" then
Chaos.kickX = Chaos.kickX - s; Chaos.rollVel = Chaos.rollVel + 7 * power
elseif dir == "RIGHT" then
Chaos.kickX = Chaos.kickX + s; Chaos.rollVel = Chaos.rollVel - 7 * power
elseif dir == "UP" then
Chaos.kickY = Chaos.kickY + s * 0.85; Chaos.rollVel = Chaos.rollVel + (math.random()-0.5) * 5
elseif dir == "DOWN" then
Chaos.kickY = Chaos.kickY - s * 0.85; Chaos.rollVel = Chaos.rollVel + (math.random()-0.5) * 5
else
Chaos.kickX = Chaos.kickX + (math.random() - 0.5) * 2 * s
end
Chaos.impact = math.min(Chaos.impact + 0.55 * power, 1.6)
Chaos.warpPhase = Chaos.warpPhase + 1.3 * power
-- two notes almost together = double-note blowout
if now - Chaos.lastHit < 0.07 then
Chaos.flashPulse(Color3.fromRGB(255, 70, 70), 1)
Chaos.glitchBurst(0.26)
Chaos.zoomVel = Chaos.zoomVel - 32
Chaos.rollVel = Chaos.rollVel + (math.random() < 0.5 and -1 or 1) * 17
elseif power >= 1.3 then
Chaos.glitchBurst(0.16 * power)
end
Chaos.lastHit = now
-- periodic tunnel transition on a long streak
Chaos.hitCount = Chaos.hitCount + 1
if Chaos.hitCount % 18 == 0 then Chaos.tunnelPull(0.8) end
end
-- Deliberate "major impact" beat: overshoot zoom, roll, flash, tear.
function Chaos.bigImpact(power)
if not Chaos.enabled then return end
power = power or 1.7
Chaos.zoomVel = Chaos.zoomVel - 70 * power * Chaos.zoomPunch
Chaos.dollyVel = Chaos.dollyVel - 46 * power * Chaos.zoomPunch
Chaos.rollVel = Chaos.rollVel + (math.random() < 0.5 and -1 or 1) * 22 * power
Chaos.kickX = Chaos.kickX + (math.random() - 0.5) * 6 * Chaos.shake
Chaos.kickY = Chaos.kickY + (math.random() - 0.5) * 4 * Chaos.shake
Chaos.impact = math.min(Chaos.impact + 1.2 * power, 2)
Chaos.flashPulse(Color3.fromRGB(255, 60, 60), 1.1)
Chaos.glitchBurst(0.32)
end
function Chaos.updateOverlays()
local L = Chaos.layers
if not L then return end
local split = Chaos.rgbSplit * Chaos.master
if L.rgb then
for i, f in ipairs(L.rgb) do
local d = (i == 1 and -1) or (i == 2 and 0.45) or 1
f.Position = UDim2.new(0, -30 + d * split * 28, 0, -30 + d * split * 8)
f.BackgroundTransparency = 1 - math.clamp(split * 0.22, 0, 0.3)
end
end
if L.vig then
local v = math.clamp(Chaos.impact * 0.5 * Chaos.warp, 0, 0.7)
local squeeze = 120 + Chaos.impact * 100
for i, f in ipairs(L.vig) do
f.BackgroundTransparency = 1 - v
if i <= 2 then f.Size = UDim2.new(1, 0, 0, squeeze)
else f.Size = UDim2.new(0, squeeze, 1, 0) end
end
end
if L.scan then
local on = (split > 0.25) or (Chaos.impact > 0.85)
L.scan.Visible = on
if on then L.scan.Position = UDim2.new(0, 0, 0, (os.clock() * 260) % 3 - 6) end
end
if Chaos.cc then
local s = Chaos.ccSpike
Chaos.cc.Brightness = s * 0.22
Chaos.cc.Contrast = s * 0.75 + Chaos.impact * 0.22
Chaos.cc.Saturation = s * 1.1 - Chaos.impact * 0.25
Chaos.cc.TintColor = Color3.new(1, 1 - s * 0.22, 1 - s * 0.22)
end
end
-- Called from CameraSystem:_update AFTER smoothing, so punches stay snappy.
function Chaos.apply(cf, fov, dt)
if not Chaos.enabled then return cf, fov end
dt = math.clamp(dt, 0, 1/20)
local now = os.clock()
-- springs
Chaos.zoomVel = Chaos.zoomVel + (-230 * Chaos.zoomPos - 16 * Chaos.zoomVel) * dt
Chaos.zoomPos = Chaos.zoomPos + Chaos.zoomVel * dt
Chaos.dollyVel = Chaos.dollyVel + (-155 * Chaos.dollyPos - 13 * Chaos.dollyVel) * dt
Chaos.dollyPos = Chaos.dollyPos + Chaos.dollyVel * dt
Chaos.rollVel = Chaos.rollVel + (-195 * Chaos.rollPos - 15 * Chaos.rollVel) * dt
Chaos.rollPos = Chaos.rollPos + Chaos.rollVel * dt
-- envelopes decay
local kd = 1 - math.exp(-11 * dt)
Chaos.kickX = Chaos.kickX - Chaos.kickX * kd
Chaos.kickY = Chaos.kickY - Chaos.kickY * kd
Chaos.impact = math.max(0, Chaos.impact - dt * 2.1)
Chaos.rgbSplit = math.max(0, Chaos.rgbSplit - dt * 3.4)
Chaos.ccSpike = math.max(0, Chaos.ccSpike - dt * 4.2)
-- irregular shake: two noise octaves plus random jitter at high impact
local amp = (0.3 + Chaos.impact * 1.55) * Chaos.shake * Chaos.master
local t = now * 27
local sx = (math.noise(t, Chaos.seed) + math.noise(t * 2.7, Chaos.seed + 11) * 0.5) * amp
local sy = (math.noise(t, Chaos.seed + 40) + math.noise(t * 3.1, Chaos.seed + 51) * 0.5) * amp * 0.8
local jitter = (Chaos.impact > 0.55) and (math.random() - 0.5) * amp * 0.9 or 0
-- readability clamp — the character never leaves frame
local maxOff = 3.4 * Chaos.readability
local offX = math.clamp(sx + Chaos.kickX + jitter, -maxOff, maxOff)
local offY = math.clamp(sy + Chaos.kickY, -maxOff, maxOff)
-- warp: FOV flex reads as lens breathing / barrel-ish pull
Chaos.warpPhase = Chaos.warpPhase + dt * (6 + Chaos.impact * 27)
local warp = math.sin(Chaos.warpPhase) * (1.4 + Chaos.impact * 9) * Chaos.warp
-- tunnel: pulled toward centre, then fired outward
local tunnelDolly, tunnelFov = 0, 0
if Chaos.tunnelUntil > now then
local a = 1 - ((Chaos.tunnelUntil - now) / math.max(Chaos.tunnelDur, 0.01))
local curve = math.sin(a * math.pi)
tunnelDolly = -curve * 9
tunnelFov = curve * 36 * Chaos.warp
Chaos.rgbSplit = math.max(Chaos.rgbSplit, curve * 0.55)
if a > 0.97 and not Chaos.tunnelFired then
Chaos.tunnelFired = true
Chaos.dollyVel = Chaos.dollyVel + 60
Chaos.flashPulse(Color3.new(1,1,1), 0.7)
end
end
-- rotation: sprung roll plus small pitch/yaw wobble
local roll = math.rad(Chaos.rollPos + math.noise(t * 0.6, Chaos.seed + 70) * 3.5 * Chaos.impact * Chaos.shake)
local pitch = math.rad(math.noise(t * 0.9, Chaos.seed + 90) * 2.2 * Chaos.impact * Chaos.shake)
local yaw = math.rad(math.noise(t * 0.8, Chaos.seed + 120) * 2.2 * Chaos.impact * Chaos.shake)
cf = cf * CFrame.new(offX, offY, Chaos.dollyPos + tunnelDolly) * CFrame.Angles(pitch, yaw, roll)
local fovOut = math.clamp(fov + Chaos.zoomPos + warp + tunnelFov, 18, 120)
Chaos.updateOverlays()
return cf, fovOut
end
function Chaos.setEnabled(state)
Chaos.enabled = state
if state then
if not Chaos.gui or not Chaos.gui.Parent then Chaos.build() end
Chaos.gui.Enabled = true
if not Chaos.beatThread then
Chaos.beatThread = task.spawn(function()
while Chaos.enabled do
task.wait(60 / math.max(40, StatusState.bpm or 100))
if Chaos.enabled and Chaos.beatSync then
Chaos.beatCount = (Chaos.beatCount + 1) % 4
Chaos.hit(Chaos.beatCount == 0 and 0.8 or 0.28)
end
end
Chaos.beatThread = nil
end)
end
else
if Chaos.gui then Chaos.gui.Enabled = false end
if Chaos.cc then
Chaos.cc.Brightness = 0; Chaos.cc.Contrast = 0
Chaos.cc.Saturation = 0; Chaos.cc.TintColor = Color3.new(1,1,1)
end
Chaos.zoomPos, Chaos.zoomVel = 0, 0
Chaos.dollyPos, Chaos.dollyVel = 0, 0
Chaos.rollPos, Chaos.rollVel = 0, 0
Chaos.kickX, Chaos.kickY = 0, 0
Chaos.impact, Chaos.rgbSplit, Chaos.ccSpike = 0, 0, 0
end
end
function Chaos.applyPreset(name)
local p = ({
Calm = { master=0.45, shake=0.5, zoomPunch=0.5, warp=0.3, glitch=0.2, flash=0.4, readability=1.2, beatSync=false },
Intense = { master=1, shake=1, zoomPunch=1, warp=0.8, glitch=0.7, flash=0.9, readability=1, beatSync=true },
Chaotic = { master=1.5, shake=1.5, zoomPunch=1.4, warp=1.4, glitch=1.3, flash=1.2, readability=0.8, beatSync=true },
Corrupted = { master=1.8, shake=1.7, zoomPunch=1.6, warp=1.8, glitch=2, flash=1.5, readability=0.7, beatSync=true },
})[name]
if not p then return end
for k, v in pairs(p) do Chaos[k] = v end
F.notify("Chaos preset: " .. name, "success")
end
-- ============================================================
-- INTRO TRANSITIONS — 5 FNF-style song intro effects
-- ============================================================
function F.playIntroTransition(name)
local sg = Instance.new("ScreenGui")
sg.Name = "FNFTransition"; sg.IgnoreGuiInset = true
sg.DisplayOrder = F.silZ(200); sg.ResetOnSpawn = false
sg.Parent = PlayerGui
if name == "COUNTDOWN" then
task.spawn(function()
local texts = { "3", "2", "1", "GO!" }
local colors = {
Color3.fromRGB(255, 80, 180),
Color3.fromRGB(0, 200, 255),
Color3.fromRGB(255, 200, 60),
Color3.fromRGB(80, 255, 120),
}
for i, txt in ipairs(texts) do
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.new(1, 0, 1, 0)
lbl.BackgroundTransparency = 1
lbl.Text = txt; lbl.TextColor3 = colors[i]
lbl.Font = Enum.Font.GothamBlack; lbl.TextSize = 120
lbl.TextTransparency = 0; lbl.TextStrokeTransparency = 0.5
lbl.TextStrokeColor3 = Color3.new(0, 0, 0)
lbl.ZIndex = F.silZ(190)
lbl.Parent = sg
lbl.TextSize = 40
TweenService:Create(lbl, TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
TextSize = 140
}):Play()
task.wait(0.45)
TweenService:Create(lbl, TweenInfo.new(0.15), { TextTransparency = 1, TextStrokeTransparency = 1 }):Play()
task.wait(0.2)
lbl:Destroy()
end
task.wait(0.3); sg:Destroy()
end)
elseif name == "WIPE" then
task.spawn(function()
local wipe = Instance.new("Frame")
wipe.Size = UDim2.new(3, 0, 3, 0)
wipe.Position = UDim2.new(-1, 0, -1, 0)
wipe.Rotation = -30
wipe.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
wipe.BorderSizePixel = 0; wipe.ZIndex = F.silZ(190); wipe.Parent = sg
local stripe = Instance.new("Frame")
stripe.Size = UDim2.new(1, 0, 0, 8)
stripe.Position = UDim2.new(0, 0, 1, 0); stripe.AnchorPoint = Vector2.new(0, 1)
stripe.BackgroundColor3 = Color3.fromRGB(255, 80, 180)
stripe.BorderSizePixel = 0; stripe.ZIndex = F.silZ(191); stripe.Parent = wipe
task.wait(0.3)
TweenService:Create(wipe, TweenInfo.new(0.7, Enum.EasingStyle.Quint, Enum.EasingDirection.In), {
Position = UDim2.new(2, 0, -1, 0)
}):Play()
task.wait(0.8); sg:Destroy()
end)
elseif name == "PIXELATE" then
task.spawn(function()
local GRID = 12
local cells = {}
for gx = 0, GRID - 1 do
for gy = 0, GRID - 1 do
local cell = Instance.new("Frame")
cell.Size = UDim2.new(1/GRID, 2, 1/GRID, 2)
cell.Position = UDim2.new(gx/GRID, 0, gy/GRID, 0)
cell.BackgroundColor3 = Color3.fromRGB(math.random(0, 20), math.random(0, 10), math.random(10, 30))
cell.BorderSizePixel = 0; cell.ZIndex = F.silZ(190); cell.Parent = sg
table.insert(cells, cell)
end
end
task.wait(0.5)
local order = {}
for i = 1, #cells do order[i] = i end
for i = #order, 2, -1 do
local j = math.random(1, i)
order[i], order[j] = order[j], order[i]
end
for idx, ci in ipairs(order) do
local cell = cells[ci]
if cell then
TweenService:Create(cell, TweenInfo.new(0.2), { BackgroundTransparency = 1 }):Play()
end
if idx % 6 == 0 then task.wait(0.03) end
end
task.wait(0.4); sg:Destroy()
end)
elseif name == "FLASH" then
task.spawn(function()
local flash = Instance.new("Frame")
flash.Size = UDim2.new(1, 0, 1, 0)
flash.BackgroundColor3 = Color3.new(1, 1, 1)
flash.BackgroundTransparency = 0
flash.BorderSizePixel = 0; flash.ZIndex = F.silZ(190); flash.Parent = sg
local title = Instance.new("TextLabel")
title.Size = UDim2.new(1, 0, 0, 60); title.Position = UDim2.new(0, 0, 0.4, 0)
title.BackgroundTransparency = 1; title.Text = "🎵 READY?"
title.TextColor3 = Color3.fromRGB(255, 80, 180)
title.Font = Enum.Font.GothamBlack; title.TextSize = 50
title.TextTransparency = 1; title.ZIndex = F.silZ(191); title.Parent = flash
task.wait(0.1)
TweenService:Create(title, TweenInfo.new(0.2), { TextTransparency = 0 }):Play()
task.wait(0.8)
TweenService:Create(flash, TweenInfo.new(0.05), { BackgroundTransparency = 0 }):Play()
task.wait(0.1)
TweenService:Create(flash, TweenInfo.new(0.5, Enum.EasingStyle.Quint), { BackgroundTransparency = 1 }):Play()
TweenService:Create(title, TweenInfo.new(0.3), { TextTransparency = 1 }):Play()
task.wait(0.6); sg:Destroy()
end)
elseif name == "SPLIT" then
task.spawn(function()
local topHalf = Instance.new("Frame")
topHalf.Size = UDim2.new(1, 0, 0.5, 0); topHalf.Position = UDim2.new(0, 0, 0, 0)
topHalf.BackgroundColor3 = Color3.fromRGB(10, 5, 20); topHalf.BorderSizePixel = 0
topHalf.ZIndex = F.silZ(190); topHalf.Parent = sg
local topLine = Instance.new("Frame")
topLine.Size = UDim2.new(1, 0, 0, 4); topLine.Position = UDim2.new(0, 0, 1, -4)
topLine.BackgroundColor3 = Color3.fromRGB(255, 80, 180); topLine.BorderSizePixel = 0
topLine.ZIndex = F.silZ(191); topLine.Parent = topHalf
local botHalf = Instance.new("Frame")
botHalf.Size = UDim2.new(1, 0, 0.5, 0); botHalf.Position = UDim2.new(0, 0, 0.5, 0)
botHalf.BackgroundColor3 = Color3.fromRGB(10, 5, 20); botHalf.BorderSizePixel = 0
botHalf.ZIndex = F.silZ(190); botHalf.Parent = sg
local botLine = Instance.new("Frame")
botLine.Size = UDim2.new(1, 0, 0, 4); botLine.Position = UDim2.new(0, 0, 0, 0)
botLine.BackgroundColor3 = Color3.fromRGB(0, 200, 255); botLine.BorderSizePixel = 0
botLine.ZIndex = F.silZ(191); botLine.Parent = botHalf
local vs = Instance.new("TextLabel")
vs.Size = UDim2.new(1, 0, 1, 0); vs.BackgroundTransparency = 1
vs.Text = "VS"; vs.TextColor3 = Color3.fromRGB(255, 255, 255)
vs.Font = Enum.Font.GothamBlack; vs.TextSize = 80
vs.TextStrokeTransparency = 0.3; vs.TextStrokeColor3 = Color3.fromRGB(255, 80, 180)
vs.ZIndex = F.silZ(192); vs.Parent = sg
task.wait(0.8)
TweenService:Create(topHalf, TweenInfo.new(0.5, Enum.EasingStyle.Quint, Enum.EasingDirection.In), {
Position = UDim2.new(0, 0, -0.5, 0)
}):Play()
TweenService:Create(botHalf, TweenInfo.new(0.5, Enum.EasingStyle.Quint, Enum.EasingDirection.In), {
Position = UDim2.new(0, 0, 1, 0)
}):Play()
TweenService:Create(vs, TweenInfo.new(0.3), { TextTransparency = 1, TextStrokeTransparency = 1 }):Play()
task.wait(0.6); sg:Destroy()
end)
end
end
-- ============================================================
-- MATCH SEARCH — find other players running this injector
-- ============================================================
local SCRIPT_TAG_ATTR = "FNFInjectorLoaded"
local SCRIPT_VERSION_ATTR = "FNFInjectorVersion"
INJECTOR_VERSION = "6.2"
local MATCH_REMOTE_NAME = "FNFMatchRemote"
local matchRemote = nil
local matchResultsCb = nil
function F.setupMatchRemote()
local ok, remote = pcall(function()
return ReplicatedStorage:WaitForChild(MATCH_REMOTE_NAME, 6)
end)
if ok and remote then
matchRemote = remote
matchRemote.OnClientEvent:Connect(function(kind, payload)
if kind == "list" and matchResultsCb then
matchResultsCb(payload)
end
end)
end
return matchRemote ~= nil
end
function F.broadcastScriptPresence()
pcall(function()
LocalPlayer:SetAttribute(SCRIPT_TAG_ATTR, true)
LocalPlayer:SetAttribute(SCRIPT_VERSION_ATTR, INJECTOR_VERSION)
end)
if matchRemote then
matchRemote:FireServer("announce", INJECTOR_VERSION)
end
end
function F.searchForMatches(query, onResults)
matchResultsCb = onResults
if matchRemote then
matchRemote:FireServer("list", query or "")
return true
end
local out = {}
local q = (query or ""):lower()
for _, plr in ipairs(Players:GetPlayers()) do
if plr ~= LocalPlayer and plr:GetAttribute(SCRIPT_TAG_ATTR) then
if q == "" or plr.Name:lower():find(q, 1, true) then
table.insert(out, { Name = plr.Name })
end
end
end
if onResults then onResults(out) end
return false
end
-- ============================================================
-- ANIMATOR HELPERS
-- ============================================================
function F.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
-- Tracks are cached per animation, not per key, so cycling between several
-- animations on one note never destroys and reloads anything.
function F.getTrack(assetId, looped)
if not animator or not assetId or assetId == "" then return nil end
if Pose.owner ~= animator then
Pose.cache = {} -- new character, new Animator: old tracks are dead
Pose.owner = animator
end
local ck = assetId .. (looped and ":L" or ":O")
local t = Pose.cache[ck]
if t then return t end
local ok, res = pcall(function()
local anim = Instance.new("Animation")
anim.AnimationId = "rbxassetid://" .. assetId
local tr = animator:LoadAnimation(anim)
tr.Priority = PRIORITY
tr.Looped = looped and true or false
return tr
end)
if ok and res then Pose.cache[ck] = res; return res end
warn("[FNF] Load fail: " .. tostring(assetId))
return nil
end
function F.playDodge()
if Pose.dodgeId == "" or not animator then return end
local t = F.getTrack(Pose.dodgeId, false)
if not t then return end
t.Priority = Enum.AnimationPriority.Action2 -- plays over a held pose
if Hold.track then Hold.clear(false) end
t:Play(0.05)
t:AdjustSpeed(Pose.dodgeSpeed)
end
-- Load and download every bind's animations ahead of time, so the very
-- first press of each one is as fast as the hundredth.
function F.preloadBinds()
if not animator then return end
local anims = {}
local function add(id, looped)
if id and id ~= "" then
local t = F.getTrack(id, looped)
if t and t.Animation then table.insert(anims, t.Animation) end
end
end
for _, row in ipairs(CurrentBindings) do
add(row.id, row.holdLoop)
for _, a in ipairs(row.alts or {}) do add(a.id, row.holdLoop) end
end
add(Pose.dodgeId, false)
task.spawn(function()
pcall(function() game:GetService("ContentProvider"):PreloadAsync(anims) end)
end)
end
-- Tracks now live in a shared cache, so a key releasing its slot only
-- stops the track; destroying it would break every other key using it.
function F.destroyTrack(key)
local t = animTracks[key]
if t then pcall(function() t:Stop(0) end) animTracks[key] = nil end
end
function F.destroyIdleTrack()
if idleTrack then pcall(function() idleTrack:Stop(0) end) pcall(function() idleTrack:Destroy() end) idleTrack = nil end
idleLooping = false
end
function F.destroySeqTrack()
if seqTrack then pcall(function() seqTrack:Stop(0) end) pcall(function() seqTrack:Destroy() end) seqTrack = nil end
end
function F.cleanupAll()
idleLooping = false; seqPlaying = false
F.destroyIdleTrack(); F.destroySeqTrack()
for k in pairs(animTracks) do F.destroyTrack(k) end
for _, c in ipairs(connections) do pcall(function() c:Disconnect() end) end
connections = {}; animator = nil; character = nil
faceDecal = nil; defaultFaceId = nil
-- cutter tracks belonged to the animator that just went away
cutterTrack = nil
Pose.cache = {}
Pose.owner = nil
Cut.tracks = {}
Cut.trackOwner = nil
end
-- ============================================================
-- NOTIFICATION TOASTS
-- ============================================================
local notifyContainer = nil
F.notify = function(text, kind)
if not notifyContainer then F.log("[toast] "..text); return end
kind = kind or "info"
local colors = {
info = Color3.fromRGB(80, 200, 255),
success = Color3.fromRGB(80, 220, 140),
warn = Color3.fromRGB(255, 190, 80),
error = Color3.fromRGB(255, 90, 90),
}
local toast = Instance.new("Frame")
toast.Size = UDim2.new(1, 0, 0, 38)
toast.BackgroundColor3 = Color3.fromRGB(30, 30, 45)
toast.BackgroundTransparency = 0.25
toast.BorderSizePixel = 0
toast.ClipsDescendants = true
toast.Parent = notifyContainer
local corner = Instance.new("UICorner"); corner.CornerRadius = UDim.new(0,12); corner.Parent = toast
local stroke = Instance.new("UIStroke"); stroke.Color = colors[kind] or colors.info; stroke.Thickness = 1; stroke.Transparency = 0.4; stroke.Parent = toast
local tGrad = Instance.new("UIGradient")
tGrad.Color = ColorSequence.new(Color3.fromRGB(255,255,255), Color3.fromRGB(180,180,200))
tGrad.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.88),
NumberSequenceKeypoint.new(0.4, 0.95),
NumberSequenceKeypoint.new(1, 0.92),
})
tGrad.Rotation = 90; tGrad.Parent = toast
local dot = Instance.new("Frame")
dot.Size = UDim2.new(0, 6, 0, 6); dot.Position = UDim2.new(0, 10, 0.5, -3)
dot.BackgroundColor3 = colors[kind] or colors.info
dot.BorderSizePixel = 0; dot.Parent = toast
local dotCorner = Instance.new("UICorner"); dotCorner.CornerRadius = UDim.new(1,0); dotCorner.Parent = dot
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.new(1, -28, 1, 0); lbl.Position = UDim2.new(0, 22, 0, 0)
lbl.BackgroundTransparency = 1; lbl.Text = text
lbl.TextColor3 = Color3.fromRGB(240,240,250); lbl.Font = Enum.Font.GothamMedium
lbl.TextSize = 11; 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.3, 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.3, Enum.EasingStyle.Quint, Enum.EasingDirection.In), { Position = UDim2.new(1, 20, 0, 0), BackgroundTransparency = 1 })
TweenService:Create(lbl, TweenInfo.new(0.25), { TextTransparency = 1 }):Play()
TweenService:Create(stroke, TweenInfo.new(0.25), { Transparency = 1 }):Play()
TweenService:Create(dot, TweenInfo.new(0.25), { BackgroundTransparency = 1 }):Play()
t:Play()
t.Completed:Connect(function() if toast then toast:Destroy() end end)
end)
end
-- ============================================================
-- THEME SYSTEM
-- ============================================================
function F.regStroke(obj) table.insert(themedStrokes, obj); obj.Color = THEMES[currentThemeName].accent end
function F.regBg(obj) table.insert(themedBgs, obj); obj.BackgroundColor3 = THEMES[currentThemeName].accent end
function F.regText(obj) table.insert(themedTexts, obj); obj.TextColor3 = THEMES[currentThemeName].accent end
function F.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
function F.setTheme(name)
if not THEMES[name] then return end
currentThemeName = name
rgbCycleOn = (name == "RGB")
if not rgbCycleOn then
F.applyThemeColors(THEMES[name].accent)
end
-- Mono goes hard black with white hairlines, like the stage overlay
local mono = (name == "Mono")
local gui = PlayerGui:FindFirstChild("FNFInjectorGui")
local main = gui and gui:FindFirstChild("Main")
if main then
main.BackgroundColor3 = mono and Color3.new(0, 0, 0) or Color3.fromRGB(13, 12, 19)
main.BackgroundTransparency = mono and 0.02 or 0.04
for _, d in ipairs(main:GetDescendants()) do
if d:IsA("UIGradient") and d.Parent == main then
d.Enabled = not mono
elseif d:IsA("UIStroke") then
d.Thickness = mono and 1.6 or 1
end
end
end
F.notify("Theme set to "..name, "info")
end
task.spawn(function()
local hue = 0
while true do
task.wait(0.05)
if rgbCycleOn then
hue = (hue + 0.006) % 1
F.applyThemeColors(Color3.fromHSV(hue, 0.85, 1))
end
end
end)
-- ============================================================
-- FACE SYSTEM
-- ============================================================
function F.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
F.log("Face found: "..defaultFaceId)
end
function F.swapFace(dir)
if not faceDecal then return end
local id = faceIds[dir]
if id and id ~= "" then
faceDecal.Texture = "rbxassetid://"..id
end
end
function F.restoreFace()
if faceDecal and defaultFaceId then
faceDecal.Texture = defaultFaceId
end
end
-- ============================================================
-- LEGS ON GROUND
-- ============================================================
function F.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
-- ============================================================
function F.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 F.refreshRecentsUI then F.refreshRecentsUI() end
end
-- ============================================================
-- LIVE PREVIEW HELPERS
-- ============================================================
function F.updatePreviewAnimation(assetId, label)
StatusState.currentAnimName = label or assetId
StatusState.currentAnimId = assetId
if F.refreshStatusUI then F.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.Looped = true -- hold the pose instead of ending after one cycle
result:Play(0.05)
end
end
-- ============================================================
-- RP TUG-OF-WAR LOGIC
-- ============================================================
function F.stopWatchingTargetAnims()
if targetAnimConn then targetAnimConn:Disconnect(); targetAnimConn = nil end
if targetCharAddedConn then targetCharAddedConn:Disconnect(); targetCharAddedConn = nil end
end
function F.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
local awayVector = (myRoot.Position - targetRoot.Position)
local distance = awayVector.Magnitude
if distance <= 0.001 then return end
if distance < rpMaxDistance then
local actualStep = math.min(rpRetreatStep, rpMaxDistance - distance)
myRoot.CFrame = myRoot.CFrame + (awayVector.Unit * actualStep)
end
end
function F.startWatchingTargetAnims()
F.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
targetAnimConn = anr.AnimationPlayed:Connect(function(track)
local pri = track.Priority
if pri == Enum.AnimationPriority.Idle
or pri == Enum.AnimationPriority.Core
or pri == Enum.AnimationPriority.Movement then
return
end
if Split.enabled and track.Animation then
Split.play("OPPONENT", (track.Animation.AnimationId or ""):match("%d+") or "")
end
FNFCamera:RegisterHit("OPPONENT")
F.fightOpponentHit()
if rpModeEnabled then F.triggerRetreat() end
end)
end
if currentTarget.Character then bindAnimator(currentTarget.Character) end
targetCharAddedConn = currentTarget.CharacterAdded:Connect(function(newChar)
F.stopWatchingTargetAnims()
bindAnimator(newChar)
end)
end
function F.setTargetPlayer(partialName)
if not partialName or partialName == "" then
currentTarget = nil
F.stopWatchingTargetAnims()
F.notify("RP Target cleared.", "info")
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
F.startWatchingTargetAnims()
F.notify("Target locked: " .. player.Name, "success")
return player
end
end
F.notify("Player not found.", "warn")
return nil
end
function F.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
if distance > rpMinDistance then
local actualStep = math.min(rpMoveStep, distance - rpMinDistance)
myRoot.CFrame = myRoot.CFrame + (direction.Unit * actualStep)
end
end
-- ============================================================
-- PICK OPPONENT (wires RP + spacing + camera in one call)
-- ============================================================
function F.pickOpponent(player)
if not player then return end
F.setTargetPlayer(player.Name)
if spacing then spacing:SetOpponent(player.Character) end
FNFCamera:SetOpponentCharacter(player.Character)
local function rebindOnRespawn(newChar)
if spacing then spacing:SetOpponent(newChar) end
FNFCamera:SetOpponentCharacter(newChar)
end
player.CharacterAdded:Connect(rebindOnRespawn)
if Split.enabled then Split.refreshClones() end
end
-- ============================================================
-- PROXIMITY BLOCK — invisible wall between you and opponent
-- ============================================================
F.startProximityBlock = function()
if proximityBlockConn then proximityBlockConn:Disconnect(); proximityBlockConn = nil end
proximityBlockConn = RunService.Heartbeat:Connect(function()
if not proximityBlockEnabled or not currentTarget then return end
if not character or not currentTarget.Character then return end
local myRoot = character:FindFirstChild("HumanoidRootPart")
local oppRoot = currentTarget.Character:FindFirstChild("HumanoidRootPart")
if not myRoot or not oppRoot then return end
local diff = myRoot.Position - oppRoot.Position
local flatDiff = Vector3.new(diff.X, 0, diff.Z)
local dist = flatDiff.Magnitude
if dist < proximityBlockDist and dist > 0.001 then
local pushOut = (proximityBlockDist - dist)
myRoot.CFrame = myRoot.CFrame + (flatDiff.Unit * pushOut)
end
end)
end
function F.stopProximityBlock()
if proximityBlockConn then proximityBlockConn:Disconnect(); proximityBlockConn = nil end
end
-- ============================================================
-- FIGHTING SYSTEM — momentum pushback + double notes
-- ============================================================
local FS = fightState
function F.getFightRoots()
if not character or not currentTarget or not currentTarget.Character then return nil, nil end
return character:FindFirstChild("HumanoidRootPart"),
currentTarget.Character:FindFirstChild("HumanoidRootPart")
end
function F.fightPush(whoRoot, awayFromRoot, studs)
if not whoRoot or not awayFromRoot then return end
local diff = whoRoot.Position - awayFromRoot.Position
local flat = Vector3.new(diff.X, 0, diff.Z)
if flat.Magnitude < 0.001 then flat = Vector3.new(0, 0, 1) end
local push = math.min(studs, FS.momentumCap)
whoRoot.CFrame = whoRoot.CFrame + (flat.Unit * push)
end
F.deactivateFight = function()
if not FS.active then return end
FS.active = false
FS.playerMomentum = 0
FS.opponentMomentum = 0
if F.refreshFightUI then F.refreshFightUI() end
end
function F.activateFight()
if FS.active then return end
FS.active = true
if F.refreshFightUI then F.refreshFightUI() end
end
F.startFightLoop = function()
if fightShakeConn then fightShakeConn:Disconnect() end
fightShakeConn = RunService.Heartbeat:Connect(function(dt)
if not fightModeEnabled or not FS.active then return end
local now = os.clock()
FS.playerMomentum = FS.playerMomentum * FS.momentumDecay
FS.opponentMomentum = FS.opponentMomentum * FS.momentumDecay
if FS.playerMomentum < 0.01 then FS.playerMomentum = 0 end
if FS.opponentMomentum < 0.01 then FS.opponentMomentum = 0 end
local lastAny = math.max(FS.lastPlayerHit, FS.lastOpponentHit)
if now - lastAny > FS.idleTimeout then
F.deactivateFight()
return
end
local totalMomentum = FS.playerMomentum + FS.opponentMomentum
local fightIntensity = math.clamp(totalMomentum / (FS.momentumCap * 0.6), 0, 1)
if fightIntensity > FNFCamera.targetIntensity then
FNFCamera.targetIntensity = fightIntensity
end
for i = #FS.playerHitTimes, 1, -1 do
if now - FS.playerHitTimes[i] > FS.streakWindow then table.remove(FS.playerHitTimes, i) end
end
for i = #FS.opponentHitTimes, 1, -1 do
if now - FS.opponentHitTimes[i] > FS.streakWindow then table.remove(FS.opponentHitTimes, i) end
end
if F.refreshFightUI then F.refreshFightUI() end
end)
end
F.fightPlayerHit = function(dir)
if not fightModeEnabled or not currentTarget then return end
local now = os.clock()
if not FS.active then F.activateFight() end
FS.lastPlayerHit = now
table.insert(FS.playerHitTimes, now)
local isDouble = false
if now - FS.lastPlayerKeyTime < FS.doubleNoteWindow and #FS.lastPlayerKeys > 0 then
local prevDir = FS.lastPlayerKeys[#FS.lastPlayerKeys]
if prevDir ~= dir then
isDouble = true
end
end
table.insert(FS.lastPlayerKeys, dir)
FS.lastPlayerKeyTime = now
while #FS.lastPlayerKeys > 4 do table.remove(FS.lastPlayerKeys, 1) end
local streakCount = #FS.playerHitTimes
local streakMult = 1 + math.min(streakCount, 10) * 0.15
local pushAmount
if isDouble then
pushAmount = FS.pushPerDouble * streakMult
FNFCamera:DoubleNoteZoom(1)
F.notify("DOUBLE NOTE", "warn")
else
pushAmount = FS.pushPerHit * streakMult
end
FS.playerMomentum = math.min(FS.playerMomentum + pushAmount, FS.momentumCap)
local myRoot, oppRoot = F.getFightRoots()
F.fightPush(oppRoot, myRoot, pushAmount)
end
F.fightOpponentHit = function()
if not fightModeEnabled or not currentTarget then return end
local now = os.clock()
if not FS.active then F.activateFight() end
FS.lastOpponentHit = now
table.insert(FS.opponentHitTimes, now)
local streakCount = #FS.opponentHitTimes
local streakMult = 1 + math.min(streakCount, 10) * 0.15
local pushAmount = FS.pushPerHit * streakMult
FS.opponentMomentum = math.min(FS.opponentMomentum + pushAmount, FS.momentumCap)
local myRoot, oppRoot = F.getFightRoots()
F.fightPush(myRoot, oppRoot, pushAmount)
end
-- ============================================================
-- DIRECTIONAL ANIMATION PLAYBACK
-- ============================================================
function F.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 = F.getTrack(assetId, wantLooped)
if not track then return end
animTracks[key] = track
if wantLooped then
track:Play(BLEND_IN)
track:AdjustSpeed(speedMult)
else
local reach = math.max(Pose.reach, 0.005) / speedMult
-- stamp this press; a delayed freeze from an earlier press on the
-- same key checks the stamp and leaves the new press alone
F.animGen = F.animGen or {}
F.animGen[key] = (F.animGen[key] or 0) + 1
local gen = F.animGen[key]
track:Play(BLEND_IN)
-- snap: straight to the pose this frame, if the clip has loaded
if Pose.snap and track.Length > 0 then
track:AdjustSpeed(0)
track.TimePosition = math.max(0, track.Length - 0.01)
else
track.TimePosition = 0
track:AdjustSpeed(track.Length > 0 and (track.Length / reach) or 10)
end
task.delay(reach, function()
if F.animGen[key] == gen and 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 F.refreshStatusUI then F.refreshStatusUI() end
F.updatePreviewAnimation(assetId, key)
Split.play("PLAYER", assetId)
F.pushRecent(assetId, key)
end
function F.stopAnim(key)
local t = animTracks[key]
if t and t.IsPlaying then
if Hold.qualifies(t) then
-- don't stop it: playAnim already freezes it on the final frame,
-- so it just stays posed until the hold runs out or is interrupted
Hold.start(t, key)
if F.refreshStatusUI then F.refreshStatusUI() end
return
end
t:Stop(BLEND_OUT)
end
StatusState.playing = false
if F.refreshStatusUI then F.refreshStatusUI() end
end
-- Builds the pool for a note: the main ID plus every filled-in extra.
function F.animPool(row)
local pool = {}
if row.id and row.id ~= "" then table.insert(pool, row.id) end
for _, a in ipairs(row.alts or {}) do
if a.id and a.id ~= "" then table.insert(pool, a.id) end
end
return pool
end
-- Picks the animation this press plays.
-- CYCLE — goes through them in order, one per press
-- RANDOM — any of them, evenly
-- WEIGHTED — each extra has a % chance; the leftover % plays the main ID
function F.rollBindingAnimId(row)
local pool = F.animPool(row)
if #pool == 0 then return row.id end
if #pool == 1 then return pool[1] end
local mode = row.mode or "CYCLE"
if mode == "RANDOM" then
return pool[math.random(1, #pool)]
elseif mode == "WEIGHTED" then
local roll = math.random() * 100
local acc = 0
for _, alt in ipairs(row.alts) do
acc += (alt.chance or 0)
if roll <= acc and alt.id ~= "" then return alt.id end
end
return row.id
end
-- CYCLE
row.cycleIdx = ((row.cycleIdx or 0) % #pool) + 1
return pool[row.cycleIdx]
end
-- ============================================================
-- EMOTE CUTTER PLAYBACK
-- ============================================================
-- The animation a note actually plays: its own ID, or the default.
function F.cutterSliceId(dir)
local s = cutterSlices[dir]
if s and type(s.id) == "string" and s.id ~= "" then return s.id end
return cutterAnimId
end
-- One gameplay track per animation, cached, so jumping between notes that
-- use different animations never reloads anything mid-song.
function F.cutterTrackFor(id)
if not animator or not id or id == "" then return nil end
-- Resetting destroys the old Animator. Tracks loaded on it still exist as
-- objects and accept Play() without error, they just never animate. So
-- whenever the Animator changes, every cached track is thrown away.
if Cut.trackOwner ~= animator then
Cut.tracks = {}
Cut.trackOwner = animator
cutterTrack = nil
end
local t = Cut.tracks[id]
if t then return t end
local ok, res = pcall(function()
local a = Instance.new("Animation")
a.AnimationId = "rbxassetid://" .. id
local tr = animator:LoadAnimation(a)
tr.Priority = PRIORITY
tr.Looped = true
return tr
end)
if ok and res then Cut.tracks[id] = res; return res end
return nil
end
function F.cutterLoadTrack()
for _, dir in ipairs(ARROW_ORDER) do F.cutterTrackFor(F.cutterSliceId(dir)) end
cutterTrack = F.cutterTrackFor(cutterAnimId) or cutterTrack
return cutterTrack
end
-- Walk the slice back to its first frame, then hold it there.
-- Stepped by hand rather than with a negative speed, so the timing is
-- exact and it can't overshoot past the start of the clip.
function F.cutterReverse(track, gen)
if not track or not track.IsPlaying then return end
local from = track.TimePosition
local to = Cut.sliceStart
local dur = math.max(Cut.returnTime, 0.01)
track:AdjustSpeed(0)
task.spawn(function()
local t0 = os.clock()
while gen == Cut.sliceGen and track.IsPlaying do
local k = math.clamp((os.clock() - t0) / dur, 0, 1)
k = k * k * (3 - 2 * k) -- ease in and out
track.TimePosition = from + (to - from) * k
if k >= 1 then break end
RunService.RenderStepped:Wait()
end
if gen ~= Cut.sliceGen or not track.IsPlaying then return end
-- back at the original pose: pin it exactly on that frame
track:AdjustSpeed(0)
track.TimePosition = to
if Cut.holdAfterReturn then
-- Pose Hold decides how long (its timer, or until the next note).
-- With Pose Hold off it simply stays here until you play again.
if Hold.enabled and Hold.cutter then Hold.start(track, "slice") end
else
if Hold.track == track then Hold.detach() end
track:Stop(BLEND_OUT)
end
end)
end
function F.cutterPlaySlice(dir)
if not animator then return end
local slice = cutterSlices[dir]
if not slice then return end
local id = F.cutterSliceId(dir)
if not id or id == "" then return end
local tr = F.cutterTrackFor(id)
if not tr then return end
-- moving to a slice from a DIFFERENT animation: retire the old track fast
if cutterTrack and cutterTrack ~= tr and cutterTrack.IsPlaying then
if Hold.track == cutterTrack then Hold.clear(false)
else pcall(function() cutterTrack:Stop(0.05) end) end
end
-- same track: hand the held pose straight over, no gap back to idle
if Hold.track then
if Hold.track == tr then Hold.detach() else Hold.clear(false) end
end
cutterTrack = tr
-- every press gets its own generation; any watcher from an earlier press
-- sees the number change and quits, so it can't freeze THIS slice
Cut.sliceGen = Cut.sliceGen + 1
local gen = Cut.sliceGen
local a, b = slice.start, slice.stop
if b < a then a, b = b, a end
local span = b - a
Cut.sliceStart = math.max(0, a)
-- already playing: don't call Play again, which restarts and hitches.
-- Just move the playhead.
if not cutterTrack.IsPlaying then cutterTrack:Play(0) end
cutterTrack.TimePosition = math.max(0, a)
if Cut.snap or span <= 0.001 then
-- instant: land on the pose this frame
cutterTrack:AdjustSpeed(0)
cutterTrack.TimePosition = math.max(0, b)
if Cut.reverse == "AUTO" then
local track = cutterTrack
task.delay(Cut.returnPause, function()
if gen == Cut.sliceGen then F.cutterReverse(track, gen) end
end)
end
return
end
-- speed the slice up so it always lands in `reach` seconds, whatever
-- its real length, exactly like normal binds do
cutterTrack:AdjustSpeed(span / math.max(Cut.reach, 0.01))
local track = cutterTrack
task.spawn(function()
while gen == Cut.sliceGen and track and track.IsPlaying do
local tp = track.TimePosition
-- passed the stop, or the looped track wrapped back past the start
if tp >= b or tp < a - 0.002 then
track:AdjustSpeed(0)
track.TimePosition = b
break
end
RunService.RenderStepped:Wait()
end
-- AUTO: it landed, now take it home
if Cut.reverse == "AUTO" and gen == Cut.sliceGen and track.IsPlaying then
task.wait(Cut.returnPause)
if gen == Cut.sliceGen then F.cutterReverse(track, gen) end
end
end)
end
function F.cutterStopSlice()
if cutterTrack and cutterTrack.IsPlaying then
-- AUTO is already on its way home; letting go changes nothing
if Cut.reverse == "AUTO" then return end
-- RELEASE: going back is what letting go means
if Cut.reverse == "RELEASE" then
Cut.sliceGen = Cut.sliceGen + 1 -- stop the forward watcher
F.cutterReverse(cutterTrack, Cut.sliceGen)
return
end
-- a slice is short by definition, so the cutter only checks the toggle
if Hold.enabled and Hold.cutter then
-- don't freeze mid-slice: the slice watcher carries it to its
-- stop frame and freezes there, same as a normal pose reaching
-- its final frame. We just stop it being cut off.
Hold.start(cutterTrack, "slice")
return
end
cutterTrack:Stop(BLEND_OUT)
end
end
-- ============================================================
-- ARROW HUD REACTIONS
-- ============================================================
function F.hitArrow(dir)
local f = arrowHudFrames[dir]
if not f then return end
F.spawnNote(dir)
F.spawnSplash(dir)
F.bumpCombo(dir)
TweenService:Create(f.bg, TweenInfo.new(0.05, Enum.EasingStyle.Linear), {
BackgroundColor3 = ARROW_COLORS[dir].hit,
Size = UDim2.new(0, Arrows.hitSize, 0, Arrows.hitSize),
}):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
function F.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, Arrows.size, 0, Arrows.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
-- ============================================================
-- IDLE ANIMATION
-- ============================================================
function F.idleSpeedFromBPM()
if not BASE_BPM or BASE_BPM <= 0 then return 1 end
return idleBPM / BASE_BPM
end
function F.loadIdleTrack(assetId, shouldLoop)
F.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
function F.playIdleOnce(assetId, cb)
local t = F.loadIdleTrack(assetId, false)
if not t then return end
idleTrack = t; t:Play(0.1)
t:AdjustSpeed(F.idleSpeedFromBPM())
F.updatePreviewAnimation(assetId, "IDLE (once)")
t.Stopped:Connect(function() if cb then cb() end end)
end
function F.playIdleLoop(assetId)
local t = F.loadIdleTrack(assetId, true)
if not t then return end
idleTrack = t; idleLooping = true; t:Play(0.1)
t:AdjustSpeed(F.idleSpeedFromBPM())
F.updatePreviewAnimation(assetId, "IDLE (loop)")
StatusState.playing = true
if F.refreshStatusUI then F.refreshStatusUI() end
end
function F.stopIdle()
idleLooping = false
if idleTrack and idleTrack.IsPlaying then idleTrack:Stop(0.2) end
StatusState.playing = false
if F.refreshStatusUI then F.refreshStatusUI() end
end
function F.setIdleBPM(bpm)
if bpm and bpm > 0 then idleBPM = bpm end
StatusState.bpm = idleBPM
if idleTrack and idleTrack.IsPlaying then
idleTrack:AdjustSpeed(F.idleSpeedFromBPM())
end
if F.refreshStatusUI then F.refreshStatusUI() end
end
function F.setIdleBaseBPM(bpm)
if bpm and bpm > 0 then BASE_BPM = bpm end
if idleTrack and idleTrack.IsPlaying then
idleTrack:AdjustSpeed(F.idleSpeedFromBPM())
end
end
-- ============================================================
-- ANIMATION BPM DETECTOR
-- ============================================================
function F.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
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
function F.detectBpmFromIdleLength(beatsPerLoop)
beatsPerLoop = beatsPerLoop or beatsPerIdleLoop
if not idleTrack or not idleTrack.Length or idleTrack.Length <= 0 then return nil end
local bpm = (beatsPerLoop * 60) / idleTrack.Length
detectedBPM = math.floor(bpm + 0.5)
return detectedBPM
end
-- ============================================================
-- SHARED MUSIC PLAYER (no VC)
-- ============================================================
function F.setupMusicRemote()
local ok, remote = pcall(function()
return ReplicatedStorage:WaitForChild("FNFMusicRemote", 8)
end)
if ok and remote then musicRemote = remote end
return musicRemote ~= nil
end
function F.setMusicStatus(text, color)
if musicStatusLbl then
musicStatusLbl.Text = text
if color then musicStatusLbl.TextColor3 = color end
end
end
function F.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
if not snd or not snd.SoundId:find(expectedId, 1, true) then
F.setMusicStatus("✖ NO SERVER SCRIPT FOUND", Color3.fromRGB(255,80,80))
return
end
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
F.setMusicStatus("✔ PLAYING — allowed in this game", Color3.fromRGB(80,220,120))
elseif snd.IsLoaded then
F.setMusicStatus("● loaded, not playing", Color3.fromRGB(255,180,60))
else
F.setMusicStatus("✖ NOT PLAYABLE HERE (blocked/private asset)", Color3.fromRGB(255,80,80))
end
end)
end
function F.playMusicTrack(id)
if not musicRemote then
F.setMusicStatus("✖ NO SERVER SCRIPT FOUND", Color3.fromRGB(255,80,80))
return
end
musicRequestTick = musicRequestTick + 1
F.setMusicStatus("⏳ requesting...", Color3.fromRGB(0,200,255))
musicRemote:FireServer("play", id)
F.watchMusicStatus(id)
end
function F.stopMusicTrack()
if not musicRemote then return end
musicRequestTick = musicRequestTick + 1
musicRemote:FireServer("stop")
F.setMusicStatus("● stopped", Color3.fromRGB(160,80,80))
end
-- ============================================================
-- ANIMATION SEQUENCE MAKER
-- ============================================================
function F.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
F.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)
F.updatePreviewAnimation(step.id, "SEQUENCE")
end
end
task.wait(math.max(0.01, step.delay or 0.5))
end
F.destroySeqTrack()
seqPlaying = false
end)
end
function F.stopSequence()
seqPlaying = false
F.destroySeqTrack()
end
-- ============================================================
-- PLAYLIST MANAGER
-- ============================================================
function F.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
F.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)
F.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 F.refreshPlaylistUI then F.refreshPlaylistUI() end
end
F.destroySeqTrack()
end)
end
function F.stopPlaylist()
playlistPlaying = false
F.destroySeqTrack()
end
function F.nextPlaylistTrack()
if #playlist == 0 then return end
playlistIndex = playlistIndex + 1
if playlistIndex > #playlist then playlistIndex = 1 end
if F.refreshPlaylistUI then F.refreshPlaylistUI() end
end
function F.prevPlaylistTrack()
if #playlist == 0 then return end
playlistIndex = playlistIndex - 1
if playlistIndex < 1 then playlistIndex = #playlist end
if F.refreshPlaylistUI then F.refreshPlaylistUI() end
end
-- ============================================================
-- INPUT HANDLER
-- ============================================================
function F.onInput(input, gameProcessed)
-- Roblox already consumed this (chat bar, a TextBox, a CoreGui menu).
-- Without this every keybind fires while you're typing.
if gameProcessed then return end
if UserInputService:GetFocusedTextBox() then return end
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
-- Solo mode toggle keybind
if input.KeyCode and input.KeyCode.Name == soloKey
and input.UserInputState == Enum.UserInputState.Begin then
if FNFCamera.enabled then
soloActive = not soloActive
F.notify(soloActive and "🎤 SOLO MODE — spotlight on you!" or "Solo ended", soloActive and "success" or "info")
end
end
-- Silhouette mode toggle keybind
if input.KeyCode and input.KeyCode.Name == silhouetteKey
and input.UserInputState == Enum.UserInputState.Begin then
Sil.set(not silhouetteActive)
end
-- Split screen toggle keybind
if input.KeyCode and input.KeyCode.Name == Split.key
and input.UserInputState == Enum.UserInputState.Begin then
Split.setEnabled(not Split.enabled)
end
-- Health bar toggle keybind
if input.KeyCode and input.KeyCode.Name == Health.key
and input.UserInputState == Enum.UserInputState.Begin then
Health.setEnabled(not Health.enabled)
end
-- Ride mode keybind
if input.KeyCode and input.KeyCode.Name == Ride.key
and input.UserInputState == Enum.UserInputState.Begin then
Ride.setEnabled(not Ride.enabled)
end
-- Boss battle cutscene keybind
if input.KeyCode and input.KeyCode.Name == Boss.key
and input.UserInputState == Enum.UserInputState.Begin then
Boss.toggle()
end
-- Win / lose
if input.KeyCode and input.UserInputState == Enum.UserInputState.Begin then
if input.KeyCode.Name == Result.winKey then Result.win()
elseif input.KeyCode.Name == Result.loseKey then Result.lose() end
end
-- Black screen fade
if input.KeyCode and input.KeyCode.Name == Vid.boKey
and input.UserInputState == Enum.UserInputState.Begin then
Vid.blackout()
end
-- Hold streaks on/off
if input.KeyCode and input.KeyCode.Name == Notes.key
and input.UserInputState == Enum.UserInputState.Begin then
Notes.setEnabled(not Notes.enabled)
end
-- Dodge: plays the animation, nothing else
if input.KeyCode and input.KeyCode.Name == Pose.dodgeKey
and input.UserInputState == Enum.UserInputState.Begin then
F.playDodge()
end
-- Intro / preset / cheat-sheet hotkeys
if input.KeyCode and input.UserInputState == Enum.UserInputState.Begin then
Keys.handle(input.KeyCode.Name)
end
-- Section switching keybinds
if input.KeyCode and input.UserInputState == Enum.UserInputState.Begin then
local kn = input.KeyCode.Name
local switched = false
if kn == sectionNextKey then
currentSectionIdx = currentSectionIdx + 1
if currentSectionIdx > #sections then currentSectionIdx = 1 end
switched = true
elseif kn == sectionPrevKey then
currentSectionIdx = currentSectionIdx - 1
if currentSectionIdx < 1 then currentSectionIdx = #sections end
switched = true
end
if switched and sections[currentSectionIdx] then
local sec = sections[currentSectionIdx]
for _, row in ipairs(CurrentBindings) do
local newId = sec.ids[row.dir]
if newId and newId ~= "0" and newId ~= "" then
row.id = newId; F.destroyTrack(row.key)
end
end
F.notify("§ " .. sec.name, "info")
if F.refreshSectionsUI then F.refreshSectionsUI() end
end
end
for _, row in ipairs(CurrentBindings) do
if input.KeyCode and input.KeyCode.Name == row.key then
if input.UserInputState == Enum.UserInputState.Begin then
F.notePress(row)
elseif input.UserInputState == Enum.UserInputState.End then
F.noteRelease(row)
end
end
end
end
-- ============================================================
-- CHARACTER SETUP
-- ============================================================
function F.buildPreviewClone(char)
if not previewCloneContainer then return end
if previewClone then previewClone:Destroy(); previewClone = nil end
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
F.notify("Preview failed: character isn't clonable right now", "error")
return
end
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
pcall(function() hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None end)
pcall(function() hum.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOff end)
pcall(function() hum.RequiresNeck = false end)
pcall(function() hum.BreakJointsOnDeath = false end)
pcall(function() hum.Health = hum.MaxHealth end)
pcall(function() hum.EvaluateStateMachine = false end)
pcall(function() hum:ChangeState(Enum.HumanoidStateType.Physics) end)
previewAnimator = hum:FindFirstChildOfClass("Animator") or Instance.new("Animator", hum)
else
F.notify("Preview clone has no Humanoid — can't animate it", "warn")
end
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("BasePart") then
d.CanCollide = false; d.Massless = true; d.Anchored = false
end
end
local root = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
if root then root.Anchored = true; clone.PrimaryPart = root 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
function F.setupCharacter(char)
F.cleanupAll(); character = char
FNFCamera:SetPlayerCharacter(char)
pcall(function() character:WaitForChild("HumanoidRootPart", 10) end)
animator = F.getAnimator(char)
if not animator then return end
F.initFace(char)
Hold.clear(false)
Hold.watch(animator)
task.defer(F.preloadBinds)
if legsLocked then F.applyLegsLock(true) end
table.insert(connections, UserInputService.InputBegan:Connect(F.onInput))
table.insert(connections, UserInputService.InputEnded:Connect(F.onInput))
local hum = char:FindFirstChildOfClass("Humanoid")
if hum then table.insert(connections, hum.Died:Connect(F.cleanupAll)) end
task.defer(F.buildPreviewClone, char)
if spacing then spacing:Destroy() end
if SpacingSystem then
spacing = SpacingSystem.new(char)
-- the UI is static; it reads the new instance through `spacing`
if spacing and currentTarget and currentTarget.Character then
spacing:SetOpponent(currentTarget.Character)
end
end
if Split.enabled then Split.refreshClones() end
if Boss.active then Boss.exit() end
if Ride.enabled then Ride.unlock("respawned") end
Result.token = Result.token + 1
Result.winTrack, Result.sitTrack = nil, nil
if Vid.stage.on then task.delay(0.5, function() pcall(Vid.stageRefresh) end) end
Cut.tracks = {}
Cut.trackOwner = nil
cutterTrack = nil
pcall(F.cutterLoadTrack) -- warm every note's animation again
if Cut.wm then task.delay(1, function()
Cut.buildClone()
Cut.editDir(Cut.selDir, true)
end) end
Health.reset()
F.log("Character ready.")
F.notify("Character loaded — ready to inject", "success")
F.startProximityBlock()
F.startFightLoop()
end
-- ============================================================
-- UI HELPER FUNCTIONS
-- ============================================================
function F.uiCorner(p, r)
local c = Instance.new("UICorner"); c.CornerRadius = r or UDim.new(0,12); c.Parent = p
end
function F.uiStroke(p, col, th)
local s = Instance.new("UIStroke"); s.Color = col; s.Thickness = th or 1; s.Transparency = 0.3; s.Parent = p; return s
end
function F.uiGradient(p, c1, c2, rot)
local g = Instance.new("UIGradient")
g.Color = ColorSequence.new(c1 or Color3.fromRGB(255,255,255), c2 or Color3.fromRGB(200,200,220))
g.Rotation = rot or 90
g.Parent = p; return g
end
function F.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(210,210,225)
l.TextSize = ts or 11; l.Font = Enum.Font.GothamMedium
l.TextXAlignment = xAlign or Enum.TextXAlignment.Center
l.Parent = parent; return l
end
function F.makeBtn(parent, text, pos, size, bg, tc, ts)
local b = Instance.new("TextButton")
b.Size = size; b.Position = pos
b.BackgroundColor3 = bg or Color3.fromRGB(38, 36, 52)
b.BackgroundTransparency = 0.08
b.Text = text; b.TextColor3 = tc or Color3.fromRGB(238, 238, 248)
b.Font = Enum.Font.GothamMedium; b.TextSize = ts or 12
b.BorderSizePixel = 0; b.Parent = parent; b.AutoButtonColor = false
F.uiCorner(b, UDim.new(0, 8))
-- single hairline, no heavy gradient stack
local edge = F.uiStroke(b, Color3.fromRGB(255, 255, 255), 1)
edge.Transparency = 0.86
-- thin accent bar down the left edge
local tab = Instance.new("Frame")
tab.Size = UDim2.new(0, 2, 1, -10); tab.Position = UDim2.new(0, 0, 0, 5)
tab.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
tab.BackgroundTransparency = 0.75
tab.BorderSizePixel = 0; tab.Parent = b
F.uiCorner(tab, UDim.new(1, 0))
local baseSize = size
b.MouseEnter:Connect(function()
TweenService:Create(b, TweenInfo.new(0.13, Enum.EasingStyle.Quad), {
BackgroundTransparency = 0
}):Play()
TweenService:Create(edge, TweenInfo.new(0.13), { Transparency = 0.55 }):Play()
TweenService:Create(tab, TweenInfo.new(0.13), { BackgroundTransparency = 0.15 }):Play()
end)
b.MouseLeave:Connect(function()
TweenService:Create(b, TweenInfo.new(0.18, Enum.EasingStyle.Quad), {
BackgroundTransparency = 0.08, Size = baseSize
}):Play()
TweenService:Create(edge, TweenInfo.new(0.18), { Transparency = 0.86 }):Play()
TweenService:Create(tab, TweenInfo.new(0.18), { BackgroundTransparency = 0.75 }):Play()
end)
b.MouseButton1Down:Connect(function()
TweenService:Create(b, TweenInfo.new(0.05), {
Size = UDim2.new(baseSize.X.Scale, baseSize.X.Offset - 3,
baseSize.Y.Scale, baseSize.Y.Offset - 2)
}):Play()
end)
b.MouseButton1Up:Connect(function()
TweenService:Create(b, TweenInfo.new(0.12, Enum.EasingStyle.Back), { Size = baseSize }):Play()
end)
return b
end
function F.makeDivider(parent, yPos, color)
local d = Instance.new("Frame"); d.Size = UDim2.new(1,-24,0,1)
d.Position = UDim2.new(0,12,0,yPos)
d.BackgroundColor3 = color or Color3.fromRGB(255,255,255)
d.BackgroundTransparency = 0.88
d.BorderSizePixel = 0; d.Parent = parent
return d
end
function F.makeBox(parent, pos, size, placeholder, default)
local b = Instance.new("TextBox")
b.Size = size; b.Position = pos
b.BackgroundColor3 = Color3.fromRGB(12, 11, 18)
b.BackgroundTransparency = 0.25
b.PlaceholderText = placeholder or ""
b.PlaceholderColor3 = Color3.fromRGB(105, 105, 125)
b.Text = default or ""; b.TextColor3 = Color3.fromRGB(240, 240, 250)
b.ClearTextOnFocus = false; b.Font = Enum.Font.Gotham; b.TextSize = 11
b.BorderSizePixel = 0
b.Parent = parent; F.uiCorner(b, UDim.new(0, 6))
local pad = Instance.new("UIPadding")
pad.PaddingLeft = UDim.new(0, 8); pad.PaddingRight = UDim.new(0, 8)
pad.Parent = b
local edge = F.uiStroke(b, Color3.fromRGB(255, 255, 255), 1)
edge.Transparency = 0.88
b.Focused:Connect(function()
TweenService:Create(b, TweenInfo.new(0.13), { BackgroundTransparency = 0.05 }):Play()
TweenService:Create(edge, TweenInfo.new(0.13),
{ Transparency = 0.25, Color = THEMES[currentThemeName].accent }):Play()
end)
b.FocusLost:Connect(function()
TweenService:Create(b, TweenInfo.new(0.18), { BackgroundTransparency = 0.25 }):Play()
TweenService:Create(edge, TweenInfo.new(0.18),
{ Transparency = 0.88, Color = Color3.fromRGB(255, 255, 255) }):Play()
end)
return b
end
function F.makeSlider(parent, pos, size, minVal, maxVal, default, onChange)
local track = Instance.new("Frame")
track.Size = size; track.Position = pos
track.BackgroundColor3 = Color3.fromRGB(255,255,255)
track.BackgroundTransparency = 0.85
track.BorderSizePixel = 0; track.Active = true
track.Parent = parent; F.uiCorner(track, UDim.new(1,0))
F.uiStroke(track, Color3.fromRGB(255,255,255), 1).Transparency = 0.85
local fill = Instance.new("Frame")
fill.BackgroundColor3 = Color3.fromRGB(255,80,180)
fill.BackgroundTransparency = 0.35
fill.BorderSizePixel = 0; fill.Size = UDim2.new(0,0,1,0)
fill.Parent = track; F.uiCorner(fill, UDim.new(1,0))
F.regBg(fill)
local handle = Instance.new("Frame")
handle.Size = UDim2.new(0,18,0,18)
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.BackgroundTransparency = 0.15
handle.BorderSizePixel = 0; handle.ZIndex = 2
handle.Parent = track; F.uiCorner(handle, UDim.new(1,0))
local hStroke = F.uiStroke(handle, Color3.fromRGB(255,80,180), 1.5)
hStroke.Transparency = 0.2
F.regStroke(hStroke)
local handleDot = Instance.new("Frame")
handleDot.Size = UDim2.new(0,6,0,6); handleDot.AnchorPoint = Vector2.new(0.5,0.5)
handleDot.Position = UDim2.new(0.5,0,0.5,0)
handleDot.BackgroundColor3 = Color3.fromRGB(255,80,180)
handleDot.BackgroundTransparency = 0.2
handleDot.BorderSizePixel = 0; handleDot.Parent = handle
F.uiCorner(handleDot, UDim.new(1,0)); F.regBg(handleDot)
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
-- One row per extra animation: an ID box, a % box (weighted mode only),
-- a test button and a remove button. Edits save straight into row.alts.
function F.refreshRowAltList(row, listFrame)
for _, c in ipairs(listFrame:GetChildren()) do
if c:IsA("Frame") or c:IsA("TextLabel") then c:Destroy() end
end
local weighted = (row.mode == "WEIGHTED")
if #row.alts == 0 then
local hint = F.makeLabel(listFrame, "No extra animations. Press + ANIM to add one.",
UDim2.new(0,0,0,0), UDim2.new(1,-4,0,22), Color3.fromRGB(120,120,140),
Enum.TextXAlignment.Center, 9)
hint.LayoutOrder = 1
return
end
for j, alt in ipairs(row.alts) do
local entry = Instance.new("Frame")
entry.Size = UDim2.new(1,-4,0,26)
entry.BackgroundColor3 = Color3.fromRGB(24,22,33)
entry.BackgroundTransparency = 0.2
entry.BorderSizePixel = 0
entry.LayoutOrder = j
entry.Parent = listFrame
F.uiCorner(entry, UDim.new(0,5))
F.makeLabel(entry, "#" .. (j + 1), UDim2.new(0,4,0,0), UDim2.new(0,26,1,0),
Color3.fromRGB(150,150,170), Enum.TextXAlignment.Left, 9)
local idW = weighted and 190 or 250
local idBox = F.makeBox(entry, UDim2.new(0,30,0,2), UDim2.new(0,idW,0,22), "animation ID", alt.id)
idBox.TextSize = 10
local a = alt
idBox.FocusLost:Connect(function()
a.id = (idBox.Text:gsub("%D", ""))
idBox.Text = a.id
end)
if weighted then
local pct = F.makeBox(entry, UDim2.new(0,30 + idW + 4,0,2), UDim2.new(0,52,0,22), "%",
tostring(alt.chance or 0))
pct.TextSize = 10
pct.FocusLost:Connect(function()
a.chance = math.clamp(tonumber(pct.Text) or 0, 0, 100)
pct.Text = tostring(a.chance)
F.refreshRowAltList(row, listFrame)
end)
end
local test = F.makeBtn(entry, ">", UDim2.new(1,-52,0,2), UDim2.new(0,24,0,22),
Color3.fromRGB(0,180,230), nil, 10)
test.MouseButton1Down:Connect(function()
if a.id ~= "" then F.playAnim(row.key, a.id, row.speedMult, row.holdLoop); F.hitArrow(row.dir) end
end)
test.MouseButton1Up:Connect(function() F.stopAnim(row.key); F.releaseArrow(row.dir) end)
local rem = F.makeBtn(entry, "X", UDim2.new(1,-26,0,2), UDim2.new(0,24,0,22),
Color3.fromRGB(170,35,35), nil, 10)
rem.MouseButton1Click:Connect(function()
table.remove(row.alts, j)
row.cycleIdx = 0
F.refreshRowAltList(row, listFrame)
end)
end
if weighted then
local total = 0
for _, x in ipairs(row.alts) do total += (x.chance or 0) end
local footer = F.makeLabel(listFrame,
string.format("Extras total %d%% - main ID plays the other %d%%", total, math.max(0, 100 - total)),
UDim2.new(0,0,0,0), UDim2.new(1,-4,0,16),
(total > 100) and Color3.fromRGB(255,80,80) or Color3.fromRGB(140,140,160),
Enum.TextXAlignment.Right, 9)
footer.LayoutOrder = 9999
end
end
-- ============================================================
-- ARROW HUD BUILD
-- ============================================================
function F.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.IgnoreGuiInset = true
sg.DisplayOrder = F.silZ(60)
sg.Parent = PlayerGui
local totalW = #ARROW_ORDER * Arrows.size + (#ARROW_ORDER - 1) * Arrows.gap
-- one container holds all four, so they always move as a unit
local holder = Instance.new("Frame")
holder.Name = "Strip"
holder.Size = UDim2.new(0, totalW, 0, Arrows.size)
holder.AnchorPoint = Vector2.new(0.5, 0.5)
holder.Position = UDim2.new(Arrows.x, Arrows.offX, Arrows.y, Arrows.offY)
holder.BackgroundTransparency = 1
holder.Active = true
holder.ZIndex = F.silZ(62)
holder.Parent = sg
Arrows.container = holder
if Mobile.active and Mobile.hideStrip then holder.Visible = false end
-- notes render behind the arrows
local noteLayer = Instance.new("Frame")
noteLayer.Name = "Notes"
noteLayer.Size = UDim2.new(0, totalW, 0, Arrows.size)
noteLayer.BackgroundTransparency = 1
noteLayer.ZIndex = F.silZ(61)
noteLayer.Parent = holder
Arrows.noteLayer = noteLayer
-- combo readout above the strip
local combo = Instance.new("TextLabel")
combo.Size = UDim2.new(1, 0, 0, 30)
combo.Position = UDim2.new(0, 0, 0, -44)
combo.BackgroundTransparency = 1
combo.Text = ""
combo.TextColor3 = Color3.fromRGB(255, 255, 255)
combo.Font = Enum.Font.GothamBlack; combo.TextSize = 26
combo.TextStrokeTransparency = 0.4
combo.TextTransparency = 1
combo.ZIndex = F.silZ(64)
combo.Parent = holder
Arrows.comboLbl = combo
arrowHudFrames = {}
for i, dir in ipairs(ARROW_ORDER) do
local colors = ARROW_COLORS[dir]
local x = (i - 1) * (Arrows.size + Arrows.gap)
local bg = Instance.new("Frame")
bg.AnchorPoint = Vector2.new(0.5, 0.5)
bg.Size = UDim2.new(0, Arrows.size, 0, Arrows.size)
bg.Position = UDim2.new(0, x + Arrows.size / 2, 0.5, 0)
bg.BackgroundColor3 = colors.idle
bg.BackgroundTransparency = 0.3
bg.BorderSizePixel = 0
bg.ZIndex = F.silZ(63)
bg.Parent = holder
F.uiCorner(bg, UDim.new(0, 14))
local stroke = F.uiStroke(bg, Color3.fromRGB(255,255,255), 1.5)
stroke.Transparency = 0.5
local grad = Instance.new("UIGradient")
grad.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.1),
NumberSequenceKeypoint.new(0.5, 0.35),
NumberSequenceKeypoint.new(1, 0.15),
})
grad.Rotation = 90; grad.Parent = bg
local glow = Instance.new("Frame")
glow.Size = UDim2.new(1, 4, 1, 4); glow.AnchorPoint = Vector2.new(0.5, 0.5)
glow.Position = UDim2.new(0.5, 0, 0.5, 0)
glow.BackgroundTransparency = 0.7
glow.BackgroundColor3 = colors.hit
glow.BorderSizePixel = 0
glow.ZIndex = F.silZ(62)
glow.Visible = false
glow.Parent = bg
F.uiCorner(glow, UDim.new(0, 16))
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.ZIndex = F.silZ(64); lbl.Parent = bg
arrowHudFrames[dir] = { bg = bg, lbl = lbl, stroke = stroke, glow = glow, slot = x }
end
-- drag the whole strip anywhere on screen
local dragging, dragStart, startPos = false, nil, nil
holder.InputBegan:Connect(function(input)
if not Arrows.draggable then return end
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging = true
dragStart = input.Position
startPos = holder.Position
end
end)
UserInputService.InputChanged:Connect(function(input)
if not dragging then return end
if input.UserInputType == Enum.UserInputType.MouseMovement
or input.UserInputType == Enum.UserInputType.Touch then
local delta = input.Position - dragStart
holder.Position = UDim2.new(
startPos.X.Scale, startPos.X.Offset + delta.X,
startPos.Y.Scale, startPos.Y.Offset + delta.Y)
Arrows.x, Arrows.offX = holder.Position.X.Scale, holder.Position.X.Offset
Arrows.y, Arrows.offY = holder.Position.Y.Scale, holder.Position.Y.Offset
end
end)
UserInputService.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging = false
end
end)
F.startArrowLoops()
end
-- A note leaves the arrow and travels off in the chosen direction.
function F.spawnNote(dir)
if not Arrows.noteEnabled or not Arrows.noteLayer then return end
local f = arrowHudFrames[dir]
if not f then return end
local colors = ARROW_COLORS[dir]
local sign = (Arrows.noteDir == "UP") and -1 or 1
local travel = Arrows.noteSpeed * Arrows.noteLife
local note = Instance.new("Frame")
note.AnchorPoint = Vector2.new(0.5, 0.5)
note.Size = UDim2.new(0, Arrows.size * 0.7, 0, Arrows.size * 0.7)
note.Position = UDim2.new(0, f.slot + Arrows.size / 2, 0.5, 0)
note.BackgroundColor3 = colors.hit
note.BackgroundTransparency = 0.15
note.BorderSizePixel = 0
note.Rotation = 45
note.ZIndex = F.silZ(61)
note.Parent = Arrows.noteLayer
F.uiCorner(note, UDim.new(0, 10))
if Arrows.trail then
local trail = Instance.new("Frame")
trail.AnchorPoint = Vector2.new(0.5, sign > 0 and 1 or 0)
trail.Size = UDim2.new(0, Arrows.size * 0.24, 0, 0)
trail.Position = UDim2.new(0.5, 0, sign > 0 and 0 or 1, 0)
trail.BackgroundColor3 = colors.hit
trail.BackgroundTransparency = 0.55
trail.BorderSizePixel = 0
trail.Rotation = -45
trail.ZIndex = F.silZ(60)
trail.Parent = note
TweenService:Create(trail, TweenInfo.new(Arrows.noteLife * 0.6, Enum.EasingStyle.Quad), {
Size = UDim2.new(0, Arrows.size * 0.24, 0, Arrows.size * 1.6),
BackgroundTransparency = 1,
}):Play()
end
TweenService:Create(note, TweenInfo.new(Arrows.noteLife, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
Position = UDim2.new(0, f.slot + Arrows.size / 2, 0.5, sign * travel),
BackgroundTransparency = 1,
Rotation = 45 + sign * 60,
Size = UDim2.new(0, Arrows.size * 0.4, 0, Arrows.size * 0.4),
}):Play()
task.delay(Arrows.noteLife + 0.15, function()
if note and note.Parent then note:Destroy() end
end)
end
-- Expanding ring where the arrow was struck.
function F.spawnSplash(dir)
if not Arrows.splash then return end
local f = arrowHudFrames[dir]
if not f then return end
local ring = Instance.new("Frame")
ring.AnchorPoint = Vector2.new(0.5, 0.5)
ring.Size = UDim2.new(0, Arrows.size, 0, Arrows.size)
ring.Position = UDim2.new(0.5, 0, 0.5, 0)
ring.BackgroundTransparency = 1
ring.ZIndex = F.silZ(65)
ring.Parent = f.bg
F.uiCorner(ring, UDim.new(0, 18))
local rs = F.uiStroke(ring, ARROW_COLORS[dir].hit, 3)
rs.Transparency = 0.1
TweenService:Create(ring, TweenInfo.new(0.35, Enum.EasingStyle.Quint, Enum.EasingDirection.Out), {
Size = UDim2.new(0, Arrows.size * 2.1, 0, Arrows.size * 2.1),
}):Play()
local t = TweenService:Create(rs, TweenInfo.new(0.35), { Transparency = 1, Thickness = 0 })
t:Play()
t.Completed:Connect(function() if ring then ring:Destroy() end end)
end
function F.bumpCombo(dir)
Arrows.comboCount = Arrows.comboCount + 1
Arrows.lastHit = os.clock()
-- lean the strip toward whichever side you hit
if Arrows.tilt then
local idx = table.find(ARROW_ORDER, dir) or 2
local side = (idx - 2.5) / 1.5 -- -1 on the far left, +1 on the far right
Arrows.tiltVel = Arrows.tiltVel + side * 26
end
local lbl = Arrows.comboLbl
if not lbl or not Arrows.combo then return end
if Arrows.comboCount < 2 then return end
lbl.Text = Arrows.comboCount .. "x"
lbl.TextColor3 = ARROW_COLORS[dir].hit
lbl.TextTransparency = 0
lbl.TextSize = 34
TweenService:Create(lbl, TweenInfo.new(0.25, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
{ TextSize = 26 }):Play()
end
function F.startArrowLoops()
if F._arrowLoopsStarted then return end
F._arrowLoopsStarted = true
-- combo decay
task.spawn(function()
while true do
task.wait(0.2)
if Arrows.comboCount > 0 and os.clock() - Arrows.lastHit > 1.8 then
Arrows.comboCount = 0
if Arrows.comboLbl then
TweenService:Create(Arrows.comboLbl, TweenInfo.new(0.3), { TextTransparency = 1 }):Play()
end
end
end
end)
-- beat bob + strip lean
task.spawn(function()
local bobPhase = 0
while true do
local dt = RunService.Heartbeat:Wait()
local holder = Arrows.container
if holder and holder.Parent then
-- sprung lean back to level
Arrows.tiltVel = Arrows.tiltVel + (-180 * Arrows.tiltPos - 16 * Arrows.tiltVel) * dt
Arrows.tiltPos = Arrows.tiltPos + Arrows.tiltVel * dt
holder.Rotation = Arrows.tilt and math.clamp(Arrows.tiltPos, -12, 12) or 0
if Arrows.beatBob then
bobPhase = bobPhase + dt * (math.max(40, StatusState.bpm or 100) / 60) * math.pi * 2
local s = 1 + math.sin(bobPhase) * 0.035
local w = #ARROW_ORDER * Arrows.size + (#ARROW_ORDER - 1) * Arrows.gap
holder.Size = UDim2.new(0, w * s, 0, Arrows.size * s)
end
end
end
end)
end
-- ============================================================
-- MIC SPECTROGRAM SETUP
-- ============================================================
function F.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
F.log("Mic spectrogram unavailable on this client/game — using animation-synced fallback")
end
return micAvailable
end
-- ============================================================
-- BEAT / VISUALIZER LOOPS
-- ============================================================
local eqBars, waveBars, beatDot, notesLayer = nil, nil, nil, nil
function F.startVisualizerLoops()
if F._vizLoopsStarted then return end
F._vizLoopsStarted = true
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)
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)
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)
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.ZIndex = F.silZ(0)
n.Parent = notesLayer
F.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)
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 * F.idleSpeedFromBPM()
end
vinylRotation = (vinylRotation + (rpm / 60) * 360 * (1/60)) % 360
vinylImage.Rotation = vinylRotation
end
end
end)
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
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)
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)
end
-- ============================================================
-- DYNAMIC SPACING SYSTEM v2.0
-- ============================================================
-- ============================================================
-- DYNAMIC SPACING SYSTEM v3
-- You step IN when you sing. If you both sing, you both step
-- in and close the gap further. Spamming notes holds you there
-- — the return timer resets on every note. Stop playing and
-- after the cooldown you ease back to exactly where you began.
-- ============================================================
local EASE = {
Linear = function(a) return a end,
SineOut = function(a) return math.sin(a * math.pi / 2) end,
SineInOut = function(a) return -(math.cos(math.pi * a) - 1) / 2 end,
BackOut = function(a)
local c1, c3 = 1.70158, 2.70158
a -= 1
return 1 + c3 * a * a * a + c1 * a * a
end,
Snap = function(a) return a < 0.5 and (a * 2) ^ 2 * 0.5 or 1 - ((1 - a) * 2) ^ 2 * 0.5 end,
}
local EASE_ORDER = { "SineOut", "SineInOut", "BackOut", "Snap", "Linear" }
SPACING = {
enabled = false,
stepIn = 2.2, -- studs you close on your first note
bothBonus = 1.3, -- extra studs when you're both singing
stepTime = 0.3, -- how long the step in takes
returnDelay = 3, -- seconds of silence before you head back
returnTime = 0.9, -- how long the walk back takes
minGap = 3, -- never get closer than this to each other
stepEase = "SineOut",
returnEase = "SineInOut",
}
local function flatDirection(fromPos, toPos)
local d = toPos - fromPos
d = Vector3.new(d.X, 0, d.Z)
if d.Magnitude < 1e-3 then return Vector3.new(0, 0, 1) end
return d.Unit
end
-- Per-character movement state. The root is anchored only while we own it,
-- so limb animation is never touched.
local function makeSpacingRig(char)
local root = char:WaitForChild("HumanoidRootPart")
return {
char = char, root = root,
anchored = false, singing = false,
home = root.Position, -- where they stood before any of this
rotationOnly = root.CFrame - root.CFrame.Position,
offset = 0, -- studs currently stepped in
moving = false, moveStart = 0, moveDur = 0,
moveEaseFn = EASE.SineOut,
startPos = root.Position, endPos = root.Position,
lastNote = 0, returning = false,
}
end
SpacingSystem = {}
SpacingSystem.__index = SpacingSystem
function SpacingSystem.new(myCharacter)
local self = setmetatable({}, SpacingSystem)
self.A = makeSpacingRig(myCharacter)
self.B = nil
self._heartbeatConn = RunService.Heartbeat:Connect(function(dt) self:_update(dt) end)
return self
end
function SpacingSystem:Destroy()
if self._heartbeatConn then self._heartbeatConn:Disconnect() end
self:_release(self.A)
if self.B then self:_release(self.B) end
end
function SpacingSystem:SetOpponent(char)
if self.B and self.B.char == char then return end
if self.B then self:_goHome(self.B, true); self:_release(self.B) end
self.B = char and makeSpacingRig(char) or nil
end
function SpacingSystem:SetEnabled(state)
SPACING.enabled = state
if not state then
self:_goHome(self.A, true)
if self.B then self:_goHome(self.B, true) end
end
end
function SpacingSystem:_own(rig)
if not rig.anchored then
rig.anchored = true
rig.root.Anchored = true
rig.rotationOnly = rig.root.CFrame - rig.root.CFrame.Position
end
end
function SpacingSystem:_release(rig)
if rig.anchored then rig.anchored = false; rig.root.Anchored = false end
end
-- Move this rig to a given step-in distance from home, along the line
-- between the two characters.
function SpacingSystem:_moveTo(rig, otherRig, targetOffset, duration, easeName)
if not otherRig then return end
self:_own(rig)
-- respect the minimum gap so they never overlap
local gap = (rig.root.Position - otherRig.root.Position).Magnitude
local room = math.max(0, gap - SPACING.minGap)
local delta = targetOffset - rig.offset
if delta > room then targetOffset = rig.offset + room end
local axis = flatDirection(rig.home, otherRig.root.Position)
rig.startPos = rig.root.Position
rig.endPos = rig.home + axis * targetOffset
rig.moveStart = os.clock()
rig.moveDur = math.max(duration, 0.001)
rig.moveEaseFn = EASE[easeName] or EASE.SineOut
rig.moving = true
rig.offset = targetOffset
end
function SpacingSystem:_goHome(rig, instant)
rig.returning = true
if instant then
rig.offset = 0
rig.moving = false
if rig.anchored then
rig.root.CFrame = CFrame.new(rig.home) * rig.rotationOnly
end
self:_release(rig)
rig.returning = false
return
end
local other = (rig == self.A) and self.B or self.A
self:_moveTo(rig, other, 0, SPACING.returnTime, SPACING.returnEase)
end
function SpacingSystem:_rigs(who)
if who == self.A.char then return self.A, self.B end
if self.B and who == self.B.char then return self.B, self.A end
return nil, nil
end
-- How far in this rig should be right now, given who's singing.
function SpacingSystem:_wantedOffset(rig, other)
if not other then return 0 end
local both = rig.singing and other.singing
if not rig.singing then return rig.offset end
return SPACING.stepIn + (both and SPACING.bothBonus or 0)
end
function SpacingSystem:OnSingStart(who)
if not SPACING.enabled or not self.B then return end
local rig, other = self:_rigs(who)
if not rig or not other then return end
rig.singing = true
rig.lastNote = os.clock()
rig.returning = false
-- step in, or deepen the step if you're both going
local want = self:_wantedOffset(rig, other)
if math.abs(want - rig.offset) > 0.02 then
self:_moveTo(rig, other, want, SPACING.stepTime, SPACING.stepEase)
end
-- the other side deepens too the moment you join in
if other.singing then
local ow = self:_wantedOffset(other, rig)
if math.abs(ow - other.offset) > 0.02 then
self:_moveTo(other, rig, ow, SPACING.stepTime, SPACING.stepEase)
end
end
end
-- Releasing a key does NOT send you home. Only the cooldown does, so
-- spamming notes keeps you planted where you stepped to.
function SpacingSystem:OnSingStop(who)
if not self.B then return end
local rig = self:_rigs(who)
if not rig then return end
rig.singing = false
rig.lastNote = os.clock()
end
function SpacingSystem:_update(dt)
local rigs = self.B and { self.A, self.B } or { self.A }
local now = os.clock()
for _, rig in ipairs(rigs) do
local other = (rig == self.A) and self.B or self.A
-- cooldown: quiet for long enough, head back to the start
if SPACING.enabled and rig.offset ~= 0 and not rig.singing
and not rig.returning and (now - rig.lastNote) > SPACING.returnDelay then
self:_goHome(rig, false)
end
if rig.anchored then
local pos
if rig.moving then
local a = math.clamp((now - rig.moveStart) / rig.moveDur, 0, 1)
pos = rig.startPos:Lerp(rig.endPos, rig.moveEaseFn(a))
if a >= 1 then
rig.moving = false
-- arrived home: give the character back to the player
if rig.returning and math.abs(rig.offset) < 0.02 then
rig.returning = false
self:_release(rig)
end
end
else
pos = rig.endPos
end
if rig.anchored and pos then
rig.root.CFrame = CFrame.new(pos) * rig.rotationOnly
end
end
end
end
-- ============================================================
-- SPACING GUI
-- ============================================================
-- Built once from buildTab_Settings. It drives the SPACING config table and
-- the current `spacing` instance, so respawning never rebuilds the UI.
function F.buildSpacingUI(page, startY)
if not page then return end
F.makeDivider(page, startY, Color3.fromRGB(0,200,255))
local title = F.makeLabel(page, "DYNAMIC SPACING", UDim2.new(0,8,0,startY+6),
UDim2.new(1,-16,0,22), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
title.Font = Enum.Font.GothamBold
F.makeLabel(page, "Step in when you sing. Both singing closes the gap further. Spamming notes holds the position; going quiet sends you back.",
UDim2.new(0,8,0,startY+28), UDim2.new(1,-16,0,40), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local toggleBtn = F.makeBtn(page, SPACING.enabled and "SPACING ON" or "SPACING OFF",
UDim2.new(0,8,0,startY+70), UDim2.new(0,190,0,28),
SPACING.enabled and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55), nil, 10)
toggleBtn.MouseButton1Click:Connect(function()
if spacing then spacing:SetEnabled(not SPACING.enabled) else SPACING.enabled = not SPACING.enabled end
toggleBtn.Text = SPACING.enabled and "SPACING ON" or "SPACING OFF"
toggleBtn.BackgroundColor3 = SPACING.enabled and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55)
end)
local homeBtn = F.makeBtn(page, "RETURN NOW", UDim2.new(0,206,0,startY+70),
UDim2.new(0,190,0,28), Color3.fromRGB(70,120,255), nil, 10)
homeBtn.MouseButton1Click:Connect(function()
if spacing then
spacing:_goHome(spacing.A, false)
if spacing.B then spacing:_goHome(spacing.B, false) end
end
end)
local oppLbl = F.makeLabel(page, "Opponent: none", UDim2.new(0,8,0,startY+104),
UDim2.new(1,-16,0,18), Color3.fromRGB(0,200,255), Enum.TextXAlignment.Left, 10)
task.spawn(function()
while true do
task.wait(0.5)
if not oppLbl.Parent then break end
local n = (spacing and spacing.B and spacing.B.char and spacing.B.char.Name)
or (currentTarget and currentTarget.Name)
oppLbl.Text = "Opponent: " .. (n or "none")
end
end)
local searchBox = F.makeBox(page, UDim2.new(0,8,0,startY+126), UDim2.new(1,-16,0,28), "Search player name...")
local results = Instance.new("ScrollingFrame")
results.Size = UDim2.new(1,-16,0,84); results.Position = UDim2.new(0,8,0,startY+160)
results.BackgroundColor3 = Color3.fromRGB(12,11,18); results.BackgroundTransparency = 0.25
results.BorderSizePixel = 0; results.ScrollBarThickness = 3
results.CanvasSize = UDim2.new(0,0,0,0); results.AutomaticCanvasSize = Enum.AutomaticSize.Y
results.Parent = page; F.uiCorner(results, UDim.new(0,6))
local rl = Instance.new("UIListLayout"); rl.Padding = UDim.new(0,2); rl.Parent = results
local function refreshResults(q)
for _, c in ipairs(results:GetChildren()) do
if c:IsA("TextButton") then c:Destroy() end
end
if q == "" then return end
q = q:lower()
for _, plr in ipairs(Players:GetPlayers()) do
if plr ~= LocalPlayer and plr.Name:lower():find(q, 1, true) then
local row = F.makeBtn(results, plr.Name, UDim2.new(0,2,0,0),
UDim2.new(1,-4,0,24), Color3.fromRGB(28,28,42), nil, 9)
row.MouseButton1Click:Connect(function()
if not plr.Character then F.notify("They have no character yet", "warn"); return end
if spacing then spacing:SetOpponent(plr.Character) end
oppLbl.Text = "Opponent: " .. plr.Name
searchBox.Text = ""
refreshResults("")
end)
end
end
end
searchBox:GetPropertyChangedSignal("Text"):Connect(function() refreshResults(searchBox.Text) end)
local rows = {
{ key = "stepIn", label = "STEP-IN DISTANCE", min = 0, max = 8, fmt = "%.2f" },
{ key = "bothBonus", label = "BOTH-SINGING BONUS",min = 0, max = 6, fmt = "%.2f" },
{ key = "stepTime", label = "STEP TIME (s)", min = 0.05,max = 2, fmt = "%.2f" },
{ key = "returnDelay", label = "RETURN COOLDOWN (s)",min = 0.5,max = 10, fmt = "%.2f" },
{ key = "returnTime", label = "RETURN TIME (s)", min = 0.1, max = 3, fmt = "%.2f" },
{ key = "minGap", label = "MINIMUM GAP", min = 0, max = 10, fmt = "%.2f" },
}
for i, row in ipairs(rows) do
local ry = startY + 254 + (i-1) * 42
F.makeLabel(page, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,220,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(page, string.format(row.fmt, SPACING[row.key]),
UDim2.new(0,306,0,ry), UDim2.new(0,90,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local ck, cfm = row.key, row.fmt
F.makeSlider(page, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12),
row.min, row.max, SPACING[row.key], function(v)
SPACING[ck] = v
vlbl.Text = string.format(cfm, v)
end)
end
-- easing pickers
local eY = startY + 254 + #rows * 42 + 6
F.makeLabel(page, "STEP EASING", UDim2.new(0,8,0,eY), UDim2.new(1,-16,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local stepBtns = {}
for i, name in ipairs(EASE_ORDER) do
local b = F.makeBtn(page, name, UDim2.new(0, 8 + (i-1) * 78, 0, eY + 20),
UDim2.new(0, 74, 0, 24),
name == SPACING.stepEase and Color3.fromRGB(255,80,180) or Color3.fromRGB(35,35,55), nil, 8)
stepBtns[name] = b
b.MouseButton1Click:Connect(function()
SPACING.stepEase = name
for n, bb in pairs(stepBtns) do
bb.BackgroundColor3 = (n == name) and Color3.fromRGB(255,80,180) or Color3.fromRGB(35,35,55)
end
end)
end
F.makeLabel(page, "RETURN EASING", UDim2.new(0,8,0,eY+52), UDim2.new(1,-16,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local retBtns = {}
for i, name in ipairs(EASE_ORDER) do
local b = F.makeBtn(page, name, UDim2.new(0, 8 + (i-1) * 78, 0, eY + 72),
UDim2.new(0, 74, 0, 24),
name == SPACING.returnEase and Color3.fromRGB(0,200,255) or Color3.fromRGB(35,35,55), nil, 8)
retBtns[name] = b
b.MouseButton1Click:Connect(function()
SPACING.returnEase = name
for n, bb in pairs(retBtns) do
bb.BackgroundColor3 = (n == name) and Color3.fromRGB(0,200,255) or Color3.fromRGB(35,35,55)
end
end)
end
end
-- ============================================================
-- MAIN GUI BUILD
-- ============================================================
function F.buildTab_Binds(pages)
local pg1 = pages["📋 BINDS"]
F.makeLabel(pg1, "DIR", UDim2.new(0,8,0,6), UDim2.new(0,36,0,20), Color3.fromRGB(255,80,180))
F.makeLabel(pg1, "KEY", UDim2.new(0,48,0,6), UDim2.new(0,62,0,20), Color3.fromRGB(255,80,180))
F.makeLabel(pg1, "ANIM ID", UDim2.new(0,116,0,6), UDim2.new(0,180,0,20),Color3.fromRGB(255,80,180))
F.makeLabel(pg1, "TEST", UDim2.new(0,308,0,6), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180))
F.makeDivider(pg1, 28, Color3.fromRGB(0,200,255))
local ROW_H = 186
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)
F.makeLabel(pg1, row.dir, UDim2.new(0,8,0,y), UDim2.new(0,36,0,28), dirColor)
local keyBtn = F.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 = F.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; F.destroyTrack(row.key)
F.getTrack(n, row.holdLoop) -- ready before the first press
else
idBox.Text = row.id
end
end)
local testBtn = F.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 = F.rollBindingAnimId(row) -- follows the note's mode
F.playAnim(row.key, chosenId, row.speedMult, row.holdLoop); F.hitArrow(row.dir); F.swapFace(row.dir)
end)
testBtn.MouseButton1Up:Connect(function() F.stopAnim(row.key); F.releaseArrow(row.dir); F.restoreFace() end)
local sy = y + 30
F.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 spdBox = F.makeBox(pg1, UDim2.new(0,42,0,sy), UDim2.new(0,120,0,22), "1.00", string.format("%.2f", row.speedMult))
local spdValLbl = F.makeLabel(pg1, string.format("x%.2f", row.speedMult),
UDim2.new(0,170,0,sy), UDim2.new(0,222,0,22), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 10)
F.regText(spdValLbl)
spdBox.FocusLost:Connect(function()
local n = tonumber(spdBox.Text)
if n and n > 0 then
row.speedMult = n
spdValLbl.Text = string.format("x%.2f", n)
if StatusState.currentAnimName == row.key then
StatusState.speedMult = n
if F.refreshStatusUI then F.refreshStatusUI() end
end
else
spdBox.Text = string.format("%.2f", row.speedMult)
end
end)
local sy2 = y + 58
local holdBtn = F.makeBtn(pg1, row.holdLoop and "HOLD-LOOP: ON" or "HOLD-LOOP: OFF",
UDim2.new(0,8,0,sy2), UDim2.new(0,126,0,24),
row.holdLoop and Color3.fromRGB(0,180,230) or 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)
-- how the note chooses between its animations
local modeBtn = F.makeBtn(pg1, "MODE: " .. (row.mode or "CYCLE"),
UDim2.new(0,140,0,sy2), UDim2.new(0,130,0,24), Color3.fromRGB(70,120,255), nil, 9)
local addBtn = F.makeBtn(pg1, "+ ANIM", UDim2.new(0,276,0,sy2),
UDim2.new(0,120,0,24), Color3.fromRGB(60,190,120), nil, 10)
local sy3 = y + 88
local altListFrame = Instance.new("ScrollingFrame")
altListFrame.Size = UDim2.new(1,-16,0,88)
altListFrame.Position = UDim2.new(0,8,0,sy3)
altListFrame.BackgroundColor3 = Color3.fromRGB(12,11,18)
altListFrame.BackgroundTransparency = 0.25
altListFrame.BorderSizePixel = 0
altListFrame.ScrollBarThickness = 3
altListFrame.CanvasSize = UDim2.new(0,0,0,0)
altListFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
altListFrame.Parent = pg1
F.uiCorner(altListFrame, UDim.new(0,6))
local altListLayout = Instance.new("UIListLayout")
altListLayout.Padding = UDim.new(0,3)
altListLayout.SortOrder = Enum.SortOrder.LayoutOrder
altListLayout.Parent = altListFrame
local altListPad = Instance.new("UIPadding")
altListPad.PaddingTop = UDim.new(0,3); altListPad.PaddingLeft = UDim.new(0,3)
altListPad.PaddingRight = UDim.new(0,3)
altListPad.Parent = altListFrame
local modeOrder = { "CYCLE", "RANDOM", "WEIGHTED" }
modeBtn.MouseButton1Click:Connect(function()
local idx = table.find(modeOrder, row.mode or "CYCLE") or 1
row.mode = modeOrder[(idx % #modeOrder) + 1]
row.cycleIdx = 0
modeBtn.Text = "MODE: " .. row.mode
F.refreshRowAltList(row, altListFrame)
F.notify(row.dir .. ": " .. ({
CYCLE = "plays each animation in order",
RANDOM = "picks one at random",
WEIGHTED = "uses the % chances",
})[row.mode], "info")
end)
addBtn.MouseButton1Click:Connect(function()
if #row.alts >= 12 then F.notify("12 extra animations is the limit per note", "warn"); return end
table.insert(row.alts, { id = "", chance = 0 })
F.refreshRowAltList(row, altListFrame)
-- jump to the new box so you can paste straight in
task.defer(function()
altListFrame.CanvasPosition = Vector2.new(0, math.max(0, altListFrame.AbsoluteCanvasSize.Y))
end)
end)
F.refreshRowAltList(row, altListFrame)
end
-- ── POSE RESPONSE ──
local prY = startY + #CurrentBindings * ROW_H + 6
F.makeDivider(pg1, prY, Color3.fromRGB(0,200,255))
local prTitle = F.makeLabel(pg1, "POSE RESPONSE", UDim2.new(0,8,0,prY+6),
UDim2.new(0,200,0,20), Color3.fromRGB(0,200,255), Enum.TextXAlignment.Left, 12)
prTitle.Font = Enum.Font.GothamBold
F.makeLabel(pg1, "How fast a pose lands when you press. Lower is snappier.",
UDim2.new(0,8,0,prY+26), UDim2.new(1,-16,0,16), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local prSnap = F.makeBtn(pg1, Pose.snap and "SNAP: ON" or "SNAP: OFF",
UDim2.new(0,8,0,prY+46), UDim2.new(0,126,0,26),
Pose.snap and Color3.fromRGB(255,160,40) or Color3.fromRGB(35,35,55), nil, 9)
prSnap.MouseButton1Click:Connect(function()
Pose.snap = not Pose.snap
prSnap.Text = Pose.snap and "SNAP: ON" or "SNAP: OFF"
prSnap.BackgroundColor3 = Pose.snap and Color3.fromRGB(255,160,40) or Color3.fromRGB(35,35,55)
end)
F.makeLabel(pg1, "Skip the motion and land on the pose that frame.",
UDim2.new(0,142,0,prY+46), UDim2.new(1,-150,0,26), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
F.makeLabel(pg1, "REACH TIME", UDim2.new(0,8,0,prY+80), UDim2.new(0,200,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local prLbl = F.makeLabel(pg1, string.format("%.3fs", Pose.reach),
UDim2.new(0,306,0,prY+80), UDim2.new(0,90,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(prLbl)
F.makeSlider(pg1, UDim2.new(0,8,0,prY+100), UDim2.new(1,-16,0,12), 0.01, 0.3, Pose.reach, function(v)
Pose.reach = v
prLbl.Text = string.format("%.3fs", v)
end)
-- ── DODGE: just an animation on a key ──
local dgY = prY + 124
F.makeDivider(pg1, dgY, Color3.fromRGB(80,220,255))
local dgTitle = F.makeLabel(pg1, "DODGE", UDim2.new(0,8,0,dgY+6),
UDim2.new(0,200,0,20), Color3.fromRGB(80,220,255), Enum.TextXAlignment.Left, 12)
dgTitle.Font = Enum.Font.GothamBold
local dgKey = F.makeBtn(pg1, Pose.dodgeKey, UDim2.new(0,8,0,dgY+30),
UDim2.new(0,70,0,28), Color3.fromRGB(25,35,48), nil, 11)
dgKey.MouseButton1Click:Connect(function()
dgKey.Text = "..."; dgKey.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
Pose.dodgeKey = inp.KeyCode.Name
dgKey.Text = Pose.dodgeKey
dgKey.BackgroundColor3 = Color3.fromRGB(25,35,48)
if Keys.refresh then Keys.refresh() end
conn:Disconnect()
end
end)
end)
local dgId = F.makeBox(pg1, UDim2.new(0,84,0,dgY+30), UDim2.new(0,218,0,28), "dodge animation ID", Pose.dodgeId)
dgId.FocusLost:Connect(function()
Pose.dodgeId = (dgId.Text:gsub("%D",""))
dgId.Text = Pose.dodgeId
if Pose.dodgeId ~= "" then F.getTrack(Pose.dodgeId, false) end
end)
local dgTest = F.makeBtn(pg1, "TEST", UDim2.new(0,308,0,dgY+30),
UDim2.new(0,88,0,28), Color3.fromRGB(0,180,230), nil, 10)
dgTest.MouseButton1Click:Connect(function()
if Pose.dodgeId == "" then F.notify("Paste a dodge animation ID first", "warn"); return end
F.playDodge()
end)
-- ── POSE HOLD (sits right under the pose binds) ──
local hdY = dgY + 70
F.makeDivider(pg1, hdY, Color3.fromRGB(255,160,40))
local hdTitle = F.makeLabel(pg1, "POSE HOLD", UDim2.new(0,8,0,hdY+6),
UDim2.new(0,160,0,20), Color3.fromRGB(255,160,40), Enum.TextXAlignment.Left, 12)
hdTitle.Font = Enum.Font.GothamBold
F.makeLabel(pg1, "Short poses stay frozen after you let go. Your next note or any emote breaks it; idle and walking don't.",
UDim2.new(0,8,0,hdY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local function hdToggle(x, key, on, off)
local b = F.makeBtn(pg1, Hold[key] and on or off, UDim2.new(0,x,0,hdY+58),
UDim2.new(0,126,0,26), Hold[key] and Color3.fromRGB(255,160,40) or Color3.fromRGB(35,35,55), nil, 9)
b.MouseButton1Click:Connect(function()
Hold[key] = not Hold[key]
b.Text = Hold[key] and on or off
b.BackgroundColor3 = Hold[key] and Color3.fromRGB(255,160,40) or Color3.fromRGB(35,35,55)
if key == "enabled" and not Hold.enabled then Hold.clear(true) end
end)
end
hdToggle(8, "enabled", "HOLD: ON", "HOLD: OFF")
hdToggle(140, "holdAll", "HOLD ALL: ON", "HOLD ALL: OFF")
hdToggle(272, "cutter", "CUTTER: ON", "CUTTER: OFF")
local unBtn = F.makeBtn(pg1, Hold.untilNext and "UNTIL NEXT POSE: ON" or "UNTIL NEXT POSE: OFF",
UDim2.new(0,8,0,hdY+88), UDim2.new(0,190,0,26),
Hold.untilNext and Color3.fromRGB(255,160,40) or Color3.fromRGB(35,35,55), nil, 9)
unBtn.MouseButton1Click:Connect(function()
Hold.untilNext = not Hold.untilNext
unBtn.Text = Hold.untilNext and "UNTIL NEXT POSE: ON" or "UNTIL NEXT POSE: OFF"
unBtn.BackgroundColor3 = Hold.untilNext and Color3.fromRGB(255,160,40) or Color3.fromRGB(35,35,55)
end)
F.makeLabel(pg1, "Never times out — stays posed until the next one.",
UDim2.new(0,206,0,hdY+88), UDim2.new(1,-214,0,26), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
local hdRows = {
{ key = "duration", label = "HOLD TIME (s)", min = 0.5, max = 15, fmt = "%.1f" },
{ key = "shortAt", label = "COUNTS AS SHORT UNDER (s)", min = 0.1, max = 5, fmt = "%.2f" },
{ key = "fadeOut", label = "RELEASE BLEND (s)", min = 0, max = 1, fmt = "%.2f" },
}
for i, row in ipairs(hdRows) do
local ry = hdY + 122 + (i-1) * 40
F.makeLabel(pg1, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,240,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 9)
local vlbl = F.makeLabel(pg1, string.format(row.fmt, Hold[row.key]),
UDim2.new(0,306,0,ry), UDim2.new(0,90,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 9)
F.regText(vlbl)
local ck, cfm = row.key, row.fmt
F.makeSlider(pg1, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12),
row.min, row.max, Hold[row.key], function(v)
Hold[ck] = v
vlbl.Text = string.format(cfm, v)
end)
end
local hdStatus = F.makeLabel(pg1, "", UDim2.new(0,8,0,hdY + 122 + #hdRows * 40 + 2),
UDim2.new(1,-16,0,18), Color3.fromRGB(160,160,180), Enum.TextXAlignment.Left, 10)
task.spawn(function()
while true do
task.wait(0.1)
if not hdStatus.Parent then break end
if Hold.track then
hdStatus.Text = Hold.untilNext
and string.format("HOLDING %s — until next pose", tostring(Hold.label))
or string.format("HOLDING %s — %.1fs left", tostring(Hold.label), Hold.remaining())
hdStatus.TextColor3 = Color3.fromRGB(255,160,40)
else
hdStatus.Text = Hold.enabled and "ready" or "off"
hdStatus.TextColor3 = Color3.fromRGB(130,130,150)
end
end
end)
-- SOLO MODE SECTION
local soloY = hdY + 122 + #hdRows * 40 + 30
F.makeDivider(pg1, soloY, Color3.fromRGB(255,200,60))
local soloTitle = F.makeLabel(pg1, "🎤 SOLO MODE", UDim2.new(0,8,0,soloY+6), UDim2.new(0,140,0,20),
Color3.fromRGB(255,200,60), Enum.TextXAlignment.Left, 12)
soloTitle.Font = Enum.Font.GothamBold
F.makeLabel(pg1, "Camera zooms in tight on you with cinematic bars. Press the key to toggle.",
UDim2.new(0,8,0,soloY+26), UDim2.new(1,-16,0,16), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
F.makeLabel(pg1, "KEY", UDim2.new(0,8,0,soloY+48), UDim2.new(0,30,0,24), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local soloKeyBtn = F.makeBtn(pg1, soloKey, UDim2.new(0,42,0,soloY+48), UDim2.new(0,60,0,24), Color3.fromRGB(25,35,48), nil, 11)
soloKeyBtn.MouseButton1Click:Connect(function()
soloKeyBtn.Text = "..."
soloKeyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
soloKey = inp.KeyCode.Name
soloKeyBtn.Text = soloKey
soloKeyBtn.BackgroundColor3 = Color3.fromRGB(25,35,48)
conn:Disconnect()
end
end)
end)
local soloStatusLbl = F.makeLabel(pg1, soloActive and "🎤 SOLO ACTIVE" or "● OFF",
UDim2.new(0,114,0,soloY+48), UDim2.new(0,120,0,24),
soloActive and Color3.fromRGB(255,200,60) or Color3.fromRGB(160,80,80), Enum.TextXAlignment.Left, 10)
F.makeLabel(pg1, "ZOOM", UDim2.new(0,240,0,soloY+48), UDim2.new(0,40,0,24), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 9)
F.makeSlider(pg1, UDim2.new(0,280,0,soloY+50), UDim2.new(0,112,0,12), 3, 12, soloCamDist, function(v)
soloCamDist = v
end)
task.spawn(function()
while true do
task.wait(0.2)
if soloStatusLbl and soloStatusLbl.Parent then
soloStatusLbl.Text = soloActive and "🎤 SOLO ACTIVE" or "● OFF"
soloStatusLbl.TextColor3 = soloActive and Color3.fromRGB(255,200,60) or Color3.fromRGB(160,80,80)
end
end
end)
local idleY = soloY + 80
F.makeDivider(pg1, idleY, Color3.fromRGB(255,80,180))
local idleTitleLbl = F.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)
F.regText(idleTitleLbl)
local statusLbl = F.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 = F.makeBox(pg1, UDim2.new(0,8,0,idleY+30), UDim2.new(1,-16,0,28), "Paste idle anim ID...")
local loopBtn = F.makeBtn(pg1, "⟳ LOOP", UDim2.new(0,8,0,idleY+64), UDim2.new(0,120,0,28), Color3.fromRGB(70,120,255))
local onceBtn = F.makeBtn(pg1, "▶ ONCE", UDim2.new(0,136,0,idleY+64), UDim2.new(0,120,0,28), Color3.fromRGB(60,190,120))
local idleStop = F.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 F.notify("Enter an idle anim ID first", "warn"); return end
F.playIdleLoop(id); statusLbl.Text = "● LOOPING"; statusLbl.TextColor3 = Color3.fromRGB(80,220,120)
F.notify("Idle looping started", "success")
end)
onceBtn.MouseButton1Click:Connect(function()
local id = getIdleId(); if id == "" then F.notify("Enter an idle anim ID first", "warn"); return end
F.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()
F.stopIdle(); statusLbl.Text = "● OFF"; statusLbl.TextColor3 = Color3.fromRGB(160,80,80)
end)
local bpmY = idleY + 98
F.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 = F.makeBox(pg1, UDim2.new(0,44,0,bpmY), UDim2.new(0,68,0,28), "100", tostring(idleBPM))
F.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 = F.makeBox(pg1, UDim2.new(0,164,0,bpmY), UDim2.new(0,68,0,28), "100", tostring(BASE_BPM))
local applyBpmBtn = F.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
F.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 = F.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)
F.regText(bpmValLbl)
local bpmSlider = F.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)
F.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)
F.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
F.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
F.setIdleBPM(idleBPM)
F.notify("BPM settings applied", "info")
end)
local vizY = bpmSliderY + 34
F.makeDivider(pg1, vizY, Color3.fromRGB(0,200,255))
F.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
F.uiCorner(beatDot, UDim.new(1,0))
F.regBg(beatDot)
F.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
F.uiCorner(bar, UDim.new(0,2))
F.regBg(bar)
table.insert(eqBars, bar)
end
F.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
F.uiCorner(bar, UDim.new(0,2))
table.insert(waveBars, bar)
end
local statusY = vizY + 60
F.makeDivider(pg1, statusY, Color3.fromRGB(255,80,180))
local statusTitle = F.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)
F.regText(statusTitle)
local statLbl1 = F.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 = F.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)
F.refreshStatusUI = function()
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
F.refreshStatusUI()
end
-- ============================================================
-- CUTTER TAB
-- Load an animation, scrub it frame by frame in a live preview,
-- then mark a start and stop for each direction straight from
-- the playhead. The timeline stretches to the animation's real
-- length, so every number on it is a real time in the clip.
-- ============================================================
function Cut.fmt(t) return string.format("%.2fs", t or 0) end
function Cut.buildClone()
if not Cut.wm then return end
if Cut.clone then pcall(function() Cut.clone:Destroy() end) end
Cut.clone, Cut.animator, Cut.track = nil, nil, nil
Cut.length, Cut.playhead = 0, 0
local char = character
if not char then return end
local wasArch = char.Archivable
if not wasArch then char.Archivable = true end
local ok, clone = pcall(function() return char:Clone() end)
if not wasArch then char.Archivable = wasArch end
if not ok or not clone then return end
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("Script") or d:IsA("LocalScript") then d:Destroy() end
end
local hum = clone:FindFirstChildOfClass("Humanoid")
if hum then
pcall(function() hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None end)
pcall(function() hum.RequiresNeck = false end)
pcall(function() hum.BreakJointsOnDeath = false end)
pcall(function() hum.EvaluateStateMachine = false end)
Cut.animator = hum:FindFirstChildOfClass("Animator") or Instance.new("Animator", hum)
end
for _, d in ipairs(clone:GetDescendants()) do
if d:IsA("BasePart") then d.CanCollide = false; d.Massless = true; d.Anchored = false end
end
local root = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
if root then root.Anchored = true; clone.PrimaryPart = root end
clone.Name = "CutterClone"
clone.Parent = Cut.wm
pcall(function() clone:PivotTo(CFrame.new(0, 3, 0) * CFrame.Angles(0, math.rad(Cut.yaw), 0)) end)
Cut.clone = clone
end
-- Put the clone on an exact time in the clip and freeze it there.
function Cut.seek(t)
Cut.playhead = math.clamp(t or 0, 0, math.max(Cut.length, 0))
local tr = Cut.track
if tr then
if not tr.IsPlaying then tr:Play(0) end
tr:AdjustSpeed(0)
tr.TimePosition = math.min(Cut.playhead, math.max(Cut.length - 0.001, 0))
end
Cut.drawHead()
end
function Cut.drawHead()
if Cut.head and Cut.length > 0 then
Cut.head.Position = UDim2.new(Cut.playhead / Cut.length, -1, 0, -4)
end
if Cut.timeLbl then
Cut.timeLbl.Text = Cut.fmt(Cut.playhead) .. " / " .. Cut.fmt(Cut.length)
end
if Cut.frameLbl then
Cut.frameLbl.Text = string.format("frame %d / %d",
math.floor(Cut.playhead * Cut.fps + 0.5), math.floor(Cut.length * Cut.fps + 0.5))
end
end
-- Coloured regions on the timeline for each direction's slice.
function Cut.drawSlices()
if Cut.length <= 0 then
for _, f in pairs(Cut.sliceFrames) do f.Visible = false end
return
end
for i, dir in ipairs(ARROW_ORDER) do
local f = Cut.sliceFrames[dir]
local s = cutterSlices[dir]
local sid = (s and s.id ~= "" and s.id) or cutterAnimId
if f and s and sid ~= Cut.editId then
f.Visible = false
elseif f and s then
local a = math.clamp(s.start / Cut.length, 0, 1)
local b = math.clamp(s.stop / Cut.length, 0, 1)
if b < a then a, b = b, a end
f.Visible = true
f.Position = UDim2.new(a, 0, (i - 1) / 4, 0)
f.Size = UDim2.new(math.max(b - a, 0.004), 0, 0.25, 0)
f.BackgroundTransparency = (dir == Cut.selDir) and 0.05 or 0.45
end
if Cut.startBoxes[dir] then Cut.startBoxes[dir].Text = string.format("%.2f", s.start) end
if Cut.stopBoxes[dir] then Cut.stopBoxes[dir].Text = string.format("%.2f", s.stop) end
end
end
function Cut.loadAnim(id)
if not id or id == "" then F.notify("Enter an animation ID", "warn"); return end
if not Cut.clone then Cut.buildClone() end
if not Cut.animator then F.notify("Preview rig unavailable", "error"); return end
if Cut.track then pcall(function() Cut.track:Stop(0) end) end
local ok, tr = pcall(function()
local a = Instance.new("Animation")
a.AnimationId = "rbxassetid://" .. id
local t = Cut.animator:LoadAnimation(a)
t.Priority = Enum.AnimationPriority.Action
t.Looped = true
return t
end)
if not ok or not tr then F.notify("Failed to load animation", "error"); return end
Cut.track = tr
tr:Play(0); tr:AdjustSpeed(0)
if Cut.statusLbl then Cut.statusLbl.Text = "loading..." end
-- Length is 0 until the asset streams in, so poll for it
task.spawn(function()
local deadline = os.clock() + 4
while tr == Cut.track and tr.Length <= 0 and os.clock() < deadline do
task.wait(0.05)
end
if tr ~= Cut.track then return end
if tr.Length <= 0 then
if Cut.statusLbl then Cut.statusLbl.Text = "couldn't read length — asset private or blocked?" end
F.notify("Couldn't read the animation length", "error")
return
end
Cut.length = tr.Length
-- clamp only the slices cut from THIS animation into its length
for _, dir in ipairs(ARROW_ORDER) do
local s = cutterSlices[dir]
local sid = (s.id ~= "" and s.id) or cutterAnimId
if sid == id then
s.start = math.clamp(s.start, 0, Cut.length)
s.stop = math.clamp(s.stop, 0, Cut.length)
end
end
if Cut.lenLbl then Cut.lenLbl.Text = "LENGTH " .. Cut.fmt(Cut.length) end
if Cut.statusLbl then Cut.statusLbl.Text = "ready — drag the timeline to scrub" end
Cut.seek(0)
Cut.drawSlices()
-- warm the gameplay track for this source so the first press is instant
F.cutterTrackFor(id)
F.notify("Loaded — " .. Cut.fmt(Cut.length), "success")
end)
end
-- Show a note's animation in the preview. Only reloads when the animation
-- actually differs, so flicking between notes that share one is instant.
function Cut.editDir(dir, force)
Cut.selDir = dir
-- rig gone (respawn, or the tab was opened before the character loaded)
if Cut.wm and (not Cut.clone or not Cut.clone.Parent or not Cut.animator) then
Cut.buildClone()
force = true
end
local id = F.cutterSliceId(dir)
if not id or id == "" then
if Cut.statusLbl then Cut.statusLbl.Text = dir .. " has no animation yet — type an ID for it" end
return
end
if force or id ~= Cut.editId or not Cut.track then
Cut.editId = id
Cut.setPlaying(false)
Cut.loadAnim(id)
else
Cut.drawSlices()
end
end
function Cut.setPlaying(state)
Cut.playing = state and Cut.length > 0
if Cut.playBtn then
Cut.playBtn.Text = Cut.playing and "PAUSE" or "PLAY"
Cut.playBtn.BackgroundColor3 = Cut.playing and Color3.fromRGB(255,160,40) or Color3.fromRGB(60,190,120)
end
end
function Cut.startLoop()
if Cut.conn then return end
Cut.conn = RunService.RenderStepped:Connect(function(dt)
if not Cut.vp or not Cut.vp.Parent then return end
if Cut.playing and Cut.length > 0 then
local lo, hi = 0, Cut.length
if Cut.loopSlice then
local s = cutterSlices[Cut.selDir]
lo, hi = math.min(s.start, s.stop), math.max(s.start, s.stop)
if hi - lo < 0.01 then lo, hi = 0, Cut.length end
end
local t = Cut.playhead + dt
if t > hi or t < lo then t = lo end
Cut.seek(t)
end
end)
end
function F.buildTab_Cutter(pages)
local pg = pages["✂ CUTTER"]
local C = Color3.fromRGB
local title = F.makeLabel(pg, "EMOTE CUTTER", UDim2.new(0,8,0,6),
UDim2.new(0,200,0,22), C(255,80,180), Enum.TextXAlignment.Left, 14)
title.Font = Enum.Font.GothamBlack
F.regText(title)
F.makeLabel(pg, "Give each note its own animation, scrub it, and cut its pose.",
UDim2.new(0,8,0,28), UDim2.new(1,-150,0,16), C(130,130,150), Enum.TextXAlignment.Left, 9)
local enBtn = F.makeBtn(pg, cutterEnabled and "CUTTER ON" or "CUTTER OFF",
UDim2.new(1,-138,0,8), UDim2.new(0,130,0,30),
cutterEnabled and THEMES[currentThemeName].accent or C(35,35,55), nil, 10)
enBtn.MouseButton1Click:Connect(function()
cutterEnabled = not cutterEnabled
enBtn.Text = cutterEnabled and "CUTTER ON" or "CUTTER OFF"
enBtn.BackgroundColor3 = cutterEnabled and THEMES[currentThemeName].accent or C(35,35,55)
if cutterEnabled then F.cutterLoadTrack() else F.cutterStopSlice() end
F.notify(cutterEnabled and "Cutter on — notes play your slices" or "Cutter off — normal binds", "info")
end)
-- ── default animation, for any note left blank ──
local defBox = F.makeBox(pg, UDim2.new(0,8,0,52), UDim2.new(1,-114,0,30),
"Default animation ID (any note left blank uses this)", cutterAnimId)
local defLoad = F.makeBtn(pg, "LOAD", UDim2.new(1,-100,0,52), UDim2.new(0,92,0,30),
C(70,120,255), nil, 11)
local function commitDefault()
cutterAnimId = (defBox.Text:gsub("%D",""))
defBox.Text = cutterAnimId
if cutterAnimId ~= "" then F.cutterTrackFor(cutterAnimId) end
-- only reload if the note on screen actually uses the default
local s = cutterSlices[Cut.selDir]
if s and (s.id == nil or s.id == "") then Cut.editDir(Cut.selDir, true) end
if Cut.refreshIdBoxes then Cut.refreshIdBoxes() end
end
defLoad.MouseButton1Click:Connect(commitDefault)
defBox.FocusLost:Connect(function() commitDefault() end)
-- ── preview viewport ──
local vp = Instance.new("ViewportFrame")
vp.Size = UDim2.new(1,-16,0,230); vp.Position = UDim2.new(0,8,0,90)
vp.BackgroundColor3 = C(8,8,12)
vp.Ambient = C(150,150,160); vp.LightColor = C(255,255,255)
vp.LightDirection = Vector3.new(-0.5,-1,-0.4)
vp.Parent = pg
F.uiCorner(vp, UDim.new(0,10))
local vpStroke = F.uiStroke(vp, THEMES[currentThemeName].accent, 1.5)
F.regStroke(vpStroke)
Cut.vp = vp
local wm = Instance.new("WorldModel"); wm.Parent = vp
local cam = Instance.new("Camera"); cam.FieldOfView = 50; cam.Parent = vp
cam.CFrame = CFrame.new(Vector3.new(0, 3.3, 7.2), Vector3.new(0, 2.7, 0))
vp.CurrentCamera = cam
Cut.wm, Cut.cam = wm, cam
local floor = Instance.new("Part")
floor.Size = Vector3.new(10,1,10); floor.Anchored = true; floor.CanCollide = false
floor.Color = C(22,20,30); floor.Material = Enum.Material.SmoothPlastic
floor.CFrame = CFrame.new(0,-0.5,0); floor.Parent = wm
Cut.lenLbl = F.makeLabel(vp, "LENGTH --", UDim2.new(0,10,0,8), UDim2.new(0,220,0,16),
C(235,235,245), Enum.TextXAlignment.Left, 10)
Cut.lenLbl.Font = Enum.Font.GothamBold
Cut.frameLbl = F.makeLabel(vp, "frame 0 / 0", UDim2.new(1,-150,0,8), UDim2.new(0,140,0,16),
C(235,235,245), Enum.TextXAlignment.Right, 10)
Cut.frameLbl.Font = Enum.Font.Code
Cut.statusLbl = F.makeLabel(vp, "type an animation ID to start", UDim2.new(0,10,1,-24),
UDim2.new(1,-20,0,16), C(150,150,170), Enum.TextXAlignment.Left, 9)
-- which note the preview is showing, big in the corner
Cut.noteLbl = F.makeLabel(vp, "", UDim2.new(0,10,0,26), UDim2.new(0,220,0,20),
C(255,255,255), Enum.TextXAlignment.Left, 13)
Cut.noteLbl.Font = Enum.Font.GothamBlack
F.makeLabel(pg, "ROTATE", UDim2.new(0,8,0,326), UDim2.new(0,60,0,16),
C(160,160,160), Enum.TextXAlignment.Left, 9)
F.makeSlider(pg, UDim2.new(0,72,0,329), UDim2.new(1,-160,0,10), -180, 180, 0, function(v)
Cut.yaw = v
if Cut.clone then
pcall(function() Cut.clone:PivotTo(CFrame.new(0,3,0) * CFrame.Angles(0, math.rad(v), 0)) end)
end
end)
local rebuild = F.makeBtn(pg, "RIG", UDim2.new(1,-80,0,322), UDim2.new(0,72,0,22), C(45,40,60), nil, 9)
rebuild.MouseButton1Click:Connect(function()
Cut.buildClone()
Cut.editDir(Cut.selDir, true)
end)
-- ── timeline ──
local tlY = 356
Cut.timeLbl = F.makeLabel(pg, "0.00s / 0.00s", UDim2.new(0,8,0,tlY), UDim2.new(0,200,0,18),
C(235,235,245), Enum.TextXAlignment.Left, 12)
Cut.timeLbl.Font = Enum.Font.Code
local bar = Instance.new("Frame")
bar.Size = UDim2.new(1,-16,0,64); bar.Position = UDim2.new(0,8,0,tlY+22)
bar.BackgroundColor3 = C(12,11,18); bar.BorderSizePixel = 0
bar.Active = true
bar.Parent = pg
F.uiCorner(bar, UDim.new(0,6))
F.uiStroke(bar, C(255,255,255), 1).Transparency = 0.8
Cut.bar = bar
Cut.sliceFrames = {}
for i, dir in ipairs(ARROW_ORDER) do
if i > 1 then
local sep = Instance.new("Frame")
sep.Size = UDim2.new(1,0,0,1); sep.Position = UDim2.new(0,0,(i-1)/4,0)
sep.BackgroundColor3 = C(255,255,255); sep.BackgroundTransparency = 0.9
sep.BorderSizePixel = 0; sep.Parent = bar
end
local f = Instance.new("Frame")
f.BackgroundColor3 = ARROW_COLORS[dir].hit
f.BorderSizePixel = 0; f.Visible = false; f.Parent = bar
F.uiCorner(f, UDim.new(0,3))
Cut.sliceFrames[dir] = f
end
for q = 1, 3 do
local tk = Instance.new("Frame")
tk.Size = UDim2.new(0,1,1,0); tk.Position = UDim2.new(q/4,0,0,0)
tk.BackgroundColor3 = C(255,255,255); tk.BackgroundTransparency = 0.85
tk.BorderSizePixel = 0; tk.Parent = bar
end
local head = Instance.new("Frame")
head.Size = UDim2.new(0,2,1,8); head.Position = UDim2.new(0,-1,0,-4)
head.BackgroundColor3 = C(255,255,255); head.BorderSizePixel = 0
head.ZIndex = 5; head.Parent = bar
local knob = Instance.new("Frame")
knob.Size = UDim2.new(0,12,0,12); knob.AnchorPoint = Vector2.new(0.5,1)
knob.Position = UDim2.new(0.5,0,0,0); knob.BackgroundColor3 = C(255,255,255)
knob.BorderSizePixel = 0; knob.ZIndex = 5; knob.Parent = head
F.uiCorner(knob, UDim.new(1,0))
Cut.head = head
local scrubbing = false
local function scrubTo(x)
if Cut.length <= 0 then return end
local w = bar.AbsoluteSize.X
if w <= 0 then return end
local a = math.clamp((x - bar.AbsolutePosition.X) / w, 0, 1)
Cut.setPlaying(false)
Cut.seek(a * Cut.length)
end
bar.InputBegan:Connect(function(inp)
if inp.UserInputType == Enum.UserInputType.MouseButton1
or inp.UserInputType == Enum.UserInputType.Touch then
scrubbing = true
scrubTo(inp.Position.X)
end
end)
UserInputService.InputChanged:Connect(function(inp)
if scrubbing and (inp.UserInputType == Enum.UserInputType.MouseMovement
or inp.UserInputType == Enum.UserInputType.Touch) then
scrubTo(inp.Position.X)
end
end)
UserInputService.InputEnded:Connect(function(inp)
if inp.UserInputType == Enum.UserInputType.MouseButton1
or inp.UserInputType == Enum.UserInputType.Touch then
scrubbing = false
end
end)
-- ── transport ──
local trY = tlY + 94
local function tbtn(txt, x, w, col, fn)
local b = F.makeBtn(pg, txt, UDim2.new(0,x,0,trY), UDim2.new(0,w,0,28), col, nil, 10)
b.MouseButton1Click:Connect(fn)
return b
end
tbtn("|<", 8, 44, C(45,40,60), function() Cut.setPlaying(false); Cut.seek(0) end)
tbtn("<", 56, 44, C(45,40,60), function() Cut.setPlaying(false); Cut.seek(Cut.playhead - 1 / Cut.fps) end)
Cut.playBtn = tbtn("PLAY", 104, 96, C(60,190,120), function() Cut.setPlaying(not Cut.playing) end)
tbtn(">", 204, 44, C(45,40,60), function() Cut.setPlaying(false); Cut.seek(Cut.playhead + 1 / Cut.fps) end)
tbtn(">|", 252, 44, C(45,40,60), function() Cut.setPlaying(false); Cut.seek(Cut.length) end)
local loopBtn
loopBtn = tbtn("LOOP: ALL", 300, 96, C(45,40,60), function()
Cut.loopSlice = not Cut.loopSlice
loopBtn.Text = Cut.loopSlice and "LOOP: SLICE" or "LOOP: ALL"
loopBtn.BackgroundColor3 = Cut.loopSlice and C(70,120,255) or C(45,40,60)
end)
-- ── notes ──
local seY = trY + 40
F.makeDivider(pg, seY, THEMES[currentThemeName].accent)
local seTitle = F.makeLabel(pg, "NOTES", UDim2.new(0,8,0,seY+6), UDim2.new(0,120,0,18),
C(255,80,180), Enum.TextXAlignment.Left, 11)
seTitle.Font = Enum.Font.GothamBold
F.regText(seTitle)
F.makeLabel(pg, "Type an ID into a note to load it. Click a note to see its animation.",
UDim2.new(0,8,0,seY+24), UDim2.new(1,-16,0,14), C(130,130,150), Enum.TextXAlignment.Left, 9)
Cut.dirBtns = {}
local rowFrames = {}
local function selectDir(dir)
for d, b in pairs(Cut.dirBtns) do
b.BackgroundColor3 = (d == dir) and ARROW_COLORS[d].hit or C(35,35,55)
b.TextColor3 = (d == dir) and C(15,15,20) or C(238,238,248)
end
for d, rf in pairs(rowFrames) do
rf.BackgroundTransparency = (d == dir) and 0.1 or 0.6
end
if Cut.noteLbl then
Cut.noteLbl.Text = ARROW_SYMBOLS[dir] .. " " .. dir
Cut.noteLbl.TextColor3 = ARROW_COLORS[dir].hit
end
Cut.editDir(dir)
Cut.drawSlices()
end
Cut.selectDir = selectDir
for i, dir in ipairs(ARROW_ORDER) do
local b = F.makeBtn(pg, ARROW_SYMBOLS[dir] .. " " .. dir,
UDim2.new(0, 8 + (i-1) * 100, 0, seY + 44), UDim2.new(0, 96, 0, 30), C(35,35,55), nil, 10)
Cut.dirBtns[dir] = b
b.MouseButton1Click:Connect(function() selectDir(dir) end)
end
local mY = seY + 82
local markStart = F.makeBtn(pg, "MARK START", UDim2.new(0,8,0,mY), UDim2.new(0,126,0,28), C(60,190,120), nil, 10)
local markStop = F.makeBtn(pg, "MARK STOP", UDim2.new(0,140,0,mY), UDim2.new(0,126,0,28), C(200,70,70), nil, 10)
local playSlice = F.makeBtn(pg, "PLAY SLICE", UDim2.new(0,272,0,mY), UDim2.new(0,124,0,28), C(70,120,255), nil, 10)
markStart.MouseButton1Click:Connect(function()
if Cut.length <= 0 then F.notify("Load an animation first", "warn"); return end
cutterSlices[Cut.selDir].start = Cut.playhead
Cut.drawSlices()
end)
markStop.MouseButton1Click:Connect(function()
if Cut.length <= 0 then F.notify("Load an animation first", "warn"); return end
cutterSlices[Cut.selDir].stop = Cut.playhead
Cut.drawSlices()
end)
playSlice.MouseButton1Click:Connect(function()
if Cut.length <= 0 then return end
Cut.loopSlice = true
loopBtn.Text = "LOOP: SLICE"; loopBtn.BackgroundColor3 = C(70,120,255)
local s = cutterSlices[Cut.selDir]
Cut.seek(math.min(s.start, s.stop))
Cut.setPlaying(true)
end)
-- one card per note: its own animation ID, then its in/out times
local nY = mY + 38
local CARD = 66
Cut.startBoxes, Cut.stopBoxes, Cut.idBoxes = {}, {}, {}
for i, dir in ipairs(ARROW_ORDER) do
local ry = nY + (i-1) * CARD
local d = dir
local card = Instance.new("Frame")
card.Size = UDim2.new(1,-16,0,CARD - 6); card.Position = UDim2.new(0,8,0,ry)
card.BackgroundColor3 = C(24,22,33); card.BackgroundTransparency = 0.6
card.BorderSizePixel = 0; card.Parent = pg
F.uiCorner(card, UDim.new(0,7))
local cs = F.uiStroke(card, ARROW_COLORS[dir].hit, 1)
cs.Transparency = 0.6
rowFrames[dir] = card
local lbl = F.makeLabel(card, ARROW_SYMBOLS[dir] .. " " .. dir, UDim2.new(0,8,0,4),
UDim2.new(0,70,0,24), ARROW_COLORS[dir].hit, Enum.TextXAlignment.Left, 11)
lbl.Font = Enum.Font.GothamBold
-- this note's own animation
local idBox = F.makeBox(card, UDim2.new(0,80,0,4), UDim2.new(1,-168,0,24),
"uses default animation", cutterSlices[dir].id or "")
idBox.TextSize = 10
Cut.idBoxes[dir] = idBox
local function commitId()
local clean = (idBox.Text:gsub("%D",""))
local changed = clean ~= (cutterSlices[d].id or "")
cutterSlices[d].id = clean
idBox.Text = clean
if clean ~= "" then F.cutterTrackFor(clean) end
-- typing an ID loads it straight into the preview
if changed then
Cut.noteLbl.Text = ARROW_SYMBOLS[d] .. " " .. d
selectDir(d)
Cut.editDir(d, true)
end
end
idBox.FocusLost:Connect(function() commitId() end)
local view = F.makeBtn(card, "VIEW", UDim2.new(1,-82,0,4), UDim2.new(0,74,0,24),
C(70,120,255), nil, 9)
view.MouseButton1Click:Connect(function() commitId(); selectDir(d) end)
-- in / out
F.makeLabel(card, "IN", UDim2.new(0,8,0,32), UDim2.new(0,20,0,24), C(140,140,160), Enum.TextXAlignment.Left, 9)
local sb = F.makeBox(card, UDim2.new(0,28,0,32), UDim2.new(0,70,0,24), "0.00",
string.format("%.2f", cutterSlices[dir].start))
F.makeLabel(card, "OUT", UDim2.new(0,104,0,32), UDim2.new(0,28,0,24), C(140,140,160), Enum.TextXAlignment.Left, 9)
local eb = F.makeBox(card, UDim2.new(0,134,0,32), UDim2.new(0,70,0,24), "0.00",
string.format("%.2f", cutterSlices[dir].stop))
Cut.startBoxes[dir], Cut.stopBoxes[dir] = sb, eb
sb.FocusLost:Connect(function()
local n = tonumber(sb.Text)
if n then cutterSlices[d].start = math.max(0, n) end
Cut.drawSlices()
end)
eb.FocusLost:Connect(function()
local n = tonumber(eb.Text)
if n then cutterSlices[d].stop = math.max(0, n) end
Cut.drawSlices()
end)
local goIn = F.makeBtn(card, "GO IN", UDim2.new(0,212,0,32), UDim2.new(0,64,0,24), C(45,40,60), nil, 9)
local goOut = F.makeBtn(card, "GO OUT", UDim2.new(0,280,0,32), UDim2.new(0,64,0,24), C(45,40,60), nil, 9)
goIn.MouseButton1Click:Connect(function()
selectDir(d); Cut.setPlaying(false); Cut.seek(cutterSlices[d].start)
end)
goOut.MouseButton1Click:Connect(function()
selectDir(d); Cut.setPlaying(false); Cut.seek(cutterSlices[d].stop)
end)
end
-- placeholder text shows what a blank note will actually use
Cut.refreshIdBoxes = function()
for _, dir in ipairs(ARROW_ORDER) do
local b = Cut.idBoxes[dir]
if b then
b.PlaceholderText = (cutterAnimId ~= "")
and ("default: " .. cutterAnimId) or "type an animation ID"
end
end
end
Cut.refreshIdBoxes()
-- ── quick split ──
local qY = nY + 4 * CARD + 4
local split = F.makeBtn(pg, "SPLIT THIS ANIM INTO 4", UDim2.new(0,8,0,qY), UDim2.new(0,250,0,30), C(70,120,255), nil, 10)
split.MouseButton1Click:Connect(function()
if Cut.length <= 0 then F.notify("Load an animation first", "warn"); return end
-- splits the animation on screen across every note that uses it
local users = {}
for _, dir in ipairs(ARROW_ORDER) do
if F.cutterSliceId(dir) == Cut.editId then table.insert(users, dir) end
end
if #users == 0 then return end
local q = Cut.length / #users
for n, dir in ipairs(users) do
cutterSlices[dir].start = (n - 1) * q
cutterSlices[dir].stop = n * q
end
Cut.drawSlices()
F.notify("Split across " .. #users .. " note" .. (#users == 1 and "" or "s"), "success")
end)
local clear = F.makeBtn(pg, "RESET TIMES", UDim2.new(0,266,0,qY), UDim2.new(0,130,0,30), C(170,35,35), nil, 10)
clear.MouseButton1Click:Connect(function()
for _, dir in ipairs(ARROW_ORDER) do cutterSlices[dir].start, cutterSlices[dir].stop = 0, 0 end
Cut.drawSlices()
end)
F.makeLabel(pg, "Your note keys play these slices while the cutter is ON.",
UDim2.new(0,8,0,qY+38), UDim2.new(1,-16,0,16), C(120,120,140), Enum.TextXAlignment.Left, 9)
-- ── response ──
local rY = qY + 62
F.makeDivider(pg, rY, THEMES[currentThemeName].accent)
local rTitle = F.makeLabel(pg, "RESPONSE", UDim2.new(0,8,0,rY+6), UDim2.new(0,120,0,18),
C(255,80,180), Enum.TextXAlignment.Left, 11)
rTitle.Font = Enum.Font.GothamBold
F.regText(rTitle)
F.makeLabel(pg, "How fast a slice lands when you press. Lower is snappier for fast playing.",
UDim2.new(0,8,0,rY+24), UDim2.new(1,-16,0,14), C(130,130,150), Enum.TextXAlignment.Left, 9)
local snapBtn = F.makeBtn(pg, Cut.snap and "SNAP: ON" or "SNAP: OFF",
UDim2.new(0,8,0,rY+44), UDim2.new(0,126,0,26),
Cut.snap and C(255,160,40) or C(35,35,55), nil, 9)
snapBtn.MouseButton1Click:Connect(function()
Cut.snap = not Cut.snap
snapBtn.Text = Cut.snap and "SNAP: ON" or "SNAP: OFF"
snapBtn.BackgroundColor3 = Cut.snap and C(255,160,40) or C(35,35,55)
end)
F.makeLabel(pg, "Skip the motion and land on the pose instantly.",
UDim2.new(0,142,0,rY+44), UDim2.new(1,-150,0,26), C(120,120,140), Enum.TextXAlignment.Left, 9)
F.makeLabel(pg, "REACH TIME", UDim2.new(0,8,0,rY+78), UDim2.new(0,200,0,18),
C(160,160,160), Enum.TextXAlignment.Left, 10)
local reachLbl = F.makeLabel(pg, string.format("%.2fs", Cut.reach),
UDim2.new(0,306,0,rY+78), UDim2.new(0,90,0,18), C(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(reachLbl)
F.makeSlider(pg, UDim2.new(0,8,0,rY+98), UDim2.new(1,-16,0,12), 0.02, 0.6, Cut.reach, function(v)
Cut.reach = v
reachLbl.Text = string.format("%.2fs", v)
end)
-- ── reverse ──
local rvY = rY + 124
F.makeLabel(pg, "PLAY IT BACK", UDim2.new(0,8,0,rvY), UDim2.new(0,200,0,18),
C(160,160,160), Enum.TextXAlignment.Left, 10)
local rvHelp = {
OFF = "Stays on the pose (Pose Hold still applies).",
AUTO = "Lands, pauses, then goes back to its start on its own.",
RELEASE = "Holds while you hold the key, goes back when you let go.",
}
local rvBtn = F.makeBtn(pg, "", UDim2.new(0,8,0,rvY+20), UDim2.new(0,160,0,26), C(35,35,55), nil, 10)
local rvNote = F.makeLabel(pg, "", UDim2.new(0,176,0,rvY+20), UDim2.new(1,-184,0,26),
C(120,120,140), Enum.TextXAlignment.Left, 9)
rvNote.TextWrapped = true
local function syncRv()
rvBtn.Text = "REVERSE: " .. (Cut.reverse == "RELEASE" and "ON RELEASE" or Cut.reverse)
rvBtn.BackgroundColor3 = (Cut.reverse == "OFF") and C(35,35,55) or C(255,160,40)
rvNote.Text = rvHelp[Cut.reverse] or ""
end
rvBtn.MouseButton1Click:Connect(function()
local order = { "OFF", "AUTO", "RELEASE" }
Cut.reverse = order[((table.find(order, Cut.reverse) or 1) % #order) + 1]
syncRv()
end)
syncRv()
local rvRows = {
{ key = "returnPause", label = "PAUSE ON THE POSE (AUTO)", min = 0, max = 1.5, fmt = "%.2fs" },
{ key = "returnTime", label = "TIME TO GO BACK", min = 0.03, max = 1.5, fmt = "%.2fs" },
}
for i, s in ipairs(rvRows) do
local ry = rvY + 54 + (i-1) * 42
F.makeLabel(pg, s.label, UDim2.new(0,8,0,ry), UDim2.new(0,240,0,18), C(160,160,160), Enum.TextXAlignment.Left, 10)
local v = F.makeLabel(pg, string.format(s.fmt, Cut[s.key]), UDim2.new(0,306,0,ry), UDim2.new(0,90,0,18),
C(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(v)
local ck, cf = s.key, s.fmt
F.makeSlider(pg, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12), s.min, s.max, Cut[s.key], function(val)
Cut[ck] = val
v.Text = string.format(cf, val)
end)
end
local haY = rvY + 54 + #rvRows * 42
local haBtn = F.makeBtn(pg, "", UDim2.new(0,8,0,haY), UDim2.new(0,190,0,26), C(35,35,55), nil, 9)
local haNote = F.makeLabel(pg, "", UDim2.new(0,206,0,haY), UDim2.new(1,-214,0,26),
C(120,120,140), Enum.TextXAlignment.Left, 9)
haNote.TextWrapped = true
local function syncHa()
haBtn.Text = Cut.holdAfterReturn and "AFTER GOING BACK: HOLD" or "AFTER GOING BACK: IDLE"
haBtn.BackgroundColor3 = Cut.holdAfterReturn and C(255,160,40) or C(35,35,55)
haNote.Text = Cut.holdAfterReturn and "Stays on the start frame. Pose Hold sets how long."
or "Blends out to idle once it's back."
end
haBtn.MouseButton1Click:Connect(function()
Cut.holdAfterReturn = not Cut.holdAfterReturn
syncHa()
end)
syncHa()
Cut.startLoop()
task.defer(function()
Cut.buildClone()
selectDir(Cut.selDir or "LEFT")
end)
end
function F.buildTab_Cam(pages)
local pgCam = pages["🎥 CAM"]
local camTitle = F.makeLabel(pgCam, "🎥 FNF CINEMATIC CAMERA", UDim2.new(0,8,0,6),
UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
F.regText(camTitle)
F.makeLabel(pgCam, "Triangle view between you & opponent. Pans to whoever is singing. Shake + letterbox on aggressive exchanges.",
UDim2.new(0,8,0,26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
F.makeDivider(pgCam, 58, Color3.fromRGB(0,200,255))
local camBtn = F.makeBtn(pgCam, "📴 CAMERA OFF", UDim2.new(0,8,0,66), UDim2.new(1,-16,0,32), Color3.fromRGB(35,35,55), nil, 11)
camBtn.MouseButton1Click:Connect(function()
local newState = not FNFCamera.enabled
FNFCamera:SetEnabled(newState)
camBtn.Text = newState and "🎥 CAMERA ON" or "📴 CAMERA OFF"
camBtn.BackgroundColor3 = newState and THEMES[currentThemeName].accent or Color3.fromRGB(35,35,55)
F.notify(newState and "Cinematic camera enabled" or "Cinematic camera disabled", "info")
end)
local camStatusLbl = F.makeLabel(pgCam, "Focus: CENTER | Intensity: 0%", UDim2.new(0,8,0,106),
UDim2.new(1,-16,0,18), Color3.fromRGB(160,160,180), Enum.TextXAlignment.Left, 10)
FNFCamera.statusCb = function(focus, intensity01)
camStatusLbl.Text = string.format("Focus: %s | Intensity: %d%%", focus, math.floor(intensity01*100))
end
F.makeLabel(pgCam, "SENSITIVITY (how fast intensity builds)", UDim2.new(0,8,0,132), UDim2.new(1,-16,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
F.makeSlider(pgCam, UDim2.new(0,8,0,154), UDim2.new(1,-16,0,14), 0.5, 2.5, 1, function(v) FNFCamera:SetSensitivity(v) end)
F.makeLabel(pgCam, "MAX SHAKE", UDim2.new(0,8,0,178), UDim2.new(0,140,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
F.makeSlider(pgCam, UDim2.new(0,8,0,200), UDim2.new(1,-16,0,14), 0, 1, 1, function(v) FNFCamera:SetShakeScale(v) end)
F.makeLabel(pgCam, "MAX ZOOM", UDim2.new(0,8,0,224), UDim2.new(0,140,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
F.makeSlider(pgCam, UDim2.new(0,8,0,246), UDim2.new(1,-16,0,14), 0, 1, 1, function(v) FNFCamera:SetZoomScale(v) end)
-- ── CAMERA POSITION CONTROLS ──
F.makeDivider(pgCam, 274, Color3.fromRGB(0,200,255))
local posTitle = F.makeLabel(pgCam, "📐 CAMERA POSITION", UDim2.new(0,8,0,280),
UDim2.new(1,-16,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
F.regText(posTitle)
local flipBtn = F.makeBtn(pgCam, "↔ LEFT SIDE", UDim2.new(0,8,0,304), UDim2.new(1,-16,0,30), Color3.fromRGB(70,120,255), nil, 11)
flipBtn.MouseButton1Click:Connect(function()
FNFCamera.camFlipped = not FNFCamera.camFlipped
flipBtn.Text = FNFCamera.camFlipped and "↔ RIGHT SIDE" or "↔ LEFT SIDE"
F.notify(FNFCamera.camFlipped and "Camera flipped to right side" or "Camera flipped to left side", "info")
end)
F.makeLabel(pgCam, "DISTANCE", UDim2.new(0,8,0,342), UDim2.new(0,100,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local distLbl = F.makeLabel(pgCam, string.format("%.0f", FNFCamera.camDistance),
UDim2.new(0,340,0,342), UDim2.new(0,70,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(distLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,364), UDim2.new(1,-16,0,14), 5, 30, FNFCamera.camDistance, function(v)
FNFCamera.camDistance = v
distLbl.Text = string.format("%.0f", v)
end)
F.makeLabel(pgCam, "HEIGHT", UDim2.new(0,8,0,388), UDim2.new(0,100,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local heightLbl = F.makeLabel(pgCam, string.format("%.1f", FNFCamera.camHeight),
UDim2.new(0,340,0,388), UDim2.new(0,70,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(heightLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,410), UDim2.new(1,-16,0,14), 0, 15, FNFCamera.camHeight, function(v)
FNFCamera.camHeight = v
heightLbl.Text = string.format("%.1f", v)
end)
F.makeLabel(pgCam, "HORIZONTAL", UDim2.new(0,8,0,434), UDim2.new(0,110,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local horzLbl = F.makeLabel(pgCam, string.format("%.1f", FNFCamera.camHorizontal),
UDim2.new(0,340,0,434), UDim2.new(0,70,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(horzLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,456), UDim2.new(1,-16,0,14), -10, 10, FNFCamera.camHorizontal, function(v)
FNFCamera.camHorizontal = v
horzLbl.Text = string.format("%.1f", v)
end)
F.makeLabel(pgCam, "ORBIT ANGLE", UDim2.new(0,8,0,480), UDim2.new(0,110,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local angleLbl = F.makeLabel(pgCam, string.format("%.0f°", FNFCamera.camAngle),
UDim2.new(0,340,0,480), UDim2.new(0,70,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(angleLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,502), UDim2.new(1,-16,0,14), -90, 90, FNFCamera.camAngle, function(v)
FNFCamera.camAngle = v
angleLbl.Text = string.format("%.0f°", v)
end)
F.makeLabel(pgCam, "BASE FOV", UDim2.new(0,8,0,526), UDim2.new(0,100,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local fovLbl = F.makeLabel(pgCam, string.format("%.0f", CAM_CFG.BaseFOV),
UDim2.new(0,340,0,526), UDim2.new(0,70,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(fovLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,548), UDim2.new(1,-16,0,14), 40, 110, CAM_CFG.BaseFOV, function(v)
CAM_CFG.BaseFOV = v
fovLbl.Text = string.format("%.0f", v)
end)
local resetCamBtn = F.makeBtn(pgCam, "↺ RESET POSITION", UDim2.new(0,8,0,572), UDim2.new(1,-16,0,28), Color3.fromRGB(170,35,35), nil, 10)
resetCamBtn.MouseButton1Click:Connect(function()
FNFCamera.camDistance = 14; FNFCamera.camHeight = 5
FNFCamera.camHorizontal = 0; FNFCamera.camAngle = 0
FNFCamera.camFlipped = false; CAM_CFG.BaseFOV = 70
distLbl.Text = "14"; heightLbl.Text = "5.0"
horzLbl.Text = "0.0"; angleLbl.Text = "0°"; fovLbl.Text = "70"
flipBtn.Text = "↔ LEFT SIDE"
F.notify("Camera position reset", "info")
end)
-- ── OPPONENT SEARCH ──
F.makeDivider(pgCam, 612, Color3.fromRGB(255,80,180))
local oppTitle = F.makeLabel(pgCam, "🎤 SET OPPONENT", UDim2.new(0,8,0,618),
UDim2.new(1,-16,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
F.regText(oppTitle)
F.makeLabel(pgCam, "Pick any player — camera will triangle between you and them.",
UDim2.new(0,8,0,636), UDim2.new(1,-16,0,16), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local camOppLbl = F.makeLabel(pgCam, "Opponent: none", UDim2.new(0,8,0,654),
UDim2.new(1,-16,0,16), Color3.fromRGB(0,200,255), Enum.TextXAlignment.Left, 10)
local oppSearchBox = F.makeBox(pgCam, UDim2.new(0,8,0,674), UDim2.new(1,-16,0,28), "Type player name...")
local oppResultsList = Instance.new("ScrollingFrame")
oppResultsList.Size = UDim2.new(1,-16,0,90); oppResultsList.Position = UDim2.new(0,8,0,706)
oppResultsList.BackgroundColor3 = Color3.fromRGB(18,18,26); oppResultsList.BorderSizePixel = 0
oppResultsList.ScrollBarThickness = 3; oppResultsList.CanvasSize = UDim2.new(0,0,0,0)
oppResultsList.AutomaticCanvasSize = Enum.AutomaticSize.Y
oppResultsList.Parent = pgCam; F.uiCorner(oppResultsList, UDim.new(0,6))
local oppResultsLayout = Instance.new("UIListLayout")
oppResultsLayout.Padding = UDim.new(0,2); oppResultsLayout.Parent = oppResultsList
local function camPickPlayer(plr)
if not plr or not plr.Character then F.notify("Player has no character yet", "warn"); return end
F.pickOpponent(plr)
camOppLbl.Text = "Opponent: " .. plr.Name
oppSearchBox.Text = ""
for _, c in ipairs(oppResultsList:GetChildren()) do
if c:IsA("TextButton") then c:Destroy() end
end
F.notify("Opponent set: " .. plr.Name .. " — camera will track both of you", "success")
end
local function refreshOppResults(query)
for _, c in ipairs(oppResultsList:GetChildren()) do
if c:IsA("TextButton") then c:Destroy() end
end
if query == "" then return end
query = query:lower()
for _, plr in ipairs(Players:GetPlayers()) do
if plr ~= LocalPlayer and plr.Name:lower():find(query, 1, true) then
local row = F.makeBtn(oppResultsList, plr.Name, UDim2.new(0,2,0,0), UDim2.new(1,-4,0,24), Color3.fromRGB(28,28,42))
row.MouseButton1Click:Connect(function() camPickPlayer(plr) end)
end
end
end
oppSearchBox:GetPropertyChangedSignal("Text"):Connect(function() refreshOppResults(oppSearchBox.Text) end)
local clearOppBtn = F.makeBtn(pgCam, "✕ CLEAR OPPONENT", UDim2.new(0,8,0,802), UDim2.new(1,-16,0,26), Color3.fromRGB(170,35,35), nil, 10)
clearOppBtn.MouseButton1Click:Connect(function()
currentTarget = nil
F.stopWatchingTargetAnims()
if spacing then spacing:SetOpponent(nil) end
FNFCamera:SetOpponentCharacter(nil)
camOppLbl.Text = "Opponent: none"
F.notify("Opponent cleared — camera centers on you", "info")
end)
-- ── MATCH SEARCH ──
F.makeDivider(pgCam, 838, Color3.fromRGB(0,200,255))
local matchTitle = F.makeLabel(pgCam, "🔍 FIND A MATCH (SAME SCRIPT)", UDim2.new(0,8,0,844),
UDim2.new(1,-16,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
F.regText(matchTitle)
F.makeLabel(pgCam, "Only finds other players currently running this injector.",
UDim2.new(0,8,0,862), UDim2.new(1,-16,0,16), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local matchSearchBox = F.makeBox(pgCam, UDim2.new(0,8,0,882), UDim2.new(1,-96,0,28), "Search name (blank = everyone)")
local matchSearchBtn = F.makeBtn(pgCam, "🔍 SCAN", UDim2.new(1,-84,0,882), UDim2.new(0,76,0,28), Color3.fromRGB(70,120,255), nil, 10)
local matchResults = Instance.new("ScrollingFrame")
matchResults.Size = UDim2.new(1,-16,0,120); matchResults.Position = UDim2.new(0,8,0,916)
matchResults.BackgroundColor3 = Color3.fromRGB(18,18,26); matchResults.BorderSizePixel = 0
matchResults.ScrollBarThickness = 3; matchResults.CanvasSize = UDim2.new(0,0,0,0)
matchResults.AutomaticCanvasSize = Enum.AutomaticSize.Y
matchResults.Parent = pgCam; F.uiCorner(matchResults, UDim.new(0,6))
local matchResultsLayout = Instance.new("UIListLayout")
matchResultsLayout.Padding = UDim.new(0,2); matchResultsLayout.Parent = matchResults
local function renderMatchResults(list)
for _, c in ipairs(matchResults:GetChildren()) do
if c:IsA("Frame") then c:Destroy() end
end
if #list == 0 then
F.notify("No other players with the injector found", "info")
return
end
for _, entry in ipairs(list) do
local row = Instance.new("Frame")
row.Size = UDim2.new(1,-4,0,26); row.BackgroundColor3 = Color3.fromRGB(24,24,36)
row.BorderSizePixel = 0; row.Parent = matchResults
F.uiCorner(row, UDim.new(0,4))
F.makeLabel(row, entry.Name, UDim2.new(0,6,0,0), UDim2.new(1,-70,1,0),
Color3.fromRGB(200,200,215), Enum.TextXAlignment.Left, 10)
local matchBtn = F.makeBtn(row, "🎯 MATCH", UDim2.new(1,-64,0,2), UDim2.new(0,60,0,22), Color3.fromRGB(60,190,120), nil, 9)
matchBtn.MouseButton1Click:Connect(function()
local plr = Players:FindFirstChild(entry.Name)
if plr then
camPickPlayer(plr)
else
F.notify("Player left before matching", "warn")
end
end)
end
end
matchSearchBtn.MouseButton1Click:Connect(function()
F.searchForMatches(matchSearchBox.Text, renderMatchResults)
end)
-- ── PROXIMITY BLOCK ──
F.makeDivider(pgCam, 1048, Color3.fromRGB(0,200,255))
local proxTitle = F.makeLabel(pgCam, "🚧 PROXIMITY BLOCK", UDim2.new(0,8,0,1054),
UDim2.new(1,-16,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
F.regText(proxTitle)
F.makeLabel(pgCam, "Invisible wall — you can't walk past this distance from your opponent.",
UDim2.new(0,8,0,1072), UDim2.new(1,-16,0,16), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local proxBtn = F.makeBtn(pgCam, proximityBlockEnabled and "🟢 BLOCK ON" or "🔴 BLOCK OFF",
UDim2.new(0,8,0,1092), UDim2.new(0,120,0,28),
proximityBlockEnabled and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50), nil, 10)
proxBtn.MouseButton1Click:Connect(function()
proximityBlockEnabled = not proximityBlockEnabled
proxBtn.Text = proximityBlockEnabled and "🟢 BLOCK ON" or "🔴 BLOCK OFF"
proxBtn.BackgroundColor3 = proximityBlockEnabled and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50)
F.notify(proximityBlockEnabled and "Proximity block enabled" or "Proximity block disabled", "info")
end)
F.makeLabel(pgCam, "DISTANCE (studs)", UDim2.new(0,8,0,1128), UDim2.new(0,140,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local proxDistLbl = F.makeLabel(pgCam, string.format("%.1f", proximityBlockDist),
UDim2.new(0,320,0,1128), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 11)
F.regText(proxDistLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,1150), UDim2.new(1,-16,0,14), 1, 8, proximityBlockDist, function(v)
proximityBlockDist = v
proxDistLbl.Text = string.format("%.1f", v)
end)
-- ── FIGHTING SYSTEM ──
F.makeDivider(pgCam, 1178, Color3.fromRGB(255,80,180))
local fightTitle = F.makeLabel(pgCam, "⚔ FIGHTING SYSTEM", UDim2.new(0,8,0,1184),
UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
F.regText(fightTitle)
F.makeLabel(pgCam, "Sustained hits push opponent back. Double notes hit harder. Returns to position when done.",
UDim2.new(0,8,0,1206), UDim2.new(1,-16,0,30), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local fightBtn = F.makeBtn(pgCam, "📴 FIGHT MODE OFF", UDim2.new(0,8,0,1240), UDim2.new(1,-16,0,32), Color3.fromRGB(35,35,55), nil, 11)
fightBtn.MouseButton1Click:Connect(function()
fightModeEnabled = not fightModeEnabled
if fightModeEnabled then
fightBtn.Text = "⚔ FIGHT MODE ON"
fightBtn.BackgroundColor3 = THEMES[currentThemeName].accent
F.notify("Fight mode enabled — push your opponent back!", "success")
else
fightBtn.Text = "📴 FIGHT MODE OFF"
fightBtn.BackgroundColor3 = Color3.fromRGB(35,35,55)
F.deactivateFight()
F.notify("Fight mode disabled", "info")
end
end)
fightStatusLbl = F.makeLabel(pgCam, "● IDLE", UDim2.new(0,8,0,1278),
UDim2.new(1,-16,0,18), Color3.fromRGB(160,80,80), Enum.TextXAlignment.Left, 10)
local fightMomentumLbl = F.makeLabel(pgCam, "YOU: 0.0 | OPP: 0.0",
UDim2.new(0,8,0,1298), UDim2.new(1,-16,0,16), Color3.fromRGB(160,160,180), Enum.TextXAlignment.Left, 10)
F.refreshFightUI = function()
if FS.active then
fightStatusLbl.Text = "⚔ FIGHTING"
fightStatusLbl.TextColor3 = Color3.fromRGB(255,80,80)
else
fightStatusLbl.Text = fightModeEnabled and "● READY" or "● IDLE"
fightStatusLbl.TextColor3 = fightModeEnabled and Color3.fromRGB(80,220,120) or Color3.fromRGB(160,80,80)
end
fightMomentumLbl.Text = string.format("YOUR PUSH: %.1f | OPP PUSH: %.1f", FS.playerMomentum, FS.opponentMomentum)
end
F.makeLabel(pgCam, "HIT POWER", UDim2.new(0,8,0,1324), UDim2.new(0,140,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local hitPowerLbl = F.makeLabel(pgCam, string.format("%.1f studs", FS.pushPerHit),
UDim2.new(0,320,0,1324), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(hitPowerLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,1346), UDim2.new(1,-16,0,14), 0.2, 3, FS.pushPerHit, function(v)
FS.pushPerHit = v
hitPowerLbl.Text = string.format("%.1f studs", v)
end)
F.makeLabel(pgCam, "DOUBLE-NOTE POWER", UDim2.new(0,8,0,1370), UDim2.new(0,160,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local dblPowerLbl = F.makeLabel(pgCam, string.format("%.1f studs", FS.pushPerDouble),
UDim2.new(0,320,0,1370), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(dblPowerLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,1392), UDim2.new(1,-16,0,14), 0.5, 5, FS.pushPerDouble, function(v)
FS.pushPerDouble = v
dblPowerLbl.Text = string.format("%.1f studs", v)
end)
F.makeLabel(pgCam, "MOMENTUM CAP", UDim2.new(0,8,0,1416), UDim2.new(0,140,0,20), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local momCapLbl = F.makeLabel(pgCam, string.format("%.0f", FS.momentumCap),
UDim2.new(0,320,0,1416), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(momCapLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,1438), UDim2.new(1,-16,0,14), 4, 30, FS.momentumCap, function(v)
FS.momentumCap = v
momCapLbl.Text = string.format("%.0f", v)
end)
-- ============================================================
-- ── SILHOUETTE MODE (you + opponent) ──
-- ============================================================
local silY = 1466
F.makeDivider(pgCam, silY, Color3.fromRGB(255,255,255))
local silTitle = F.makeLabel(pgCam, "🖤 SILHOUETTE MODE", UDim2.new(0,8,0,silY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(255,255,255), Enum.TextXAlignment.Left, 12)
silTitle.Font = Enum.Font.GothamBold
F.makeLabel(pgCam, "World washes white — you and your opponent become flat black cut-outs.",
UDim2.new(0,8,0,silY+26), UDim2.new(1,-16,0,16), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local silBtn = F.makeBtn(pgCam, "○ SILHOUETTE OFF", UDim2.new(0,8,0,silY+46),
UDim2.new(0,200,0,28), Color3.fromRGB(35,35,55), nil, 10)
silBtn.MouseButton1Click:Connect(function() Sil.set(not silhouetteActive) end)
F.makeLabel(pgCam, "KEY", UDim2.new(0,216,0,silY+46), UDim2.new(0,30,0,28),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local silKeyBtn = F.makeBtn(pgCam, silhouetteKey, UDim2.new(0,248,0,silY+46),
UDim2.new(0,60,0,28), Color3.fromRGB(25,35,48), nil, 11)
silKeyBtn.MouseButton1Click:Connect(function()
silKeyBtn.Text = "..."; silKeyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
silhouetteKey = inp.KeyCode.Name
silKeyBtn.Text = silhouetteKey
silKeyBtn.BackgroundColor3 = Color3.fromRGB(25,35,48)
conn:Disconnect()
end
end)
end)
-- who gets blacked out
local silMeBtn = F.makeBtn(pgCam, "🧍 YOU: ON", UDim2.new(0,8,0,silY+80),
UDim2.new(0,116,0,26), Color3.fromRGB(60,190,120), nil, 9)
local silOppBtn = F.makeBtn(pgCam, "🎤 OPPONENT: ON", UDim2.new(0,130,0,silY+80),
UDim2.new(0,146,0,26), Color3.fromRGB(60,190,120), nil, 9)
local silTopBtn = F.makeBtn(pgCam, "👁 ON TOP", UDim2.new(0,282,0,silY+80),
UDim2.new(0,110,0,26), Color3.fromRGB(70,120,255), nil, 9)
silMeBtn.MouseButton1Click:Connect(function()
SilFX.includePlayer = not SilFX.includePlayer
silMeBtn.Text = SilFX.includePlayer and "🧍 YOU: ON" or "🧍 YOU: OFF"
silMeBtn.BackgroundColor3 = SilFX.includePlayer and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50)
end)
silOppBtn.MouseButton1Click:Connect(function()
SilFX.includeOpponent = not SilFX.includeOpponent
silOppBtn.Text = SilFX.includeOpponent and "🎤 OPPONENT: ON" or "🎤 OPPONENT: OFF"
silOppBtn.BackgroundColor3 = SilFX.includeOpponent and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50)
end)
silTopBtn.MouseButton1Click:Connect(function()
SilFX.alwaysOnTop = not SilFX.alwaysOnTop
silTopBtn.Text = SilFX.alwaysOnTop and "👁 ON TOP" or "👁 OCCLUDED"
silTopBtn.BackgroundColor3 = SilFX.alwaysOnTop and Color3.fromRGB(70,120,255) or Color3.fromRGB(35,35,55)
end)
-- strength
F.makeLabel(pgCam, "STRENGTH", UDim2.new(0,8,0,silY+114), UDim2.new(0,110,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local silStrLbl = F.makeLabel(pgCam, string.format("%d%%", math.floor(SilFX.strength*100 + 0.5)),
UDim2.new(0,320,0,silY+114), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(silStrLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,silY+136), UDim2.new(1,-16,0,14), 0.2, 1, SilFX.strength, function(v)
SilFX.strength = v
silStrLbl.Text = string.format("%d%%", math.floor(v*100 + 0.5))
end)
-- cut-out colour
F.makeLabel(pgCam, "CUT-OUT COLOR", UDim2.new(0,8,0,silY+158), UDim2.new(0,140,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local silColors = {
{ name = "BLACK", c = Color3.new(0,0,0) },
{ name = "WHITE", c = Color3.new(1,1,1) },
{ name = "ACCENT", c = nil },
}
for ci, entry in ipairs(silColors) do
local cx = 150 + (ci-1) * 84
local cbtn = F.makeBtn(pgCam, entry.name, UDim2.new(0,cx,0,silY+158), UDim2.new(0,80,0,22),
entry.c or THEMES[currentThemeName].accent, Color3.fromRGB(20,20,20), 9)
cbtn.MouseButton1Click:Connect(function()
SilFX.fillColor = entry.c or THEMES[currentThemeName].accent
F.notify("Silhouette color: " .. entry.name, "info")
end)
end
F.refreshSilUI = function()
if not silBtn or not silBtn.Parent then return end
silBtn.Text = silhouetteActive and "🖤 SILHOUETTE ON" or "○ SILHOUETTE OFF"
silBtn.BackgroundColor3 = silhouetteActive and Color3.fromRGB(20,20,20) or Color3.fromRGB(35,35,55)
end
F.refreshSilUI()
-- ── INTRO TRANSITIONS ──
local introY = silY + 200
F.makeDivider(pgCam, introY, Color3.fromRGB(255,200,60))
F.makeLabel(pgCam, "🎬 INTRO TRANSITIONS", UDim2.new(0,8,0,introY+6),
UDim2.new(1,-16,0,18), Color3.fromRGB(255,200,60), Enum.TextXAlignment.Left, 11)
F.makeLabel(pgCam, "Play a cinematic intro before a song starts. Click any to preview.",
UDim2.new(0,8,0,introY+24), UDim2.new(1,-16,0,16), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local introData = {
{ name = "COUNTDOWN", label = "🔢 3-2-1-GO!", desc = "Classic FNF countdown" },
{ name = "WIPE", label = "🔲 DIAGONAL WIPE", desc = "Black screen slides away" },
{ name = "PIXELATE", label = "🟦 PIXEL DISSOLVE", desc = "Pixelated grid dissolves" },
{ name = "FLASH", label = "⚡ FLASH BANG", desc = "White flash then reveal" },
{ name = "SPLIT", label = "⚔ VS SPLIT", desc = "Curtains split open with VS" },
}
for i, intro in ipairs(introData) do
local iy = introY + 46 + (i-1) * 38
local introBtn = F.makeBtn(pgCam, intro.label, UDim2.new(0,8,0,iy), UDim2.new(0,200,0,32), Color3.fromRGB(35,35,55), nil, 10)
F.makeLabel(pgCam, intro.desc, UDim2.new(0,216,0,iy), UDim2.new(1,-224,0,32), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
introBtn.MouseButton1Click:Connect(function()
F.playIntroTransition(intro.name)
F.notify("Playing: " .. intro.label, "info")
end)
end
-- ============================================================
-- ── CHAOS CAMERA (music-video / edit mode) ──
-- ============================================================
local chaosY = introY + 250
F.makeDivider(pgCam, chaosY, Color3.fromRGB(255,60,60))
local chTitle = F.makeLabel(pgCam, "CHAOS CAMERA", UDim2.new(0,8,0,chaosY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(255,60,60), Enum.TextXAlignment.Left, 12)
chTitle.Font = Enum.Font.GothamBold
F.makeLabel(pgCam, "Violent punch zooms, irregular shake, roll, tearing and RGB split. Needs the cinematic camera ON.",
UDim2.new(0,8,0,chaosY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local chBtn = F.makeBtn(pgCam, "CHAOS OFF", UDim2.new(0,8,0,chaosY+58),
UDim2.new(1,-16,0,32), Color3.fromRGB(35,35,55), nil, 11)
chBtn.MouseButton1Click:Connect(function()
Chaos.setEnabled(not Chaos.enabled)
chBtn.Text = Chaos.enabled and "CHAOS ON — CAMERA IS UNSTABLE" or "CHAOS OFF"
chBtn.BackgroundColor3 = Chaos.enabled and Color3.fromRGB(200,40,40) or Color3.fromRGB(35,35,55)
if Chaos.enabled and not FNFCamera.enabled then
F.notify("Turn the cinematic camera ON or chaos has nothing to drive", "warn")
end
end)
-- presets
F.makeLabel(pgCam, "PRESET", UDim2.new(0,8,0,chaosY+96), UDim2.new(0,80,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local chPresets = { "Calm", "Intense", "Chaotic", "Corrupted" }
local chSliders = {}
for i, pname in ipairs(chPresets) do
local px = 8 + (i-1) * 100
local pb = F.makeBtn(pgCam, pname, UDim2.new(0,px,0,chaosY+116), UDim2.new(0,94,0,26),
Color3.fromRGB(45,30,60), nil, 9)
pb.MouseButton1Click:Connect(function()
Chaos.applyPreset(pname)
for key, sl in pairs(chSliders) do
if sl.setValue then sl.setValue(Chaos[key], true) end
end
end)
end
-- intensity sliders
local chRows = {
{ key = "master", label = "MASTER", min = 0, max = 2 },
{ key = "shake", label = "SHAKE", min = 0, max = 2 },
{ key = "zoomPunch", label = "PUNCH ZOOM", min = 0, max = 2 },
{ key = "warp", label = "WARP / LENS", min = 0, max = 2 },
{ key = "glitch", label = "GLITCH / TEAR",min = 0, max = 2 },
{ key = "flash", label = "FLASH / RGB", min = 0, max = 2 },
{ key = "readability", label = "READABILITY", min = 0.4, max = 1.6 },
}
for i, row in ipairs(chRows) do
local ry = chaosY + 152 + (i-1) * 44
F.makeLabel(pgCam, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,150,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pgCam, string.format("%.2f", Chaos[row.key]),
UDim2.new(0,320,0,ry), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local capturedKey = row.key
chSliders[row.key] = F.makeSlider(pgCam, UDim2.new(0,8,0,ry+22), UDim2.new(1,-16,0,14),
row.min, row.max, Chaos[row.key], function(v)
Chaos[capturedKey] = v
vlbl.Text = string.format("%.2f", v)
end)
end
local chBeatY = chaosY + 152 + #chRows * 44
local chBeatBtn = F.makeBtn(pgCam, Chaos.beatSync and "BEAT SYNC: ON" or "BEAT SYNC: OFF",
UDim2.new(0,8,0,chBeatY), UDim2.new(0,160,0,26),
Chaos.beatSync and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50), nil, 9)
chBeatBtn.MouseButton1Click:Connect(function()
Chaos.beatSync = not Chaos.beatSync
chBeatBtn.Text = Chaos.beatSync and "BEAT SYNC: ON" or "BEAT SYNC: OFF"
chBeatBtn.BackgroundColor3 = Chaos.beatSync and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50)
end)
F.makeLabel(pgCam, "Pulses the camera on every beat of the current BPM.",
UDim2.new(0,176,0,chBeatY), UDim2.new(1,-184,0,26), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
-- manual triggers
local chTrigY = chBeatY + 34
local chImpact = F.makeBtn(pgCam, "IMPACT", UDim2.new(0,8,0,chTrigY), UDim2.new(0,126,0,28), Color3.fromRGB(200,40,40), nil, 10)
local chGlitch = F.makeBtn(pgCam, "GLITCH", UDim2.new(0,140,0,chTrigY), UDim2.new(0,126,0,28), Color3.fromRGB(140,40,180), nil, 10)
local chTunnel = F.makeBtn(pgCam, "TUNNEL", UDim2.new(0,272,0,chTrigY), UDim2.new(0,126,0,28), Color3.fromRGB(40,120,220), nil, 10)
chImpact.MouseButton1Click:Connect(function() Chaos.bigImpact(1.8) end)
chGlitch.MouseButton1Click:Connect(function() Chaos.glitchBurst(0.4) end)
chTunnel.MouseButton1Click:Connect(function() Chaos.tunnelPull(0.9) end)
-- ============================================================
-- ── CINEMATIC BARS + FULL POWER ──
-- ============================================================
local extraY = chTrigY + 58
F.makeDivider(pgCam, extraY, Color3.fromRGB(255,200,60))
local barsBtn = F.makeBtn(pgCam, "CINEMATIC BARS: ON", UDim2.new(0,8,0,extraY+10),
UDim2.new(0,220,0,28), Color3.fromRGB(60,190,120), nil, 10)
barsBtn.MouseButton1Click:Connect(function()
CAM_CFG.BarsEnabled = not CAM_CFG.BarsEnabled
barsBtn.Text = CAM_CFG.BarsEnabled and "CINEMATIC BARS: ON" or "CINEMATIC BARS: OFF"
barsBtn.BackgroundColor3 = CAM_CFG.BarsEnabled and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50)
if not CAM_CFG.BarsEnabled then
if FNFCamera.letterTop then FNFCamera.letterTop.Size = UDim2.new(1,0,0,0) end
if FNFCamera.letterBottom then FNFCamera.letterBottom.Size = UDim2.new(1,0,0,0) end
end
F.notify(CAM_CFG.BarsEnabled and "Letterbox bars on" or "Letterbox bars off", "info")
end)
local fullBtn = F.makeBtn(pgCam, "FULL POWER (100%)", UDim2.new(0,236,0,extraY+10),
UDim2.new(0,166,0,28), Color3.fromRGB(200,40,40), nil, 10)
fullBtn.MouseButton1Click:Connect(function()
FNFCamera:SetEnabled(true)
FNFCamera:SetSensitivity(3)
FNFCamera:SetShakeScale(1)
FNFCamera:SetZoomScale(1)
FNFCamera.targetIntensity = 1
FNFCamera.intensity = 1
camBtn.Text = "CAMERA ON"
camBtn.BackgroundColor3 = THEMES[currentThemeName].accent
F.notify("Cinematic camera at 100% — max shake, zoom and sensitivity", "success")
end)
-- ============================================================
-- ── SPLIT SCREEN ──
-- ============================================================
local spY = extraY + 50
F.makeDivider(pgCam, spY, Color3.fromRGB(255,110,230))
local spTitle = F.makeLabel(pgCam, "SPLIT SCREEN", UDim2.new(0,8,0,spY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(255,110,230), Enum.TextXAlignment.Left, 12)
spTitle.Font = Enum.Font.GothamBold
F.makeLabel(pgCam, "You left, opponent right, turned to face each other. Both cameras stay locked still.",
UDim2.new(0,8,0,spY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
F.makeLabel(pgCam, "Roblox renders the world through one camera, so these panes show cloned characters only - no map behind them.",
UDim2.new(0,8,0,spY+54), UDim2.new(1,-16,0,28), Color3.fromRGB(200,140,60), Enum.TextXAlignment.Left, 9)
local spBtn = F.makeBtn(pgCam, "SPLIT SCREEN OFF", UDim2.new(0,8,0,spY+86),
UDim2.new(0,250,0,32), Color3.fromRGB(35,35,55), nil, 11)
spBtn.MouseButton1Click:Connect(function() Split.setEnabled(not Split.enabled) end)
F.makeLabel(pgCam, "KEY", UDim2.new(0,266,0,spY+86), UDim2.new(0,30,0,32),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local spKeyBtn = F.makeBtn(pgCam, Split.key, UDim2.new(0,298,0,spY+86),
UDim2.new(0,64,0,32), Color3.fromRGB(25,35,48), nil, 11)
spKeyBtn.MouseButton1Click:Connect(function()
spKeyBtn.Text = "..."; spKeyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
Split.key = inp.KeyCode.Name
spKeyBtn.Text = Split.key
spKeyBtn.BackgroundColor3 = Color3.fromRGB(25,35,48)
conn:Disconnect()
end
end)
end)
Split.refreshUI = function()
if not spBtn or not spBtn.Parent then return end
spBtn.Text = Split.enabled and "SPLIT SCREEN ON" or "SPLIT SCREEN OFF"
spBtn.BackgroundColor3 = Split.enabled and Color3.fromRGB(200,60,160) or Color3.fromRGB(35,35,55)
end
Split.refreshUI()
local spRefresh = F.makeBtn(pgCam, "REBUILD CLONES", UDim2.new(0,8,0,spY+126),
UDim2.new(0,180,0,26), Color3.fromRGB(70,120,255), nil, 9)
spRefresh.MouseButton1Click:Connect(function()
if Split.gui then Split.refreshClones(); F.notify("Split clones rebuilt", "info")
else F.notify("Turn split screen on first", "warn") end
end)
local spFace = F.makeBtn(pgCam, "FACE EACH OTHER", UDim2.new(0,196,0,spY+126),
UDim2.new(0,100,0,26), Color3.fromRGB(60,190,120), nil, 9)
spFace.MouseButton1Click:Connect(function()
Split.rotL, Split.rotR = -65, 65
Split.reface()
if Split.refreshRots then Split.refreshRots() end
end)
local spLoop = F.makeBtn(pgCam, "LOOP MIRRORED: ON", UDim2.new(0,8,0,spY+158),
UDim2.new(0,190,0,26), Color3.fromRGB(60,190,120), nil, 9)
spLoop.MouseButton1Click:Connect(function()
Split.loopMirrored = not Split.loopMirrored
spLoop.Text = Split.loopMirrored and "LOOP MIRRORED: ON" or "LOOP MIRRORED: OFF"
spLoop.BackgroundColor3 = Split.loopMirrored and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50)
end)
local spFront = F.makeBtn(pgCam, "FACE FRONT", UDim2.new(0,304,0,spY+126),
UDim2.new(0,98,0,26), Color3.fromRGB(70,120,255), nil, 9)
spFront.MouseButton1Click:Connect(function()
Split.rotL, Split.rotR = 0, 0
Split.reface()
if Split.refreshRots then Split.refreshRots() end
end)
local spSliders, spLabels = {}, {}
Split.refreshRots = function()
for k, sl in pairs(spSliders) do
if sl.setValue then sl.setValue(Split[k], false) end
if spLabels[k] then spLabels[k].Text = string.format("%.1f", Split[k]) end
end
end
local spRows = {
{ key = "dist", label = "CAMERA DISTANCE", min = 3, max = 14 },
{ key = "height", label = "CAMERA HEIGHT", min = -1, max = 6 },
{ key = "rotL", label = "YOUR ROTATION", min = -180, max = 180 },
{ key = "rotR", label = "OPP ROTATION", min = -180, max = 180 },
}
for i, row in ipairs(spRows) do
local ry = spY + 196 + (i-1) * 44
F.makeLabel(pgCam, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,200,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pgCam, string.format("%.1f", Split[row.key]),
UDim2.new(0,320,0,ry), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local capturedKey = row.key
spLabels[row.key] = vlbl
spSliders[row.key] = F.makeSlider(pgCam, UDim2.new(0,8,0,ry+22), UDim2.new(1,-16,0,14),
row.min, row.max, Split[row.key], function(v)
Split[capturedKey] = v
vlbl.Text = string.format("%.1f", v)
if capturedKey == "rotL" or capturedKey == "rotR" then Split.reface() end
end)
end
-- ============================================================
-- ── HEALTH BAR ──
-- ============================================================
local hbY = spY + 390
F.makeDivider(pgCam, hbY, Color3.fromRGB(60,230,120))
local hbTitle = F.makeLabel(pgCam, "HEALTH BAR", UDim2.new(0,8,0,hbY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(60,230,120), Enum.TextXAlignment.Left, 12)
hbTitle.Font = Enum.Font.GothamBold
F.makeLabel(pgCam, "Your notes push you forward, their poses push you back. Icon position shows how close you are to losing.",
UDim2.new(0,8,0,hbY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local hbBtn = F.makeBtn(pgCam, "HEALTH BAR ON", UDim2.new(0,8,0,hbY+58),
UDim2.new(0,250,0,32), Color3.fromRGB(40,170,90), nil, 11)
hbBtn.MouseButton1Click:Connect(function() Health.setEnabled(not Health.enabled) end)
F.makeLabel(pgCam, "KEY", UDim2.new(0,266,0,hbY+58), UDim2.new(0,30,0,32),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local hbKeyBtn = F.makeBtn(pgCam, Health.key, UDim2.new(0,298,0,hbY+58),
UDim2.new(0,64,0,32), Color3.fromRGB(25,35,48), nil, 11)
hbKeyBtn.MouseButton1Click:Connect(function()
hbKeyBtn.Text = "..."; hbKeyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
Health.key = inp.KeyCode.Name
hbKeyBtn.Text = Health.key
hbKeyBtn.BackgroundColor3 = Color3.fromRGB(25,35,48)
conn:Disconnect()
end
end)
end)
Health.refreshUI = function()
if not hbBtn or not hbBtn.Parent then return end
hbBtn.Text = Health.enabled and "HEALTH BAR ON" or "HEALTH BAR OFF"
hbBtn.BackgroundColor3 = Health.enabled and Color3.fromRGB(40,170,90) or Color3.fromRGB(35,35,55)
end
Health.refreshUI()
local hbSync = F.makeBtn(pgCam, Health.syncRoblox and "ROBLOX HEALTH SYNC: ON" or "ROBLOX HEALTH SYNC: OFF",
UDim2.new(0,196,0,hbY+98), UDim2.new(0,200,0,26),
Health.syncRoblox and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55), nil, 9)
hbSync.MouseButton1Click:Connect(function()
Health.syncRoblox = not Health.syncRoblox
hbSync.Text = Health.syncRoblox and "ROBLOX HEALTH SYNC: ON" or "ROBLOX HEALTH SYNC: OFF"
hbSync.BackgroundColor3 = Health.syncRoblox and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55)
if not Health.syncRoblox then Health.releaseRoblox() end
F.notify(Health.syncRoblox and "Your Roblox health now follows the rap battle"
or "Roblox health released", "info")
end)
local hbDmg = F.makeBtn(pgCam, Health.respectDamage and "RESPECT GAME DAMAGE: ON" or "RESPECT GAME DAMAGE: OFF",
UDim2.new(0,8,0,hbY+130), UDim2.new(0,200,0,26),
Health.respectDamage and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55), nil, 9)
hbDmg.MouseButton1Click:Connect(function()
Health.respectDamage = not Health.respectDamage
hbDmg.Text = Health.respectDamage and "RESPECT GAME DAMAGE: ON" or "RESPECT GAME DAMAGE: OFF"
hbDmg.BackgroundColor3 = Health.respectDamage and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55)
end)
local hbVig = F.makeBtn(pgCam, Health.lowVignette and "RED VIGNETTE: ON" or "RED VIGNETTE: OFF",
UDim2.new(0,214,0,hbY+130), UDim2.new(0,182,0,26),
Health.lowVignette and Color3.fromRGB(190,40,50) or Color3.fromRGB(35,35,55), nil, 9)
hbVig.MouseButton1Click:Connect(function()
Health.lowVignette = not Health.lowVignette
hbVig.Text = Health.lowVignette and "RED VIGNETTE: ON" or "RED VIGNETTE: OFF"
hbVig.BackgroundColor3 = Health.lowVignette and Color3.fromRGB(190,40,50) or Color3.fromRGB(35,35,55)
end)
F.makeLabel(pgCam, "Rap drain stops at 1 HP. Shows on your screen only.",
UDim2.new(0,8,0,hbY+158), UDim2.new(1,-16,0,16), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
local hbReset = F.makeBtn(pgCam, "RESET TO 50%", UDim2.new(0,8,0,hbY+98),
UDim2.new(0,180,0,26), Color3.fromRGB(70,120,255), nil, 9)
hbReset.MouseButton1Click:Connect(function() Health.reset() end)
-- optional custom icons
F.makeLabel(pgCam, "YOUR ICON ID", UDim2.new(0,8,0,hbY+180), UDim2.new(0,110,0,24),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 9)
local hbIconP = F.makeBox(pgCam, UDim2.new(0,120,0,hbY+180), UDim2.new(0,120,0,24), "decal id", Health.playerIcon)
F.makeLabel(pgCam, "THEIR ICON", UDim2.new(0,248,0,hbY+180), UDim2.new(0,90,0,24),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 9)
local hbIconO = F.makeBox(pgCam, UDim2.new(0,330,0,hbY+180), UDim2.new(0,72,0,24), "decal id", Health.oppIcon)
local function applyIcons()
Health.setIcons(hbIconP.Text:gsub("%D",""), hbIconO.Text:gsub("%D",""))
end
hbIconP.FocusLost:Connect(applyIcons)
hbIconO.FocusLost:Connect(applyIcons)
local hbRows = {
{ key = "gainPerNote", label = "GAIN PER NOTE", min = 0, max = 0.15 },
{ key = "drainPerPose", label = "DRAIN PER POSE", min = 0, max = 0.2 },
{ key = "passiveDrain", label = "PASSIVE DRAIN /s", min = 0, max = 0.08 },
{ key = "smooth", label = "BAR SMOOTHING", min = 2, max = 18 },
{ key = "vignetteAt", label = "VIGNETTE STARTS BELOW", min = 0.05, max = 0.6 },
}
for i, row in ipairs(hbRows) do
local ry = hbY + 216 + (i-1) * 44
F.makeLabel(pgCam, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,200,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pgCam, string.format("%.3f", Health[row.key]),
UDim2.new(0,320,0,ry), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local ck = row.key
F.makeSlider(pgCam, UDim2.new(0,8,0,ry+22), UDim2.new(1,-16,0,14),
row.min, row.max, Health[row.key], function(v)
Health[ck] = v
vlbl.Text = string.format("%.3f", v)
end)
end
-- ============================================================
-- ── PERFECT DOUBLE NOTE ──
-- ============================================================
local pdY = hbY + 216 + #hbRows * 44 + 10
F.makeDivider(pgCam, pdY, Color3.fromRGB(120,240,255))
local pdTitle = F.makeLabel(pgCam, "PERFECT DOUBLE NOTE", UDim2.new(0,8,0,pdY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(120,240,255), Enum.TextXAlignment.Left, 12)
pdTitle.Font = Enum.Font.GothamBold
F.makeLabel(pgCam, "Hit two different notes inside the window and an afterimage of you flies off in that note's direction.",
UDim2.new(0,8,0,pdY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
F.makeLabel(pgCam, "Input fires once per frame, so the tightest window that can actually trigger is about 16ms at 60fps.",
UDim2.new(0,8,0,pdY+54), UDim2.new(1,-16,0,28), Color3.fromRGB(200,140,60), Enum.TextXAlignment.Left, 9)
local pdBtn = F.makeBtn(pgCam, "AFTERIMAGE ON", UDim2.new(0,8,0,pdY+86),
UDim2.new(0,200,0,30), Color3.fromRGB(40,150,200), nil, 10)
pdBtn.MouseButton1Click:Connect(function()
Ghost.enabled = not Ghost.enabled
pdBtn.Text = Ghost.enabled and "AFTERIMAGE ON" or "AFTERIMAGE OFF"
pdBtn.BackgroundColor3 = Ghost.enabled and Color3.fromRGB(40,150,200) or Color3.fromRGB(35,35,55)
end)
local pdTest = F.makeBtn(pgCam, "TEST GHOST", UDim2.new(0,216,0,pdY+86),
UDim2.new(0,186,0,30), Color3.fromRGB(140,40,180), nil, 10)
pdTest.MouseButton1Click:Connect(function()
Ghost.spawn(ARROW_ORDER[math.random(1, #ARROW_ORDER)])
end)
-- zoom power lives on the camera, so it gets its own row
F.makeLabel(pgCam, "DOUBLE-NOTE ZOOM", UDim2.new(0,8,0,pdY+122), UDim2.new(0,220,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local pdZoomLbl = F.makeLabel(pgCam, string.format("%.0f", FNFCamera.dnPower),
UDim2.new(0,320,0,pdY+122), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(pdZoomLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,pdY+144), UDim2.new(1,-16,0,14), 0, 120, FNFCamera.dnPower, function(v)
FNFCamera.dnPower = v
pdZoomLbl.Text = string.format("%.0f", v)
end)
local pdRows = {
{ key = "window", label = "PERFECT WINDOW (s)", min = 0.005, max = 0.12, fmt = "%.3f" },
{ key = "life", label = "AFTERIMAGE LIFE (s)",min = 0.15, max = 1.5, fmt = "%.2f" },
{ key = "distance", label = "TRAVEL DISTANCE", min = 1, max = 20, fmt = "%.1f" },
}
for i, row in ipairs(pdRows) do
local ry = pdY + 170 + (i-1) * 44
F.makeLabel(pgCam, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,220,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pgCam, string.format(row.fmt, Ghost[row.key]),
UDim2.new(0,320,0,ry), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local ck, cf2 = row.key, row.fmt
F.makeSlider(pgCam, UDim2.new(0,8,0,ry+22), UDim2.new(1,-16,0,14),
row.min, row.max, Ghost[row.key], function(v)
Ghost[ck] = v
vlbl.Text = string.format(cf2, v)
end)
end
-- ============================================================
-- ── CINEMATIC PRESETS ──
-- ============================================================
local cpY = pdY + 170 + #pdRows * 44 + 10
F.makeDivider(pgCam, cpY, Color3.fromRGB(255,200,60))
local cpTitle = F.makeLabel(pgCam, "CINEMATIC PRESETS", UDim2.new(0,8,0,cpY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(255,200,60), Enum.TextXAlignment.Left, 12)
cpTitle.Font = Enum.Font.GothamBold
local syncSnapRef = nil
local cpDesc = F.makeLabel(pgCam, CAM_PRESETS[FNFCamera.preset] and CAM_PRESETS[FNFCamera.preset].desc or "",
UDim2.new(0,8,0,cpY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local cpBtns = {}
for i, pname in ipairs(CAM_PRESET_ORDER) do
local col = (i - 1) % 3
local rowN = math.floor((i - 1) / 3)
local pb = F.makeBtn(pgCam, pname,
UDim2.new(0, 8 + col * 132, 0, cpY + 58 + rowN * 34),
UDim2.new(0, 126, 0, 30),
pname == FNFCamera.preset and Color3.fromRGB(255,160,40) or Color3.fromRGB(45,35,55), nil, 10)
cpBtns[pname] = pb
pb.MouseButton1Click:Connect(function()
FNFCamera:ApplyPreset(pname)
if syncSnapRef then syncSnapRef() end
cpDesc.Text = CAM_PRESETS[pname].desc
for n, b in pairs(cpBtns) do
b.BackgroundColor3 = (n == pname) and Color3.fromRGB(255,160,40) or Color3.fromRGB(45,35,55)
end
distLbl.Text = string.format("%.0f", FNFCamera.camDistance)
heightLbl.Text = string.format("%.1f", FNFCamera.camHeight)
fovLbl.Text = string.format("%.0f", CAM_CFG.BaseFOV)
F.notify("Camera preset: " .. pname, "success")
end)
end
-- hotkeys drive the same buttons, so mirror the highlight
Keys.onPreset = function(pname)
if not CAM_PRESETS[pname] then return end
if syncSnapRef then syncSnapRef() end
cpDesc.Text = CAM_PRESETS[pname].desc
for n, b in pairs(cpBtns) do
b.BackgroundColor3 = (n == pname) and Color3.fromRGB(255,160,40) or Color3.fromRGB(45,35,55)
end
distLbl.Text = string.format("%.0f", FNFCamera.camDistance)
heightLbl.Text = string.format("%.1f", FNFCamera.camHeight)
fovLbl.Text = string.format("%.0f", CAM_CFG.BaseFOV)
end
-- settle controls
local cpSetY = cpY + 58 + math.ceil(#CAM_PRESET_ORDER / 3) * 34 + 8
local cpSettleBtn = F.makeBtn(pgCam,
FNFCamera.settleEnabled and "SETTLE WHEN QUIET: ON" or "SETTLE WHEN QUIET: OFF",
UDim2.new(0,8,0,cpSetY), UDim2.new(0,220,0,26),
FNFCamera.settleEnabled and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50), nil, 9)
cpSettleBtn.MouseButton1Click:Connect(function()
FNFCamera.settleEnabled = not FNFCamera.settleEnabled
cpSettleBtn.Text = FNFCamera.settleEnabled and "SETTLE WHEN QUIET: ON" or "SETTLE WHEN QUIET: OFF"
cpSettleBtn.BackgroundColor3 = FNFCamera.settleEnabled and Color3.fromRGB(60,190,120) or Color3.fromRGB(190,50,50)
end)
F.makeLabel(pgCam, "Locks the frame in place once both of you stop.",
UDim2.new(0,236,0,cpSetY), UDim2.new(1,-244,0,26), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
-- beat zoom
local cpBeat = F.makeBtn(pgCam, "BEAT ZOOM: OFF", UDim2.new(0,8,0,cpSetY+60),
UDim2.new(0,190,0,26), Color3.fromRGB(35,35,55), nil, 9)
local function syncBeatBtn()
cpBeat.Text = FNFCamera.beatZoom and "BEAT ZOOM: ON" or "BEAT ZOOM: OFF"
cpBeat.BackgroundColor3 = FNFCamera.beatZoom and Color3.fromRGB(255,160,40) or Color3.fromRGB(35,35,55)
end
cpBeat.MouseButton1Click:Connect(function()
FNFCamera:SetBeatZoom(not FNFCamera.beatZoom)
syncBeatBtn()
end)
Keys.onBeatZoom = syncBeatBtn
syncBeatBtn()
F.makeLabel(pgCam, "Pulses every beat, heavier on every 4th.",
UDim2.new(0,206,0,cpSetY+60), UDim2.new(1,-214,0,26), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
-- upgraded follow toggles
local cpSpring = F.makeBtn(pgCam, "SPRING FOLLOW: ON", UDim2.new(0,8,0,cpSetY+30),
UDim2.new(0,190,0,26), Color3.fromRGB(60,190,120), nil, 9)
cpSpring.MouseButton1Click:Connect(function()
FNFCamera.springFollow = not FNFCamera.springFollow
cpSpring.Text = FNFCamera.springFollow and "SPRING FOLLOW: ON" or "SPRING FOLLOW: OFF"
cpSpring.BackgroundColor3 = FNFCamera.springFollow and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55)
end)
local cpAuto = F.makeBtn(pgCam, "AUTO FRAMING: ON", UDim2.new(0,206,0,cpSetY+30),
UDim2.new(0,190,0,26), Color3.fromRGB(60,190,120), nil, 9)
cpAuto.MouseButton1Click:Connect(function()
FNFCamera.autoFrame = not FNFCamera.autoFrame
cpAuto.Text = FNFCamera.autoFrame and "AUTO FRAMING: ON" or "AUTO FRAMING: OFF"
cpAuto.BackgroundColor3 = FNFCamera.autoFrame and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55)
end)
local cpSnap = F.makeBtn(pgCam, FNFCamera.snapHits and "SNAPPY HITS: ON" or "SNAPPY HITS: OFF",
UDim2.new(0,8,0,cpSetY+90), UDim2.new(0,190,0,26),
FNFCamera.snapHits and Color3.fromRGB(255,160,40) or Color3.fromRGB(35,35,55), nil, 9)
local function syncSnap()
cpSnap.Text = FNFCamera.snapHits and "SNAPPY HITS: ON" or "SNAPPY HITS: OFF"
cpSnap.BackgroundColor3 = FNFCamera.snapHits and Color3.fromRGB(255,160,40) or Color3.fromRGB(35,35,55)
end
cpSnap.MouseButton1Click:Connect(function()
FNFCamera.snapHits = not FNFCamera.snapHits
syncSnap()
end)
syncSnapRef = syncSnap
F.makeLabel(pgCam, "Kicks land instantly instead of easing in.",
UDim2.new(0,206,0,cpSetY+90), UDim2.new(1,-214,0,26), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
local cpRows = {
{ key = "nudgeScale", label = "HIT KICK", min = 0, max = 3, fmt = "%.2f" },
{ key = "tiltScale", label = "HIT TILT", min = 0, max = 3, fmt = "%.2f" },
{ key = "punchScale", label = "HIT ZOOM", min = 0, max = 4, fmt = "%.2f" },
{ key = "returnSpeed", label = "HIT RETURN SPEED", min = 0.3, max = 4, fmt = "%.2f" },
{ key = "settleAfter", label = "SETTLE DELAY (s)", min = 0.3, max = 5, fmt = "%.2f" },
{ key = "springStiff", label = "FOLLOW STIFFNESS", min = 3, max = 22, fmt = "%.1f" },
{ key = "leadTime", label = "MOTION LEAD (s)", min = 0, max = 0.6,fmt = "%.2f" },
{ key = "dirBoost", label = "DIRECTIONAL KICK", min = 1, max = 7, fmt = "%.2f" },
{ key = "dirHold", label = "KICK HANG TIME", min = 0.3, max = 3, fmt = "%.2f" },
{ key = "beatPower", label = "BEAT ZOOM POWER", min = 0, max = 45, fmt = "%.1f" },
{ key = "beatAccent", label = "DOWNBEAT ACCENT", min = 1, max = 3, fmt = "%.2f" },
{ key = "orbitSpeed", label = "ORBIT SPEED", min = 0, max = 2, fmt = "%.2f" },
{ key = "orbitAmp", label = "ORBIT SWEEP (deg)",min = 0, max = 90, fmt = "%.0f" },
{ key = "dutch", label = "DUTCH TILT (deg)", min = -12, max = 12, fmt = "%.1f" },
{ key = "handheld", label = "HANDHELD DRIFT", min = 0, max = 2, fmt = "%.2f" },
{ key = "lead", label = "LEAN TOWARD SINGER",min = 0, max = 3, fmt = "%.2f" },
}
for i, row in ipairs(cpRows) do
local ry = cpSetY + 126 + (i-1) * 44
F.makeLabel(pgCam, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,220,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pgCam, string.format(row.fmt, FNFCamera[row.key]),
UDim2.new(0,320,0,ry), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local ck, cfm = row.key, row.fmt
F.makeSlider(pgCam, UDim2.new(0,8,0,ry+22), UDim2.new(1,-16,0,14),
row.min, row.max, FNFCamera[row.key], function(v)
FNFCamera[ck] = v
vlbl.Text = string.format(cfm, v)
end)
end
-- ============================================================
-- ── BOSS BATTLE CUTSCENE ──
-- ============================================================
local bsY = cpSetY + 126 + #cpRows * 44 + 10
F.makeDivider(pgCam, bsY, Color3.fromRGB(200,40,40))
local bsTitle = F.makeLabel(pgCam, "BOSS BATTLE VIEW", UDim2.new(0,8,0,bsY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(220,60,60), Enum.TextXAlignment.Left, 12)
bsTitle.Font = Enum.Font.GothamBold
F.makeLabel(pgCam, "Opponent towers over you, you shrink, camera drops to the floor looking up at them.",
UDim2.new(0,8,0,bsY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
F.makeLabel(pgCam, "Scaling is local to your client - the opponent still sees themselves at normal size.",
UDim2.new(0,8,0,bsY+54), UDim2.new(1,-16,0,28), Color3.fromRGB(200,140,60), Enum.TextXAlignment.Left, 9)
local bsBtn = F.makeBtn(pgCam, "ENTER BOSS VIEW", UDim2.new(0,8,0,bsY+86),
UDim2.new(0,250,0,32), Color3.fromRGB(150,35,35), nil, 11)
bsBtn.MouseButton1Click:Connect(function() Boss.toggle() end)
F.makeLabel(pgCam, "KEY", UDim2.new(0,266,0,bsY+86), UDim2.new(0,30,0,32),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local bsKeyBtn = F.makeBtn(pgCam, Boss.key, UDim2.new(0,298,0,bsY+86),
UDim2.new(0,64,0,32), Color3.fromRGB(25,35,48), nil, 11)
bsKeyBtn.MouseButton1Click:Connect(function()
bsKeyBtn.Text = "..."; bsKeyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
Boss.key = inp.KeyCode.Name
bsKeyBtn.Text = Boss.key
bsKeyBtn.BackgroundColor3 = Color3.fromRGB(25,35,48)
conn:Disconnect()
end
end)
end)
Boss.refreshUI = function()
if not bsBtn or not bsBtn.Parent then return end
bsBtn.Text = Boss.active and "EXIT BOSS VIEW" or "ENTER BOSS VIEW"
bsBtn.BackgroundColor3 = Boss.active and Color3.fromRGB(220,60,60) or Color3.fromRGB(150,35,35)
end
Boss.refreshUI()
local bsRows = {
{ key = "oppScale", label = "OPPONENT SCALE", min = 1, max = 6, fmt = "%.2f", live = true },
{ key = "playerScale", label = "YOUR SCALE", min = 0.3, max = 1.5, fmt = "%.2f", live = true },
{ key = "dist", label = "CAMERA DISTANCE", min = 3, max = 18, fmt = "%.1f" },
{ key = "height", label = "CAMERA HEIGHT", min = -1, max = 6, fmt = "%.1f" },
{ key = "lookHeight", label = "LOOK-UP HEIGHT", min = 1, max = 18, fmt = "%.1f" },
{ key = "bars", label = "LETTERBOX", min = 0, max = 0.28, fmt = "%.3f" },
}
for i, row in ipairs(bsRows) do
local ry = bsY + 126 + (i-1) * 44
F.makeLabel(pgCam, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,220,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pgCam, string.format(row.fmt, Boss[row.key]),
UDim2.new(0,320,0,ry), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local ck, cfm, live = row.key, row.fmt, row.live
F.makeSlider(pgCam, UDim2.new(0,8,0,ry+22), UDim2.new(1,-16,0,14),
row.min, row.max, Boss[row.key], function(v)
Boss[ck] = v
vlbl.Text = string.format(cfm, v)
if live then Boss.restyle() end
end)
end
-- ============================================================
-- ── KEYBINDS ──
-- ============================================================
local kbY = bsY + 126 + #bsRows * 44 + 10
F.makeDivider(pgCam, kbY, Color3.fromRGB(160,160,255))
local kbTitle = F.makeLabel(pgCam, "KEYBINDS", UDim2.new(0,8,0,kbY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(160,160,255), Enum.TextXAlignment.Left, 12)
kbTitle.Font = Enum.Font.GothamBold
F.makeLabel(pgCam, "Every bind below works with the panel closed. Click a key to rebind it.",
UDim2.new(0,8,0,kbY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
-- helper: a rebind button plus an X that clears the bind entirely
local function bindBtn(x, y, w, current, onSet)
local bw = w - 26
local b = F.makeBtn(pgCam, current == "" and "None" or current,
UDim2.new(0,x,0,y), UDim2.new(0,bw,0,24), Color3.fromRGB(25,35,48), nil, 10)
b.MouseButton1Click:Connect(function()
b.Text = "..."; b.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
onSet(inp.KeyCode.Name)
b.Text = inp.KeyCode.Name
b.BackgroundColor3 = Color3.fromRGB(25,35,48)
Keys.refresh()
conn:Disconnect()
end
end)
end)
local clr = F.makeBtn(pgCam, "X", UDim2.new(0, x + bw + 2, 0, y),
UDim2.new(0, 22, 0, 24), Color3.fromRGB(150,35,35), nil, 10)
clr.MouseButton1Click:Connect(function()
onSet("None")
b.Text = "None"
b.BackgroundColor3 = Color3.fromRGB(30,30,40)
Keys.refresh()
end)
return b
end
-- cheat sheet toggle
local kbBtn = F.makeBtn(pgCam, "SHOW KEYBIND LIST", UDim2.new(0,8,0,kbY+58),
UDim2.new(0,250,0,30), Color3.fromRGB(45,45,80), nil, 10)
kbBtn.MouseButton1Click:Connect(function() Keys.setOverlay(not Keys.overlayOn) end)
bindBtn(258, kbY+61, 92, Keys.overlayKey, function(k) Keys.overlayKey = k end)
Keys.refreshUI = function()
if not kbBtn or not kbBtn.Parent then return end
kbBtn.Text = Keys.overlayOn and "HIDE KEYBIND LIST" or "SHOW KEYBIND LIST"
kbBtn.BackgroundColor3 = Keys.overlayOn and Color3.fromRGB(80,80,160) or Color3.fromRGB(45,45,80)
end
Keys.refreshUI()
F.makeLabel(pgCam, "BEAT ZOOM", UDim2.new(0,8,0,kbY+94), UDim2.new(0,90,0,24),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
bindBtn(100, kbY+94, 124, Keys.beatKey, function(k) Keys.beatKey = k end)
F.makeLabel(pgCam, "Toggles the lens pulsing in time with your BPM.",
UDim2.new(0,232,0,kbY+94), UDim2.new(1,-240,0,24), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
F.makeLabel(pgCam, "LIST OPACITY", UDim2.new(0,8,0,kbY+128), UDim2.new(0,200,0,20),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local kbOpLbl = F.makeLabel(pgCam, string.format("%.2f", Keys.opacity),
UDim2.new(0,320,0,kbY+128), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(kbOpLbl)
F.makeSlider(pgCam, UDim2.new(0,8,0,kbY+150), UDim2.new(1,-16,0,14), 0.1, 0.9, Keys.opacity, function(v)
Keys.opacity = v
kbOpLbl.Text = string.format("%.2f", v)
Keys.applyOpacity()
end)
-- intro transition binds
F.makeLabel(pgCam, "INTRO TRANSITIONS", UDim2.new(0,8,0,kbY+176), UDim2.new(1,-16,0,18),
Color3.fromRGB(255,200,60), Enum.TextXAlignment.Left, 10)
for i, e in ipairs(Keys.intro) do
local ry = kbY + 198 + (i-1) * 30
F.makeLabel(pgCam, e.label, UDim2.new(0,8,0,ry), UDim2.new(0,220,0,24),
Color3.fromRGB(180,180,195), Enum.TextXAlignment.Left, 10)
local entry = e
bindBtn(276, ry, 124, e.key, function(k) entry.key = k end)
end
-- cinematic preset binds
local kbPY = kbY + 198 + #Keys.intro * 30 + 10
F.makeLabel(pgCam, "CINEMATIC PRESETS", UDim2.new(0,8,0,kbPY), UDim2.new(1,-16,0,18),
Color3.fromRGB(255,200,60), Enum.TextXAlignment.Left, 10)
for i, e in ipairs(Keys.preset) do
local ry = kbPY + 22 + (i-1) * 30
F.makeLabel(pgCam, e.name, UDim2.new(0,8,0,ry), UDim2.new(0,220,0,24),
Color3.fromRGB(180,180,195), Enum.TextXAlignment.Left, 10)
local entry = e
bindBtn(276, ry, 124, e.key, function(k) entry.key = k end)
end
-- ============================================================
-- ── RIDE LOCK ──
-- ============================================================
local rdY = kbPY + 22 + #Keys.preset * 30 + 20
F.makeDivider(pgCam, rdY, Color3.fromRGB(120,220,255))
local rdTitle = F.makeLabel(pgCam, "RIDE LOCK", UDim2.new(0,8,0,rdY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(120,220,255), Enum.TextXAlignment.Left, 12)
rdTitle.Font = Enum.Font.GothamBold
F.makeLabel(pgCam, "Locks you to the object you're on and keeps you there at any speed. You can still walk around on it and jump — you land back on the same spot.",
UDim2.new(0,8,0,rdY+26), UDim2.new(1,-16,0,40), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local rdBtn = F.makeBtn(pgCam, "LOCK ON", UDim2.new(0,8,0,rdY+70),
UDim2.new(0,250,0,30), Color3.fromRGB(35,35,55), nil, 10)
rdBtn.MouseButton1Click:Connect(function() Ride.setEnabled(not Ride.enabled) end)
F.makeLabel(pgCam, "KEY", UDim2.new(0,266,0,rdY+70), UDim2.new(0,30,0,30),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local rdKeyBtn = F.makeBtn(pgCam, Ride.key, UDim2.new(0,298,0,rdY+70),
UDim2.new(0,64,0,30), Color3.fromRGB(25,35,48), nil, 10)
rdKeyBtn.MouseButton1Click:Connect(function()
rdKeyBtn.Text = "..."; rdKeyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
Ride.key = inp.KeyCode.Name
rdKeyBtn.Text = Ride.key
rdKeyBtn.BackgroundColor3 = Color3.fromRGB(25,35,48)
if Keys.refresh then Keys.refresh() end
conn:Disconnect()
end
end)
end)
Ride.refreshUI = function()
if not rdBtn or not rdBtn.Parent then return end
rdBtn.Text = Ride.enabled and "UNLOCK" or "LOCK ON"
rdBtn.BackgroundColor3 = Ride.enabled and Color3.fromRGB(60,160,220) or Color3.fromRGB(35,35,55)
end
Ride.refreshUI()
local rdStatus = F.makeLabel(pgCam, "", UDim2.new(0,8,0,rdY+106),
UDim2.new(1,-16,0,24), Color3.fromRGB(160,160,180), Enum.TextXAlignment.Left, 9)
rdStatus.TextWrapped = true
rdStatus.TextYAlignment = Enum.TextYAlignment.Top
task.spawn(function()
while true do
task.wait(0.25)
if not rdStatus.Parent then break end
if Ride.enabled and Ride.part then
local pingTxt
if Ride.pingSource == "AVERAGE" and Ride.pingOthers then
pingTxt = string.format("average ping %d ms (%d players)",
math.floor(Ride.pingMs + 0.5), Ride.pingCount)
else
pingTxt = string.format("ping %d ms (yours only)", math.floor(Ride.pingMs + 0.5))
end
rdStatus.Text = string.format("locked to %s | %.0f studs/s, turning %.0f°/s | %s\n%s | sending you %d ms ahead (%.1f studs)",
Ride.part.Name:sub(1, 14), Ride.smooth, Ride.turnDeg,
Ride.grounded and "on it" or "jumping", pingTxt,
math.floor(Ride.leadTime * 1000 + 0.5), Ride.leadStuds)
rdStatus.TextColor3 = Color3.fromRGB(120,220,255)
else
rdStatus.Text = "not locked"
rdStatus.TextColor3 = Color3.fromRGB(130,130,150)
end
end
end)
local rdToggles = {
{ key = "walkWith", on = "WALK ON IT: ON", off = "WALK ON IT: OFF" },
{ key = "jumpWith", on = "JUMP: ON", off = "JUMP: OFF" },
{ key = "clampEdges", on = "STAY ON: ON", off = "STAY ON: OFF" },
{ key = "shake", on = "SHAKE: ON", off = "SHAKE: OFF" },
{ key = "wind", on = "WIND: ON", off = "WIND: OFF" },
{ key = "speedFov", on = "SPEED FOV: ON", off = "SPEED FOV: OFF" },
{ key = "gLean", on = "G-LEAN: ON", off = "G-LEAN: OFF" },
{ key = "pingComp", on = "PING LEAD: ON", off = "PING LEAD: OFF" },
}
for i, t in ipairs(rdToggles) do
local col = (i - 1) % 3
local rowN = math.floor((i - 1) / 3)
local b = F.makeBtn(pgCam, Ride[t.key] and t.on or t.off,
UDim2.new(0, 8 + col * 132, 0, rdY + 130 + rowN * 32),
UDim2.new(0, 126, 0, 26),
Ride[t.key] and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55), nil, 9)
local ck, on, off = t.key, t.on, t.off
b.MouseButton1Click:Connect(function()
Ride[ck] = not Ride[ck]
b.Text = Ride[ck] and on or off
b.BackgroundColor3 = Ride[ck] and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55)
end)
end
local rdPingSrc = F.makeBtn(pgCam, "PING: " .. Ride.pingSource,
UDim2.new(0, 8 + 2 * 132, 0, rdY + 130 + 2 * 32), UDim2.new(0,126,0,26),
Color3.fromRGB(70,120,255), nil, 9)
rdPingSrc.MouseButton1Click:Connect(function()
Ride.pingSource = (Ride.pingSource == "AVERAGE") and "MINE" or "AVERAGE"
rdPingSrc.Text = "PING: " .. Ride.pingSource
Ride.warnedOthers, Ride.lastPingT, Ride.pingMs = false, 0, 0 -- start fresh with the new source
F.notify(Ride.pingSource == "AVERAGE" and "Using everyone's average ping (your own is usually more accurate)"
or "Using your ping — the number that decides the gap", "info")
end)
local rdRows = {
{ key = "edgeMargin", label = "EDGE MARGIN", min = 0, max = 5, fmt = "%.1f" },
{ key = "gravityScale", label = "JUMP GRAVITY", min = 0.2, max = 3, fmt = "%.2f" },
{ key = "pickRadius", label = "GRAB RADIUS", min = 4, max = 40, fmt = "%.0f" },
{ key = "maxSpeedAt", label = "FULL EFFECT AT (studs/s)", min = 20, max = 300, fmt = "%.0f" },
{ key = "minSpeed", label = "DEADZONE (studs/s)", min = 0, max = 40, fmt = "%.0f" },
{ key = "shakeScale", label = "SHAKE AMOUNT", min = 0, max = 3, fmt = "%.2f" },
{ key = "windScale", label = "WIND AMOUNT", min = 0, max = 2, fmt = "%.2f" },
{ key = "leanScale", label = "G-LEAN AMOUNT", min = 0, max = 3, fmt = "%.2f" },
{ key = "pingScale", label = "PING LEAD STRENGTH", min = 0, max = 2, fmt = "%.2f" },
{ key = "extraMs", label = "EXTRA LEAD (ms)", min = 0, max = 300, fmt = "%.0f" },
}
for i, row in ipairs(rdRows) do
local ry = rdY + 130 + math.ceil(#rdToggles / 3) * 32 + 6 + (i-1) * 42
F.makeLabel(pgCam, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,240,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pgCam, string.format(row.fmt, Ride[row.key]),
UDim2.new(0,316,0,ry), UDim2.new(0,80,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local ck, cfm = row.key, row.fmt
F.makeSlider(pgCam, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12),
row.min, row.max, Ride[row.key], function(v)
Ride[ck] = v
vlbl.Text = string.format(cfm, v)
end)
end
end
function F.buildTab_Maker(pages)
local pg2 = pages["🎬 MAKER"]
local makerTitle = F.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)
F.regText(makerTitle)
F.makeDivider(pg2, 28, Color3.fromRGB(0,200,255))
F.makeLabel(pg2, "ANIM ID", UDim2.new(0,8,0,34), UDim2.new(0,220,0,16), Color3.fromRGB(160,160,160))
F.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; F.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 = F.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
F.uiCorner(row, UDim.new(0,6))
local idIn = F.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 = F.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 = F.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
F.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 = F.makeBtn(pg2, "▶ PLAY SEQUENCE", UDim2.new(0,8,0,272), UDim2.new(0,176,0,30), Color3.fromRGB(60,190,120))
local seqStop = F.makeBtn(pg2, "■ STOP", UDim2.new(0,192,0,272), UDim2.new(0,90,0,30), Color3.fromRGB(190,50,50))
local seqClear = F.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"; F.notify("Add a step first", "warn"); return end
F.playSequence()
seqStatusLbl.Text = "▶ RUNNING"; seqStatusLbl.TextColor3 = Color3.fromRGB(80,220,120)
F.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()
F.stopSequence(); seqStatusLbl.Text = "● IDLE"; seqStatusLbl.TextColor3 = Color3.fromRGB(160,80,80)
end)
seqClear.MouseButton1Click:Connect(function()
F.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)
F.makeDivider(pg2, 330, Color3.fromRGB(0,200,255))
F.makeLabel(pg2, "🎵 SONG SECTIONS", UDim2.new(0,8,0,336), UDim2.new(1,-16,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
F.makeLabel(pg2, "Each section holds 4 animation IDs. Switch sections live with [ and ] keys.",
UDim2.new(0,8,0,354), UDim2.new(1,-16,0,16), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local secCurrentLbl = F.makeLabel(pg2, "§ " .. (sections[currentSectionIdx] and sections[currentSectionIdx].name or "—"),
UDim2.new(0,8,0,376), UDim2.new(1,-16,0,20), Color3.fromRGB(255,200,60), Enum.TextXAlignment.Left, 12)
F.makeLabel(pg2, "PREV", UDim2.new(0,8,0,400), UDim2.new(0,36,0,24), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 9)
local secPrevKeyBtn = F.makeBtn(pg2, sectionPrevKey, UDim2.new(0,46,0,400), UDim2.new(0,70,0,24), Color3.fromRGB(25,35,48), nil, 9)
secPrevKeyBtn.MouseButton1Click:Connect(function()
secPrevKeyBtn.Text = "..."; secPrevKeyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
sectionPrevKey = inp.KeyCode.Name; secPrevKeyBtn.Text = sectionPrevKey
secPrevKeyBtn.BackgroundColor3 = Color3.fromRGB(25,35,48); conn:Disconnect()
end
end)
end)
F.makeLabel(pg2, "NEXT", UDim2.new(0,124,0,400), UDim2.new(0,36,0,24), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 9)
local secNextKeyBtn = F.makeBtn(pg2, sectionNextKey, UDim2.new(0,162,0,400), UDim2.new(0,70,0,24), Color3.fromRGB(25,35,48), nil, 9)
secNextKeyBtn.MouseButton1Click:Connect(function()
secNextKeyBtn.Text = "..."; secNextKeyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
sectionNextKey = inp.KeyCode.Name; secNextKeyBtn.Text = sectionNextKey
secNextKeyBtn.BackgroundColor3 = Color3.fromRGB(25,35,48); conn:Disconnect()
end
end)
end)
local secScroll = Instance.new("ScrollingFrame")
secScroll.Size = UDim2.new(1,-16,0,320); secScroll.Position = UDim2.new(0,8,0,430)
secScroll.BackgroundColor3 = Color3.fromRGB(18,18,26); secScroll.BackgroundTransparency = 0.5
secScroll.BorderSizePixel = 0; secScroll.ScrollBarThickness = 3
secScroll.CanvasSize = UDim2.new(0,0,0,0); secScroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
secScroll.Parent = pg2; F.uiCorner(secScroll, UDim.new(0,8))
local secLayout = Instance.new("UIListLayout")
secLayout.Padding = UDim.new(0,6); secLayout.Parent = secScroll
local secPad = Instance.new("UIPadding")
secPad.PaddingTop = UDim.new(0,4); secPad.PaddingLeft = UDim.new(0,4); secPad.PaddingRight = UDim.new(0,4)
secPad.Parent = secScroll
local function buildSectionsUI()
for _, c in ipairs(secScroll:GetChildren()) do
if c:IsA("Frame") then c:Destroy() end
end
for idx, sec in ipairs(sections) do
local isActive = (idx == currentSectionIdx)
local card = Instance.new("Frame")
card.Size = UDim2.new(1,-4,0,120); card.BackgroundColor3 = isActive and Color3.fromRGB(40,25,55) or Color3.fromRGB(22,22,32)
card.BackgroundTransparency = 0.3; card.BorderSizePixel = 0; card.LayoutOrder = idx
card.Parent = secScroll; F.uiCorner(card, UDim.new(0,8))
if isActive then F.uiStroke(card, Color3.fromRGB(255,200,60), 1.5) end
local nameBox = F.makeBox(card, UDim2.new(0,4,0,4), UDim2.new(0,140,0,22), "Name", sec.name)
nameBox.Font = Enum.Font.GothamBold; nameBox.TextSize = 11
nameBox.FocusLost:Connect(function()
if nameBox.Text ~= "" then sec.name = nameBox.Text end
end)
local loadBtn = F.makeBtn(card, "▶ LOAD", UDim2.new(0,150,0,4), UDim2.new(0,60,0,22), Color3.fromRGB(60,190,120), nil, 9)
loadBtn.MouseButton1Click:Connect(function()
currentSectionIdx = idx
for _, row in ipairs(CurrentBindings) do
local newId = sec.ids[row.dir]
if newId and newId ~= "0" and newId ~= "" then
row.id = newId; F.destroyTrack(row.key)
end
end
F.notify("§ Loaded: " .. sec.name, "success")
buildSectionsUI()
secCurrentLbl.Text = "§ " .. sec.name
end)
local saveBtn = F.makeBtn(card, "💾 SAVE", UDim2.new(0,216,0,4), UDim2.new(0,60,0,22), Color3.fromRGB(70,120,255), nil, 9)
saveBtn.MouseButton1Click:Connect(function()
for _, row in ipairs(CurrentBindings) do
sec.ids[row.dir] = row.id
end
F.notify("Saved current binds to § " .. sec.name, "success")
end)
local delBtn = F.makeBtn(card, "✕", UDim2.new(0,282,0,4), UDim2.new(0,24,0,22), Color3.fromRGB(170,35,35), nil, 10)
delBtn.MouseButton1Click:Connect(function()
if #sections <= 1 then F.notify("Need at least one section", "warn"); return end
table.remove(sections, idx)
if currentSectionIdx > #sections then currentSectionIdx = #sections end
buildSectionsUI()
end)
local dirOrder = { "LEFT", "DOWN", "UP", "RIGHT" }
for di, dir in ipairs(dirOrder) do
local dy = 30 + (di-1) * 22
local col = ARROW_COLORS[dir] and ARROW_COLORS[dir].hit or Color3.fromRGB(255,255,255)
F.makeLabel(card, dir, UDim2.new(0,6,0,dy), UDim2.new(0,42,0,20), col, Enum.TextXAlignment.Left, 9)
local idBox = F.makeBox(card, UDim2.new(0,50,0,dy), UDim2.new(1,-58,0,20), "anim ID", sec.ids[dir] ~= "0" and sec.ids[dir] or "")
idBox.TextSize = 9
local capturedDir = dir
idBox.FocusLost:Connect(function()
local cleaned = idBox.Text:gsub("%D","")
sec.ids[capturedDir] = cleaned ~= "" and cleaned or "0"
end)
end
end
end
F.refreshSectionsUI = function()
secCurrentLbl.Text = "§ " .. (sections[currentSectionIdx] and sections[currentSectionIdx].name or "—")
buildSectionsUI()
end
local addSecBtn = F.makeBtn(pg2, "+ NEW SECTION", UDim2.new(0,8,0,756), UDim2.new(1,-16,0,28), Color3.fromRGB(35,35,55), nil, 11)
addSecBtn.MouseButton1Click:Connect(function()
local newIds = {}
for _, row in ipairs(CurrentBindings) do newIds[row.dir] = row.id end
table.insert(sections, { name = "Section " .. (#sections + 1), ids = newIds })
buildSectionsUI()
F.notify("Added new section", "success")
end)
local dupSecBtn = F.makeBtn(pg2, "📋 DUPLICATE CURRENT", UDim2.new(0,8,0,790), UDim2.new(1,-16,0,28), Color3.fromRGB(70,120,255), nil, 11)
dupSecBtn.MouseButton1Click:Connect(function()
local src = sections[currentSectionIdx]
if not src then return end
local copy = { name = src.name .. " (copy)", ids = {} }
for dir, id in pairs(src.ids) do copy.ids[dir] = id end
table.insert(sections, currentSectionIdx + 1, copy)
buildSectionsUI()
F.notify("Duplicated § " .. src.name, "success")
end)
buildSectionsUI()
end
function F.buildTab_Preview(pages)
local pg3v = pages["👁 PREVIEW"]
local pvTitle = F.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)
F.regText(pvTitle)
F.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)
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
F.uiCorner(viewport)
local vpStroke = F.uiStroke(viewport, Color3.fromRGB(255,80,180), 2)
F.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
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)
F.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 = F.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 F.buildPreviewClone(character); F.notify("Preview clone refreshed", "info") end
end)
F.makeDivider(pg3v, 362, Color3.fromRGB(255,80,180))
local pvStat1 = F.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 = F.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 = F.refreshStatusUI
F.refreshStatusUI = function()
if oldRefresh then oldRefresh() end
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
F.makeDivider(pg3v, 414, Color3.fromRGB(0,200,255))
local recentsTitle = F.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)
F.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.AutomaticCanvasSize = Enum.AutomaticSize.Y
recentsScroll.Parent = pg3v; F.uiCorner(recentsScroll)
local recentsLayout = Instance.new("UIListLayout")
recentsLayout.Padding = UDim.new(0,4); recentsLayout.Parent = recentsScroll
F.refreshRecentsUI = function()
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
F.uiCorner(row, UDim.new(0,6))
F.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 = F.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()
F.updatePreviewAnimation(r.id, r.label)
F.notify("Previewing "..r.label, "info")
end)
end
end
F.refreshRecentsUI()
end
function F.buildTab_Favorites(pages)
local pg4 = pages["❤ FAVES"]
local favTitle = F.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)
F.regText(favTitle)
F.makeDivider(pg4, 28, Color3.fromRGB(0,200,255))
local favNameBox = F.makeBox(pg4, UDim2.new(0,8,0,36), UDim2.new(0,180,0,28), "Name")
local favIdBox = F.makeBox(pg4, UDim2.new(0,196,0,36), UDim2.new(0,138,0,28), "Anim ID")
local favAddBtn = F.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.AutomaticCanvasSize = Enum.AutomaticSize.Y
favScroll.Parent = pg4; F.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
F.uiCorner(row, UDim.new(0,6))
F.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 = F.makeBtn(row, "▶", UDim2.new(1,-146,0,3), UDim2.new(0,34,0,26), Color3.fromRGB(0,180,230), nil, 11)
local addPlBtn = F.makeBtn(row, "➕LIST", UDim2.new(1,-108,0,3), UDim2.new(0,58,0,26), Color3.fromRGB(70,120,255), nil, 9)
local delBtn = F.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()
F.updatePreviewAnimation(f.id, f.name)
F.pushRecent(f.id, f.name)
F.notify("Previewing favorite: "..f.name, "info")
end)
addPlBtn.MouseButton1Click:Connect(function()
table.insert(playlist, { name = f.name, id = f.id, delay = 1 })
if F.refreshPlaylistUI then F.refreshPlaylistUI() end
F.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 F.notify("Enter an anim ID to save", "warn"); return end
table.insert(favorites, { name = nm, id = id })
favNameBox.Text = ""; favIdBox.Text = ""
refreshFavUI()
F.notify("Saved to favorites: "..nm, "success")
end)
refreshFavUI()
end
function F.buildTab_Playlist(pages)
local pg5 = pages["🎶 LIST"]
local plTitle = F.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)
F.regText(plTitle)
F.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.AutomaticCanvasSize = Enum.AutomaticSize.Y
plScroll.Parent = pg5; F.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
F.refreshPlaylistUI = function()
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
F.uiCorner(row, UDim.new(0,6))
F.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 = F.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)
F.refreshPlaylistUI()
end)
end
end
F.refreshPlaylistUI()
local plAddBox = F.makeBox(pg5, UDim2.new(0,8,0,300), UDim2.new(0,220,0,26), "Animation ID to add")
local plAddBtn = F.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 F.notify("Enter an anim ID", "warn"); return end
table.insert(playlist, { name = "Track "..(#playlist+1), id = id, delay = 1 })
plAddBox.Text = ""
F.refreshPlaylistUI()
end)
local plPlay = F.makeBtn(pg5, "▶ PLAY", UDim2.new(0,8,0,332), UDim2.new(0,96,0,28), Color3.fromRGB(60,190,120), nil, 11)
local plPrev = F.makeBtn(pg5, "⏮ PREV", UDim2.new(0,110,0,332), UDim2.new(0,86,0,28), Color3.fromRGB(70,120,255), nil, 11)
local plNext = F.makeBtn(pg5, "NEXT ⏭", UDim2.new(0,202,0,332), UDim2.new(0,86,0,28), Color3.fromRGB(70,120,255), nil, 11)
local plStop = F.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 F.notify("Playlist is empty", "warn"); return end
F.playPlaylist(); F.notify("Playlist started", "info")
end)
plPrev.MouseButton1Click:Connect(F.prevPlaylistTrack)
plNext.MouseButton1Click:Connect(F.nextPlaylistTrack)
plStop.MouseButton1Click:Connect(function() F.stopPlaylist(); F.refreshPlaylistUI() end)
end
function F.buildTab_FX(pages)
local pgFx = pages["🎛 FX"]
local vinylTitle = F.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)
F.regText(vinylTitle)
F.makeDivider(pgFx, 28, Color3.fromRGB(0,200,255))
F.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; F.uiCorner(vinylHolder, UDim.new(1,0))
local vinylHolderStroke = F.uiStroke(vinylHolder, Color3.fromRGB(255,80,180), 2)
F.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
local vinylRing = Instance.new("Frame")
vinylRing.Size = UDim2.new(1,0,1,0); vinylRing.BackgroundTransparency = 1
vinylRing.Parent = vinylHolder
F.uiCorner(vinylRing, UDim.new(1,0))
F.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
F.uiCorner(vinylCenter, UDim.new(1,0)); F.regBg(vinylCenter)
local vinylIdBox = F.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)
F.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)
F.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 = F.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)
F.makeDivider(pgFx, 190, Color3.fromRGB(255,80,180))
local crtTitle = F.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)
F.regText(crtTitle)
F.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 = F.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
F.notify(crtEnabled and "CRT mode enabled" or "CRT mode disabled", "info")
end)
F.makeDivider(pgFx, 284, Color3.fromRGB(0,200,255))
local specTitle = F.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)
F.regText(specTitle)
local specStatusLbl = F.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)
F.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; F.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
F.uiCorner(bar, UDim.new(0,2))
table.insert(specBars, bar)
end
local micBtn = F.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 = F.setupMicSpectrogram()
if success then
specStatusLbl.Text = "● mic connected"; specStatusLbl.TextColor3 = Color3.fromRGB(80,220,120)
F.notify("Mic spectrogram connected", "success")
else
specStatusLbl.Text = "● using fallback"; specStatusLbl.TextColor3 = Color3.fromRGB(255,180,60)
F.notify("Mic unavailable here — using animation-synced fallback", "warn")
end
end)
F.makeDivider(pgFx, 456, Color3.fromRGB(255,80,180))
local bpmDetectTitle = F.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)
F.regText(bpmDetectTitle)
F.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 = F.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)
F.refreshBpmDetectorUI = function()
if detectedBPM then
detectedLbl.Text = string.format("Detected: %d BPM (from %d taps)", detectedBPM, #bpmDetectTaps)
end
end
F.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 = F.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 = F.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 = F.detectBpmFromIdleLength(beatsPerIdleLoop)
if bpm then
F.refreshBpmDetectorUI()
F.notify("Detected "..bpm.." BPM from idle length", "success")
else
F.notify("Load an idle animation first", "warn")
end
end)
local resetTapBtn = F.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"
F.notify("Tap history cleared", "info")
end)
local applyDetectedBtn = F.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
F.setIdleBPM(detectedBPM)
F.notify("Idle BPM set to "..detectedBPM, "success")
else
F.notify("No BPM detected yet — tap some keys or use idle length", "warn")
end
end)
F.makeDivider(pgFx, 616, Color3.fromRGB(0,200,255))
local musicTitle = F.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)
F.regText(musicTitle)
F.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)
F.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 = F.makeBox(pgFx, UDim2.new(0,8,0,698), UDim2.new(1,-16,0,28), "Audio asset ID (e.g. 1234567890)")
local musicPlayBtn = F.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 = F.makeBtn(pgFx, "■ STOP", UDim2.new(0,236,0,730), UDim2.new(0,160,0,30), Color3.fromRGB(190,50,50), nil, 11)
musicStatusLbl = F.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 F.notify("Enter an audio asset ID first", "warn"); return end
F.playMusicTrack(id)
F.notify("Requested track "..id.." for the whole server", "info")
end)
musicStopBtn.MouseButton1Click:Connect(function()
F.stopMusicTrack()
F.notify("Stopped shared music", "info")
end)
end
function F.buildTab_Video(pages)
local pg = pages["🎞 VIDEO"]
local C = Color3.fromRGB
local title = F.makeLabel(pg, "VIDEO PLAYER", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,22),
C(255,80,180), Enum.TextXAlignment.Left, 14)
title.Font = Enum.Font.GothamBlack
F.regText(title)
F.makeLabel(pg, "Plays videos from your computer. Only you can see them.",
UDim2.new(0,8,0,28), UDim2.new(1,-16,0,16), C(130,130,150), Enum.TextXAlignment.Left, 9)
-- ── what this executor can do ──
local caps = F.makeLabel(pg, "", UDim2.new(0,8,0,48), UDim2.new(1,-16,0,16),
C(160,160,180), Enum.TextXAlignment.Left, 10)
local function refreshCaps()
local a = Vid.api.asset and "YES" or "NO"
local l = Vid.api.list and "YES" or "NO"
caps.Text = "local video support: " .. a .. " | folder listing: " .. l
caps.TextColor3 = Vid.api.asset and C(120,230,150) or C(255,120,90)
end
refreshCaps()
-- ── the drop box ──
F.makeDivider(pg, 70, THEMES[currentThemeName].accent)
local dropHdr = F.makeLabel(pg, "YOUR VIDEOS", UDim2.new(0,8,0,76), UDim2.new(0,160,0,18),
C(255,80,180), Enum.TextXAlignment.Left, 11)
dropHdr.Font = Enum.Font.GothamBold
F.regText(dropHdr)
local howTo = F.makeLabel(pg,
"Drag a .webm file into this folder in Windows Explorer, then press REFRESH:\n"
.. "your executor's workspace / " .. Vid.folder,
UDim2.new(0,8,0,96), UDim2.new(1,-16,0,32), C(200,200,215), Enum.TextXAlignment.Left, 9)
howTo.TextWrapped = true
local refresh = F.makeBtn(pg, "REFRESH", UDim2.new(1,-100,0,74), UDim2.new(0,92,0,22), C(70,120,255), nil, 9)
local list = Instance.new("ScrollingFrame")
list.Size = UDim2.new(1,-16,0,130); list.Position = UDim2.new(0,8,0,132)
list.BackgroundColor3 = C(12,11,18); list.BackgroundTransparency = 0.25
list.BorderSizePixel = 0; list.ScrollBarThickness = 3
list.CanvasSize = UDim2.new(0,0,0,0); list.AutomaticCanvasSize = Enum.AutomaticSize.Y
list.Parent = pg
F.uiCorner(list, UDim.new(0,8))
local ll = Instance.new("UIListLayout"); ll.Padding = UDim.new(0,3); ll.Parent = list
local lp = Instance.new("UIPadding")
lp.PaddingTop = UDim.new(0,4); lp.PaddingLeft = UDim.new(0,4); lp.PaddingRight = UDim.new(0,4)
lp.Parent = list
local function row(text, color, order)
local l = F.makeLabel(list, text, UDim2.new(0,0,0,0), UDim2.new(1,-8,0,0),
color, Enum.TextXAlignment.Left, 9)
l.AutomaticSize = Enum.AutomaticSize.Y
l.TextWrapped = true
l.LayoutOrder = order
return l
end
local function fill()
for _, c in ipairs(list:GetChildren()) do
if c:IsA("GuiObject") then c:Destroy() end
end
if not Vid.api.list then
row("Your executor can't list folders. Type the file name below instead.", C(255,170,80), 1)
return
end
local files = Vid.listFiles()
if #files == 0 then
row("Nothing here yet. Drop a .webm into the folder above and press REFRESH.", C(140,140,160), 1)
return
end
for i, f in ipairs(files) do
local playable = (f.ext == "webm")
local b = F.makeBtn(list,
(playable and "▶ " or "⚠ ") .. f.name .. (playable and "" or " (convert to .webm)"),
UDim2.new(0,0,0,0), UDim2.new(1,-8,0,26),
playable and C(34,32,48) or C(55,35,30), nil, 10)
b.TextXAlignment = Enum.TextXAlignment.Left
b.LayoutOrder = i
local pad = Instance.new("UIPadding"); pad.PaddingLeft = UDim.new(0,10); pad.Parent = b
local path = f.path
b.MouseButton1Click:Connect(function() Vid.play(path) end)
end
end
refresh.MouseButton1Click:Connect(function()
Vid.resolveApi(); refreshCaps(); fill()
end)
fill()
-- ── manual entry: a file name, or a Roblox video ID ──
local manual = F.makeBox(pg, UDim2.new(0,8,0,270), UDim2.new(1,-114,0,28),
"file name (clip.webm) or a Roblox video ID", Vid.lastFile ~= "" and Vid.lastFile:match("[^/\\]+$") or "")
local manualGo = F.makeBtn(pg, "PLAY", UDim2.new(1,-100,0,270), UDim2.new(0,92,0,28), C(60,190,120), nil, 11)
manualGo.MouseButton1Click:Connect(function()
local t = manual.Text:gsub("^%s+", ""):gsub("%s+$", "")
if t == "" then return end
if t:match("^%d+$") then
Vid.play(t, true) -- a Roblox-hosted video
else
local path = t:find("/") and t or (Vid.folder .. "/" .. t)
Vid.play(path)
end
end)
Vid.statusLbl = F.makeLabel(pg, "pick a video", UDim2.new(0,8,0,302), UDim2.new(1,-16,0,28),
C(160,160,180), Enum.TextXAlignment.Left, 10)
Vid.statusLbl.TextWrapped = true
-- ── transport ──
local trY = 334
local function tb(txt, x, w, col, fn)
local b = F.makeBtn(pg, txt, UDim2.new(0,x,0,trY), UDim2.new(0,w,0,28), col, nil, 10)
b.MouseButton1Click:Connect(fn)
return b
end
tb("PLAY / PAUSE", 8, 126, C(60,190,120), Vid.togglePause)
tb("RESTART", 140, 90, C(45,40,60), Vid.restart)
tb("STOP", 236, 70, C(190,50,50), Vid.stop)
local loopB
loopB = tb(Vid.looped and "LOOP: ON" or "LOOP: OFF", 312, 84, Vid.looped and C(70,120,255) or C(45,40,60), function()
Vid.looped = not Vid.looped
if Vid.frame then Vid.frame.Looped = Vid.looped end
loopB.Text = Vid.looped and "LOOP: ON" or "LOOP: OFF"
loopB.BackgroundColor3 = Vid.looped and C(70,120,255) or C(45,40,60)
end)
Vid.timeLbl = F.makeLabel(pg, "", UDim2.new(0,8,0,trY+32), UDim2.new(1,-16,0,16),
C(200,200,215), Enum.TextXAlignment.Left, 10)
Vid.timeLbl.Font = Enum.Font.Code
-- ── display mode ──
local dY = trY + 56
F.makeLabel(pg, "SHOW IT AS", UDim2.new(0,8,0,dY), UDim2.new(1,-16,0,16),
C(160,160,160), Enum.TextXAlignment.Left, 10)
local modes = { { "WINDOW", "Window" }, { "FULLSCREEN", "Fullscreen" }, { "BACKDROP", "Backdrop" } }
local modeBtns = {}
local function syncModes()
local shown = Vid.stage.on and "BACKDROP" or Vid.display
for k, b in pairs(modeBtns) do
b.BackgroundColor3 = (k == shown) and C(255,80,180) or C(35,35,55)
b.AutoButtonColor = not Vid.stage.on
end
end
Vid.syncModes = syncModes
for i, m in ipairs(modes) do
local b = F.makeBtn(pg, m[2], UDim2.new(0, 8 + (i-1) * 132, 0, dY + 20), UDim2.new(0,126,0,26), C(35,35,55), nil, 10)
modeBtns[m[1]] = b
b.MouseButton1Click:Connect(function()
if Vid.stage.on then
F.notify("The stage keeps the video as the backdrop — turn it off to change this", "info")
return
end
Vid.display = m[1]; syncModes(); Vid.applyLayout()
end)
end
syncModes()
F.makeLabel(pg, "Backdrop plays full screen behind every other panel.",
UDim2.new(0,8,0,dY+50), UDim2.new(1,-16,0,14), C(120,120,140), Enum.TextXAlignment.Left, 9)
local sliders = {
{ key = "volume", label = "VOLUME", min = 0, max = 1, fmt = "%.2f" },
{ key = "opacity", label = "SEE-THROUGH", min = 0, max = 0.9, fmt = "%.2f" },
{ key = "scale", label = "WINDOW SIZE", min = 0.5, max = 2.2, fmt = "%.2f" },
}
for i, s in ipairs(sliders) do
local ry = dY + 72 + (i-1) * 42
F.makeLabel(pg, s.label, UDim2.new(0,8,0,ry), UDim2.new(0,200,0,18), C(160,160,160), Enum.TextXAlignment.Left, 10)
local v = F.makeLabel(pg, string.format(s.fmt, Vid[s.key]), UDim2.new(0,306,0,ry), UDim2.new(0,90,0,18),
C(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(v)
local ck, cf = s.key, s.fmt
F.makeSlider(pg, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12), s.min, s.max, Vid[s.key], function(val)
Vid[ck] = val
v.Text = string.format(cf, val)
if ck == "volume" and Vid.frame then Vid.frame.Volume = val end
Vid.applyLayout()
end)
end
-- ── video stage ──
local sgY = dY + 72 + #sliders * 42 + 4
F.makeDivider(pg, sgY, THEMES[currentThemeName].accent)
local sgT = F.makeLabel(pg, "VIDEO STAGE", UDim2.new(0,8,0,sgY+6), UDim2.new(1,-16,0,18),
C(255,80,180), Enum.TextXAlignment.Left, 11)
sgT.Font = Enum.Font.GothamBold
F.regText(sgT)
local sgBody = F.makeLabel(pg,
"Your video replaces the whole world. Only you and your opponent stay, in their normal colours. "
.. "Set your opponent in the CAM tab.",
UDim2.new(0,8,0,sgY+26), UDim2.new(1,-16,0,28), C(130,130,150), Enum.TextXAlignment.Left, 9)
sgBody.TextWrapped = true
local sgBtn = F.makeBtn(pg, "VIDEO STAGE OFF", UDim2.new(0,8,0,sgY+58), UDim2.new(1,-16,0,32), C(35,35,55), nil, 11)
sgBtn.MouseButton1Click:Connect(function()
if Vid.stage.on then Vid.stopStage() else Vid.startStage() end
end)
local sgInfo = F.makeLabel(pg, "", UDim2.new(0,8,0,sgY+92), UDim2.new(1,-16,0,16),
C(160,160,180), Enum.TextXAlignment.Left, 9)
Vid.refreshStageUI = function()
if not sgBtn.Parent then return end
sgBtn.Text = Vid.stage.on and "VIDEO STAGE ON" or "VIDEO STAGE OFF"
sgBtn.BackgroundColor3 = Vid.stage.on and C(255,80,180) or C(35,35,55)
if not Vid.stage.on then sgInfo.Text = ""; return end
-- what the stage is actually drawing right now
local function part(e, label, expected)
if not expected then return label .. ": not set" end
if not e then return label .. ": copy FAILED" end
return string.format("%s: %d parts", label, #e.map)
end
local me, op = Vid.stage.me, Vid.stage.opp
sgInfo.Text = part(me, "you", character ~= nil) .. " | "
.. part(op, "opponent", currentTarget and currentTarget.Character ~= nil)
local ok = me and #me.map > 0
sgInfo.TextColor3 = ok and C(120,230,150) or C(255,120,90)
end
Vid.refreshStageUI()
-- ── converting ──
local cvY = sgY + 114
F.makeDivider(pg, cvY, THEMES[currentThemeName].accent)
local cvT = F.makeLabel(pg, "HAVE AN .MP4?", UDim2.new(0,8,0,cvY+6), UDim2.new(1,-16,0,18),
C(255,170,80), Enum.TextXAlignment.Left, 11)
cvT.Font = Enum.Font.GothamBold
local cvBody = F.makeLabel(pg,
"Roblox only plays .webm. Any free video converter works — pick WebM (VP9). "
.. "Or with ffmpeg, run this and drop the result in the folder:",
UDim2.new(0,8,0,cvY+26), UDim2.new(1,-16,0,30), C(200,200,215), Enum.TextXAlignment.Left, 9)
cvBody.TextWrapped = true
local cmd = F.makeBox(pg, UDim2.new(0,8,0,cvY+60), UDim2.new(1,-16,0,26), "",
"ffmpeg -i input.mp4 -c:v libvpx-vp9 -b:v 2M -c:a libopus output.webm")
cmd.TextSize = 9
cmd.Font = Enum.Font.Code
cmd.ClearTextOnFocus = false
cmd.TextEditable = false
F.makeLabel(pg, "Short clips at 720p or below load most reliably.",
UDim2.new(0,8,0,cvY+90), UDim2.new(1,-16,0,14), C(120,120,140), Enum.TextXAlignment.Left, 9)
-- ── black screen fade ──
local boY = cvY + 116
F.makeDivider(pg, boY, C(255,255,255))
local boT = F.makeLabel(pg, "BLACK SCREEN FADE", UDim2.new(0,8,0,boY+6), UDim2.new(1,-16,0,18),
C(235,235,245), Enum.TextXAlignment.Left, 11)
boT.Font = Enum.Font.GothamBold
F.makeLabel(pg, "Smoothly fades to black, holds a moment, then slowly fades back in.",
UDim2.new(0,8,0,boY+26), UDim2.new(1,-16,0,14), C(130,130,150), Enum.TextXAlignment.Left, 9)
local boTest = F.makeBtn(pg, "TRY IT", UDim2.new(0,8,0,boY+46), UDim2.new(0,250,0,28), C(20,20,24), nil, 11)
boTest.MouseButton1Click:Connect(Vid.blackout)
F.makeLabel(pg, "KEY", UDim2.new(0,266,0,boY+46), UDim2.new(0,30,0,28), C(160,160,160), Enum.TextXAlignment.Left, 10)
local boKey = F.makeBtn(pg, Vid.boKey, UDim2.new(0,298,0,boY+46), UDim2.new(0,64,0,28), C(25,35,48), nil, 11)
boKey.MouseButton1Click:Connect(function()
boKey.Text = "..."; boKey.BackgroundColor3 = C(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
Vid.boKey = inp.KeyCode.Name
boKey.Text = Vid.boKey
boKey.BackgroundColor3 = C(25,35,48)
if Keys.refresh then Keys.refresh() end
conn:Disconnect()
end
end)
end)
local boRows = {
{ key = "boFadeIn", label = "FADE TO BLACK (s)", min = 0.05, max = 2, fmt = "%.2f" },
{ key = "boHold", label = "STAY BLACK (s)", min = 0, max = 3, fmt = "%.2f" },
{ key = "boFadeOut", label = "FADE BACK IN (s)", min = 0.2, max = 6, fmt = "%.2f" },
}
for i, s in ipairs(boRows) do
local ry = boY + 84 + (i-1) * 42
F.makeLabel(pg, s.label, UDim2.new(0,8,0,ry), UDim2.new(0,200,0,18), C(160,160,160), Enum.TextXAlignment.Left, 10)
local v = F.makeLabel(pg, string.format(s.fmt, Vid[s.key]), UDim2.new(0,306,0,ry), UDim2.new(0,90,0,18),
C(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(v)
local ck, cf = s.key, s.fmt
F.makeSlider(pg, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12), s.min, s.max, Vid[s.key], function(val)
Vid[ck] = val
v.Text = string.format(cf, val)
end)
end
end
function F.buildTab_Result(pages)
local pg = pages["🏆 RESULT"]
local C = Color3.fromRGB
local title = F.makeLabel(pg, "WIN / LOSE", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,22),
C(255,80,180), Enum.TextXAlignment.Left, 14)
title.Font = Enum.Font.GothamBlack
F.regText(title)
F.makeLabel(pg, "You decide how the battle ends. Press a button or its key.",
UDim2.new(0,8,0,28), UDim2.new(1,-16,0,16), C(130,130,150), Enum.TextXAlignment.Left, 9)
-- two big buttons
local winBtn = F.makeBtn(pg, "WIN", UDim2.new(0,8,0,52), UDim2.new(0.5,-12,0,70), C(210,160,30), C(20,20,25), 22)
winBtn.Font = Enum.Font.GothamBlack
winBtn.MouseButton1Click:Connect(Result.win)
local loseBtn = F.makeBtn(pg, "LOSE", UDim2.new(0.5,4,0,52), UDim2.new(0.5,-12,0,70), C(190,40,50), nil, 22)
loseBtn.Font = Enum.Font.GothamBlack
loseBtn.MouseButton1Click:Connect(Result.lose)
-- keybinds under each
local function keyBtn(x, field)
local b = F.makeBtn(pg, "KEY: " .. Result[field], UDim2.new(x == 0 and 0 or 0.5, x == 0 and 8 or 4, 0, 128),
UDim2.new(0.5,-12,0,26), C(25,35,48), nil, 10)
b.MouseButton1Click:Connect(function()
b.Text = "press a key..."; b.BackgroundColor3 = C(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
Result[field] = inp.KeyCode.Name
b.Text = "KEY: " .. Result[field]
b.BackgroundColor3 = C(25,35,48)
if Keys.refresh then Keys.refresh() end
conn:Disconnect()
end
end)
end)
end
keyBtn(0, "winKey")
keyBtn(1, "loseKey")
local bannerBtn = F.makeBtn(pg, "", UDim2.new(0,8,0,162), UDim2.new(1,-16,0,26), C(35,35,55), nil, 10)
local function syncBanner()
bannerBtn.Text = Result.banner and "YOU WIN / YOU LOSE TEXT: ON" or "YOU WIN / YOU LOSE TEXT: OFF"
bannerBtn.BackgroundColor3 = Result.banner and C(60,190,120) or C(35,35,55)
end
bannerBtn.MouseButton1Click:Connect(function() Result.banner = not Result.banner; syncBanner() end)
syncBanner()
-- ── win ──
local wY = 200
F.makeDivider(pg, wY, C(255,215,70))
local wT = F.makeLabel(pg, "WIN", UDim2.new(0,8,0,wY+6), UDim2.new(1,-16,0,18), C(255,215,70), Enum.TextXAlignment.Left, 11)
wT.Font = Enum.Font.GothamBold
local cheerBox = F.makeBox(pg, UDim2.new(0,8,0,wY+28), UDim2.new(1,-16,0,28),
"cheer animation ID (blank = your avatar's own cheer)", Result.cheerId)
cheerBox.FocusLost:Connect(function()
Result.cheerId = (cheerBox.Text:gsub("%D",""))
cheerBox.Text = Result.cheerId
end)
local loopBtn = F.makeBtn(pg, "", UDim2.new(0,8,0,wY+62), UDim2.new(0,160,0,26), C(35,35,55), nil, 10)
local function syncLoop()
loopBtn.Text = Result.cheerLoop and "KEEP CHEERING: ON" or "KEEP CHEERING: OFF"
loopBtn.BackgroundColor3 = Result.cheerLoop and C(210,160,30) or C(35,35,55)
end
loopBtn.MouseButton1Click:Connect(function() Result.cheerLoop = not Result.cheerLoop; syncLoop() end)
syncLoop()
F.makeLabel(pg, "Loops the cheer for the time below.", UDim2.new(0,176,0,wY+62), UDim2.new(1,-184,0,26),
C(120,120,140), Enum.TextXAlignment.Left, 9)
-- ── lose ──
local lY = wY + 142
F.makeDivider(pg, lY, C(255,70,70))
local lT = F.makeLabel(pg, "LOSE", UDim2.new(0,8,0,lY+6), UDim2.new(1,-16,0,18), C(255,90,90), Enum.TextXAlignment.Left, 11)
lT.Font = Enum.Font.GothamBold
local sitBox = F.makeBox(pg, UDim2.new(0,8,0,lY+28), UDim2.new(1,-16,0,28),
"sitting animation ID (blank = your avatar's own sit)", Result.sitId)
sitBox.FocusLost:Connect(function()
Result.sitId = (sitBox.Text:gsub("%D",""))
sitBox.Text = Result.sitId
end)
F.makeLabel(pg, "FLING DIRECTION", UDim2.new(0,8,0,lY+62), UDim2.new(1,-16,0,16), C(160,160,160), Enum.TextXAlignment.Left, 10)
local dirs = { { "AWAY", "Away from opponent" }, { "BACK", "Backwards" }, { "RANDOM", "Random" } }
local dirBtns = {}
local function syncDirs()
for k, b in pairs(dirBtns) do
b.BackgroundColor3 = (k == Result.direction) and C(190,40,50) or C(35,35,55)
end
end
for i, d in ipairs(dirs) do
local b = F.makeBtn(pg, d[2], UDim2.new(0, 8 + (i-1) * 132, 0, lY + 82), UDim2.new(0,126,0,26), C(35,35,55), nil, 9)
dirBtns[d[1]] = b
b.MouseButton1Click:Connect(function() Result.direction = d[1]; syncDirs() end)
end
syncDirs()
-- speed shows studs/s and mph side by side
local rows = {
{ key = "flingSpeed", label = "FLING SPEED", min = 10, max = 300, speed = true },
{ key = "lift", label = "UPWARD KICK", min = 0, max = 150, fmt = "%.0f studs/s" },
{ key = "spin", label = "TUMBLE", min = 0, max = 40, fmt = "%.1f" },
{ key = "getUpAfter", label = "GET UP AFTER", min = 0.5, max = 8, fmt = "%.1fs" },
{ key = "cheerTime", label = "CHEER LENGTH (WIN)", min = 1, max = 10, fmt = "%.1fs" },
}
local function fmtRow(r, v)
if r.speed then return string.format("%.0f studs/s ≈ %.0f mph", v, Result.mph(v)) end
return string.format(r.fmt, v)
end
for i, r in ipairs(rows) do
local ry = lY + 118 + (i-1) * 42
F.makeLabel(pg, r.label, UDim2.new(0,8,0,ry), UDim2.new(0,180,0,18), C(160,160,160), Enum.TextXAlignment.Left, 10)
local v = F.makeLabel(pg, fmtRow(r, Result[r.key]), UDim2.new(0,186,0,ry), UDim2.new(0,210,0,18),
C(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(v)
local rr = r
F.makeSlider(pg, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12), r.min, r.max, Result[r.key], function(val)
Result[rr.key] = val
v.Text = fmtRow(rr, val)
end)
end
F.makeLabel(pg, "Your own physics, so other players see the fling. Careful near edges: it can throw you off the map.",
UDim2.new(0,8,0,lY + 118 + #rows * 42 + 4), UDim2.new(1,-16,0,28), C(255,170,80), Enum.TextXAlignment.Left, 9).TextWrapped = true
end
function F.buildTab_Settings(pages)
local pg6 = pages["⚙ SETTINGS"]
local legsTitle = F.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)
F.regText(legsTitle)
F.makeDivider(pg6, 30, Color3.fromRGB(0,200,255))
F.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 = F.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
F.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
F.notify(legsLocked and "Legs locked" or "Legs unlocked", "info")
end)
F.makeDivider(pg6, 114, Color3.fromRGB(255,80,180))
local faceTitle = F.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)
F.regText(faceTitle)
F.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)
F.makeLabel(pg6, dir, UDim2.new(0,8,0,y), UDim2.new(0,52,0,28), col, Enum.TextXAlignment.Left)
local fb = F.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
F.makeDivider(pg6, 320, Color3.fromRGB(0,200,255))
local themeTitle = F.makeLabel(pg6, "🎨 THEME", UDim2.new(0,8,0,326), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
F.regText(themeTitle)
local themeNames = { "Pink", "Cyan", "Purple", "RGB", "Mono" }
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),
Mono = Color3.fromRGB(235,235,240) }
for i, tn in ipairs(themeNames) do
local tx = 8 + (i-1) * 82
local tbtn = F.makeBtn(pg6, tn, UDim2.new(0, tx, 0, 356), UDim2.new(0, 76, 0, 32), themeSwatch[tn], Color3.fromRGB(20,20,20), 10)
tbtn.MouseButton1Click:Connect(function() F.setTheme(tn) end)
end
F.makeDivider(pg6, 400, Color3.fromRGB(0,200,255))
local rpTitle = F.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)
F.regText(rpTitle)
F.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)
local rpBtn = F.makeBtn(pg6, rpModeEnabled and "⚔ RP MODE ON" or "📴 RP MODE OFF",
UDim2.new(0,8,0,450), UDim2.new(0,160,0,32),
rpModeEnabled and THEMES[currentThemeName].accent or 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)
local targetBox = F.makeBox(pg6, UDim2.new(0,176,0,450), UDim2.new(1,-184,0,32), "Target Player Name...")
targetBox.FocusLost:Connect(function()
F.setTargetPlayer(targetBox.Text)
end)
F.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 = F.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)
F.regText(rpValLbl)
F.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)
-- spacing owns 530 through ~1140
F.buildSpacingUI(pg6, 530)
-- ============================================================
-- ── SAVE / LOAD ──
-- ============================================================
local svY = 1180
F.makeDivider(pg6, svY, Color3.fromRGB(120,220,160))
local svTitle = F.makeLabel(pg6, "SAVE / LOAD", UDim2.new(0,8,0,svY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(120,220,160), Enum.TextXAlignment.Left, 12)
svTitle.Font = Enum.Font.GothamBold
local svStatus = F.makeLabel(pg6, "", UDim2.new(0,8,0,svY+28),
UDim2.new(1,-16,0,32), Color3.fromRGB(160,160,180), Enum.TextXAlignment.Left, 9)
svStatus.TextWrapped = true
-- slot picker
F.makeLabel(pg6, "SLOT", UDim2.new(0,8,0,svY+66), UDim2.new(0,40,0,26),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local slotBtns = {}
for s = 1, Save.slots do
local b = F.makeBtn(pg6, tostring(s), UDim2.new(0, 50 + (s-1) * 46, 0, svY+66),
UDim2.new(0, 40, 0, 26), Color3.fromRGB(35,35,55), nil, 11)
slotBtns[s] = b
b.MouseButton1Click:Connect(function()
Save.slot = s
if Save.refreshUI then Save.refreshUI() end
end)
end
local svSave = F.makeBtn(pg6, "SAVE", UDim2.new(0,8,0,svY+100),
UDim2.new(0,126,0,30), Color3.fromRGB(50,170,110), nil, 11)
local svLoad = F.makeBtn(pg6, "LOAD", UDim2.new(0,140,0,svY+100),
UDim2.new(0,126,0,30), Color3.fromRGB(70,120,255), nil, 11)
local svWipe = F.makeBtn(pg6, "CLEAR SLOT", UDim2.new(0,272,0,svY+100),
UDim2.new(0,124,0,30), Color3.fromRGB(170,35,35), nil, 10)
local svAuto = F.makeBtn(pg6, "AUTOSAVE: ON", UDim2.new(0,8,0,svY+138),
UDim2.new(0,190,0,26), Color3.fromRGB(60,190,120), nil, 9)
F.makeLabel(pg6, "Checks every 20s, only writes when something changed.",
UDim2.new(0,206,0,svY+138), UDim2.new(1,-214,0,26), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
-- clipboard export / import, for executors without file access
F.makeLabel(pg6, "EXPORT / IMPORT", UDim2.new(0,8,0,svY+176), UDim2.new(1,-16,0,18),
Color3.fromRGB(120,220,160), Enum.TextXAlignment.Left, 10)
local ioBox = F.makeBox(pg6, UDim2.new(0,8,0,svY+198), UDim2.new(1,-16,0,30),
"Paste exported settings here, then press IMPORT")
ioBox.TextTruncate = Enum.TextTruncate.AtEnd
local svExport = F.makeBtn(pg6, "EXPORT", UDim2.new(0,8,0,svY+234),
UDim2.new(0,190,0,28), Color3.fromRGB(45,40,60), nil, 10)
local svImport = F.makeBtn(pg6, "IMPORT", UDim2.new(0,206,0,svY+234),
UDim2.new(0,190,0,28), Color3.fromRGB(45,40,60), nil, 10)
Save.refreshUI = function()
if not svStatus or not svStatus.Parent then return end
if Save.available then
local where = Save.useFolder and (Save.folder .. "/") or ""
local line = string.format("slot %d | saved %s | loaded %s | %s%s",
Save.slot, Save.lastSaved, Save.lastLoaded, where,
Save.exists(Save.slot) and "file exists" or "empty")
if Save.lastError then line = line .. "\nerror: " .. Save.lastError end
svStatus.Text = line
svStatus.TextColor3 = Save.lastError and Color3.fromRGB(255,150,70) or Color3.fromRGB(160,160,180)
else
svStatus.Text = "No writefile/readfile in this executor. Use EXPORT/IMPORT below instead."
svStatus.TextColor3 = Color3.fromRGB(255,150,70)
end
for s, b in pairs(slotBtns) do
b.BackgroundColor3 = (s == Save.slot) and Color3.fromRGB(120,220,160) or Color3.fromRGB(35,35,55)
b.TextColor3 = (s == Save.slot) and Color3.fromRGB(15,15,20) or Color3.fromRGB(238,238,248)
end
svAuto.Text = Save.autoSave and "AUTOSAVE: ON" or "AUTOSAVE: OFF"
svAuto.BackgroundColor3 = Save.autoSave and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55)
end
svSave.MouseButton1Click:Connect(function() Save.write(Save.slot) end)
svLoad.MouseButton1Click:Connect(function() Save.load(Save.slot) end)
svWipe.MouseButton1Click:Connect(function() Save.wipe(Save.slot) end)
svAuto.MouseButton1Click:Connect(function()
Save.autoSave = not Save.autoSave
Save.refreshUI()
end)
local svTour = F.makeBtn(pg6, "REPLAY THE TOUR", UDim2.new(0,8,0,svY+272),
UDim2.new(0,190,0,26), Color3.fromRGB(255,190,60), Color3.fromRGB(20,20,25), 10)
svTour.MouseButton1Click:Connect(function() Intro.startTour() end)
svExport.MouseButton1Click:Connect(function()
local json = Save.export()
if json then ioBox.Text = json end
end)
svImport.MouseButton1Click:Connect(function()
if ioBox.Text == "" then F.notify("Paste an export into the box first", "warn"); return end
Save.import(ioBox.Text)
end)
Save.refreshUI()
-- ============================================================
-- ── ARROW HUD ──
-- ============================================================
local arY = svY + 310
F.makeDivider(pg6, arY, Color3.fromRGB(255,110,230))
local arTitle = F.makeLabel(pg6, "ARROW HUD", UDim2.new(0,8,0,arY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(255,110,230), Enum.TextXAlignment.Left, 12)
arTitle.Font = Enum.Font.GothamBold
F.makeLabel(pg6, "Drag the arrows anywhere on screen — all four move together as one strip.",
UDim2.new(0,8,0,arY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local arToggles = {
{ key = "draggable", on = "DRAG: ON", off = "DRAG: OFF" },
{ key = "noteEnabled", on = "NOTES: ON", off = "NOTES: OFF" },
{ key = "trail", on = "TRAILS: ON", off = "TRAILS: OFF" },
{ key = "splash", on = "SPLASH: ON", off = "SPLASH: OFF" },
{ key = "combo", on = "COMBO: ON", off = "COMBO: OFF" },
{ key = "beatBob", on = "BEAT BOB: ON",off = "BEAT BOB: OFF" },
{ key = "tilt", on = "LEAN: ON", off = "LEAN: OFF" },
}
for i, t in ipairs(arToggles) do
local col = (i - 1) % 3
local rowN = math.floor((i - 1) / 3)
local b = F.makeBtn(pg6, Arrows[t.key] and t.on or t.off,
UDim2.new(0, 8 + col * 132, 0, arY + 58 + rowN * 32),
UDim2.new(0, 126, 0, 26),
Arrows[t.key] and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55), nil, 9)
local ck, on, off = t.key, t.on, t.off
b.MouseButton1Click:Connect(function()
Arrows[ck] = not Arrows[ck]
b.Text = Arrows[ck] and on or off
b.BackgroundColor3 = Arrows[ck] and Color3.fromRGB(60,190,120) or Color3.fromRGB(35,35,55)
end)
end
local arDirY = arY + 58 + math.ceil(#arToggles / 3) * 32 + 4
local arDirBtn = F.makeBtn(pg6, "NOTES TRAVEL: DOWN", UDim2.new(0,8,0,arDirY),
UDim2.new(0,190,0,28), Color3.fromRGB(70,120,255), nil, 10)
arDirBtn.MouseButton1Click:Connect(function()
Arrows.noteDir = (Arrows.noteDir == "DOWN") and "UP" or "DOWN"
arDirBtn.Text = "NOTES TRAVEL: " .. Arrows.noteDir
end)
local arReset = F.makeBtn(pg6, "RESET POSITION", UDim2.new(0,206,0,arDirY),
UDim2.new(0,190,0,28), Color3.fromRGB(170,35,35), nil, 10)
arReset.MouseButton1Click:Connect(function()
Arrows.x, Arrows.y, Arrows.offX, Arrows.offY = 0.5, 1, 0, -82
if Arrows.container then
Arrows.container.Position = UDim2.new(0.5, 0, 1, -82)
end
F.notify("Arrow strip recentred", "info")
end)
local arRows = {
{ key = "size", label = "ARROW SIZE", min = 30, max = 120, fmt = "%.0f", rebuild = true },
{ key = "gap", label = "ARROW SPACING", min = 0, max = 60, fmt = "%.0f", rebuild = true },
{ key = "noteSpeed", label = "NOTE SPEED", min = 80, max = 1200,fmt = "%.0f" },
{ key = "noteLife", label = "NOTE TRAVEL (s)", min = 0.15,max = 1.5, fmt = "%.2f" },
}
for i, row in ipairs(arRows) do
local ry = arDirY + 38 + (i-1) * 42
F.makeLabel(pg6, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,200,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pg6, string.format(row.fmt, Arrows[row.key]),
UDim2.new(0,306,0,ry), UDim2.new(0,90,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local ck, cfm, rb = row.key, row.fmt, row.rebuild
F.makeSlider(pg6, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12),
row.min, row.max, Arrows[row.key], function(v)
Arrows[ck] = v
if ck == "size" then Arrows.hitSize = v * 1.16 end
vlbl.Text = string.format(cfm, v)
if rb then F.buildArrowHud() end
end)
end
-- ============================================================
-- ── HOLD STREAK ──
-- ============================================================
local hnY = arY + 380
F.makeDivider(pg6, hnY, Color3.fromRGB(255,110,230))
local hnTitle = F.makeLabel(pg6, "HOLD STREAK", UDim2.new(0,8,0,hnY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(255,110,230), Enum.TextXAlignment.Left, 12)
hnTitle.Font = Enum.Font.GothamBold
F.makeLabel(pg6, "Hold any note key and a streak grows out of that arrow. Health and score trickle in while it fills.",
UDim2.new(0,8,0,hnY+26), UDim2.new(1,-16,0,40), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)
local hnBtn = F.makeBtn(pg6, "HOLD STREAK ON", UDim2.new(0,8,0,hnY+70),
UDim2.new(0,250,0,30), Color3.fromRGB(200,70,170), nil, 10)
hnBtn.MouseButton1Click:Connect(function() Notes.setEnabled(not Notes.enabled) end)
F.makeLabel(pg6, "KEY", UDim2.new(0,266,0,hnY+70), UDim2.new(0,30,0,30),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local hnKey = F.makeBtn(pg6, Notes.key, UDim2.new(0,298,0,hnY+70),
UDim2.new(0,64,0,30), Color3.fromRGB(25,35,48), nil, 10)
hnKey.MouseButton1Click:Connect(function()
hnKey.Text = "..."; hnKey.BackgroundColor3 = Color3.fromRGB(0,200,255)
local conn; conn = UserInputService.InputBegan:Connect(function(inp)
if inp.KeyCode and inp.KeyCode.Name ~= "Unknown" and not inp.KeyCode.Name:match("Mouse") then
Notes.key = inp.KeyCode.Name
hnKey.Text = Notes.key
hnKey.BackgroundColor3 = Color3.fromRGB(25,35,48)
if Keys.refresh then Keys.refresh() end
conn:Disconnect()
end
end)
end)
local hnGrow = F.makeBtn(pg6, "STREAK GROWS: UP", UDim2.new(0,8,0,hnY+108),
UDim2.new(0,250,0,26), Color3.fromRGB(70,120,255), nil, 9)
hnGrow.MouseButton1Click:Connect(function()
Notes.grow = (Notes.grow == "UP") and "DOWN" or "UP"
hnGrow.Text = "STREAK GROWS: " .. Notes.grow
end)
local hnReset = F.makeBtn(pg6, "RESET SCORE", UDim2.new(0,266,0,hnY+108),
UDim2.new(0,130,0,26), Color3.fromRGB(170,35,35), nil, 9)
hnReset.MouseButton1Click:Connect(function() Notes.resetScore() end)
local hnBest = F.makeLabel(pg6, "", UDim2.new(0,8,0,hnY+140),
UDim2.new(1,-16,0,18), Color3.fromRGB(255,190,80), Enum.TextXAlignment.Left, 10)
task.spawn(function()
while true do
task.wait(0.5)
if not hnBest.Parent then break end
hnBest.Text = string.format("Score %d | Best %d",
math.floor(Notes.score), math.floor(Notes.best))
end
end)
Notes.refreshUI = function()
if not hnBtn or not hnBtn.Parent then return end
hnBtn.Text = Notes.enabled and "HOLD STREAK ON" or "HOLD STREAK OFF"
hnBtn.BackgroundColor3 = Notes.enabled and Color3.fromRGB(200,70,170) or Color3.fromRGB(35,35,55)
end
Notes.refreshUI()
local hnRows = {
{ key = "fullTime", label = "TIME TO FILL (s)", min = 0.2, max = 4, fmt = "%.2f" },
{ key = "startDelay", label = "TAP IGNORED UNDER (s)", min = 0, max = 0.4, fmt = "%.2f" },
{ key = "length", label = "STREAK LENGTH (px)", min = 40, max = 400, fmt = "%.0f" },
{ key = "holdHealPerSec", label = "HEAL WHILE HOLDING /s", min = 0, max = 0.3, fmt = "%.3f" },
}
for i, row in ipairs(hnRows) do
local ry = hnY + 166 + (i-1) * 42
F.makeLabel(pg6, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,230,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pg6, string.format(row.fmt, Notes[row.key]),
UDim2.new(0,306,0,ry), UDim2.new(0,90,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local ck, cfm = row.key, row.fmt
F.makeSlider(pg6, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12),
row.min, row.max, Notes[row.key], function(v)
Notes[ck] = v
vlbl.Text = string.format(cfm, v)
end)
end
-- ============================================================
-- ── MOBILE ──
-- ============================================================
local mbY = hnY + 340
F.makeDivider(pg6, mbY, Color3.fromRGB(120,220,255))
local mbTitle = F.makeLabel(pg6, "MOBILE ARROWS", UDim2.new(0,8,0,mbY+6),
UDim2.new(1,-16,0,20), Color3.fromRGB(120,220,255), Enum.TextXAlignment.Left, 12)
mbTitle.Font = Enum.Font.GothamBold
F.makeLabel(pg6, "Big touch arrows along the bottom, split into two pairs like FNF on mobile. Tap or hold them exactly like your note keys.",
UDim2.new(0,8,0,mbY+26), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9).TextWrapped = true
local mbMode = F.makeBtn(pg6, "", UDim2.new(0,8,0,mbY+58), UDim2.new(0,190,0,28), Color3.fromRGB(35,35,55), nil, 10)
local mbNote = F.makeLabel(pg6, "", UDim2.new(0,206,0,mbY+58), UDim2.new(1,-214,0,28),
Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
mbNote.TextWrapped = true
local modeHelp = {
AUTO = "Shows on phones and tablets, hides with a keyboard.",
ON = "Always shown, even on PC (handy for testing).",
OFF = "Never shown.",
}
mbMode.MouseButton1Click:Connect(function()
local order = { "AUTO", "ON", "OFF" }
Mobile.mode = order[((table.find(order, Mobile.mode) or 1) % #order) + 1]
Mobile.refresh()
end)
local mbStrip = F.makeBtn(pg6, "", UDim2.new(0,8,0,mbY+92), UDim2.new(0,190,0,26), Color3.fromRGB(35,35,55), nil, 9)
mbStrip.MouseButton1Click:Connect(function()
Mobile.hideStrip = not Mobile.hideStrip
Mobile.refresh()
end)
F.makeLabel(pg6, "Hold streaks and splashes live on the top strip.",
UDim2.new(0,206,0,mbY+92), UDim2.new(1,-214,0,26), Color3.fromRGB(120,120,140), Enum.TextXAlignment.Left, 9)
Mobile.refreshUI = function()
if not mbMode.Parent then return end
mbMode.Text = "MOBILE ARROWS: " .. Mobile.mode
mbMode.BackgroundColor3 = (Mobile.mode == "OFF") and Color3.fromRGB(35,35,55) or Color3.fromRGB(60,160,220)
mbNote.Text = (modeHelp[Mobile.mode] or "") .. (Mobile.active and " (showing)" or "")
mbStrip.Text = Mobile.hideStrip and "HIDE TOP STRIP: ON" or "HIDE TOP STRIP: OFF"
mbStrip.BackgroundColor3 = Mobile.hideStrip and Color3.fromRGB(60,160,220) or Color3.fromRGB(35,35,55)
end
Mobile.refreshUI()
local mbRows = {
{ key = "size", label = "ARROW SIZE", min = 0.5, max = 1.8, fmt = "%.2f" },
{ key = "spread", label = "GAP BETWEEN PAIRS", min = 0, max = 0.6, fmt = "%.2f" },
{ key = "pairGap", label = "GAP INSIDE A PAIR", min = 0, max = 80, fmt = "%.0f px" },
{ key = "bottom", label = "HEIGHT FROM BOTTOM", min = 0, max = 260, fmt = "%.0f px" },
{ key = "idleFade", label = "SEE-THROUGH WHEN IDLE", min = 0, max = 0.8, fmt = "%.2f" },
}
for i, row in ipairs(mbRows) do
local ry = mbY + 128 + (i-1) * 42
F.makeLabel(pg6, row.label, UDim2.new(0,8,0,ry), UDim2.new(0,230,0,18),
Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
local vlbl = F.makeLabel(pg6, string.format(row.fmt, Mobile[row.key]),
UDim2.new(0,306,0,ry), UDim2.new(0,90,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
F.regText(vlbl)
local ck, cfm = row.key, row.fmt
F.makeSlider(pg6, UDim2.new(0,8,0,ry+20), UDim2.new(1,-16,0,12),
row.min, row.max, Mobile[row.key], function(v)
Mobile[ck] = v
vlbl.Text = string.format(cfm, v)
Mobile.layout()
if ck == "idleFade" then
for d in pairs(Mobile.btns) do
if (Mobile.held[d] or 0) == 0 then Mobile.paint(d, false) end
end
end
end)
end
end
function F.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.DisplayOrder = F.silZ(100) -- above the video backdrop and stage layers
sg.Parent = PlayerGui
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 = 440, 620
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, 12, 19)
main.BackgroundTransparency = 0.04
main.BorderSizePixel = 0; main.Active = true; main.Draggable = true
main.ClipsDescendants = true
main.Parent = sg; F.uiCorner(main, UDim.new(0,18))
local mainStroke = F.uiStroke(main, Color3.fromRGB(255,255,255), 1)
mainStroke.Transparency = 0.85
local mainAccentStroke = Instance.new("UIStroke")
mainAccentStroke.Color = Color3.fromRGB(255,80,180)
mainAccentStroke.Thickness = 1; mainAccentStroke.Transparency = 0.7
mainAccentStroke.Parent = main
F.regStroke(mainAccentStroke)
local mainGrad = Instance.new("UIGradient")
mainGrad.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(26, 22, 38)),
ColorSequenceKeypoint.new(0.5, Color3.fromRGB(13, 12, 19)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(10, 9, 15)),
})
mainGrad.Rotation = 115; mainGrad.Parent = main
notesLayer = Instance.new("Frame")
notesLayer.Size = UDim2.new(1,0,1,0); notesLayer.BackgroundTransparency = 1
notesLayer.ClipsDescendants = true; notesLayer.ZIndex = F.silZ(0); notesLayer.Parent = main
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 = F.silZ(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 = F.silZ(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 = F.silZ(51)
line.Parent = scanlineHolder
end
local titleBar = Instance.new("Frame")
titleBar.Size = UDim2.new(1,0,0,40)
titleBar.BackgroundColor3 = Color3.fromRGB(19, 17, 27)
titleBar.BackgroundTransparency = 0.15
titleBar.BorderSizePixel = 0; titleBar.Parent = main; F.uiCorner(titleBar, UDim.new(0,18))
-- accent dot, left of the title
local tbDot = Instance.new("Frame")
tbDot.Size = UDim2.new(0, 8, 0, 8); tbDot.Position = UDim2.new(0, 14, 0.5, -4)
tbDot.BackgroundColor3 = Color3.fromRGB(255, 80, 180)
tbDot.BorderSizePixel = 0; tbDot.Parent = titleBar
F.uiCorner(tbDot, UDim.new(1, 0)); F.regBg(tbDot)
-- accent hairline under the header
local tbSep = Instance.new("Frame")
tbSep.Size = UDim2.new(1,0,0,1); tbSep.Position = UDim2.new(0,0,1,-1)
tbSep.BackgroundColor3 = Color3.fromRGB(255,80,180)
tbSep.BackgroundTransparency = 0.55
tbSep.BorderSizePixel = 0; tbSep.Parent = titleBar
F.regBg(tbSep)
local titleLbl = F.makeLabel(titleBar, "FNF ANIM INJECTOR",
UDim2.new(0,30,0,0), UDim2.new(0,190,1,0),
Color3.fromRGB(242,242,250), Enum.TextXAlignment.Left, 13)
titleLbl.Font = Enum.Font.GothamBold
-- version chip
local verChip = Instance.new("Frame")
verChip.Size = UDim2.new(0, 38, 0, 18); verChip.Position = UDim2.new(0, 196, 0.5, -9)
verChip.BackgroundColor3 = Color3.fromRGB(255, 80, 180)
verChip.BackgroundTransparency = 0.78
verChip.BorderSizePixel = 0; verChip.Parent = titleBar
F.uiCorner(verChip, UDim.new(1, 0)); F.regBg(verChip)
local verLbl = F.makeLabel(verChip, "v6.2", UDim2.new(0,0,0,0), UDim2.new(1,0,1,0),
Color3.fromRGB(255,255,255), Enum.TextXAlignment.Center, 9)
verLbl.Font = Enum.Font.GothamBold
local toggleBtn = F.makeBtn(titleBar, "HIDE",
UDim2.new(1,-70,0,8), UDim2.new(0,58,0,24), Color3.fromRGB(34,31,46), nil, 10)
local TAB_H = 36
local tabBar = Instance.new("Frame")
tabBar.Size = UDim2.new(1,-8,0,TAB_H); tabBar.Position = UDim2.new(0,4,0,42)
tabBar.BackgroundTransparency = 1
tabBar.BorderSizePixel = 0; tabBar.Parent = main
local tabScroll = Instance.new("ScrollingFrame")
tabScroll.Size = UDim2.new(1,0,1,0); tabScroll.BackgroundTransparency = 1
tabScroll.ScrollBarThickness = 0; tabScroll.BorderSizePixel = 0
tabScroll.CanvasSize = UDim2.new(0,0,1,0); tabScroll.AutomaticCanvasSize = Enum.AutomaticSize.X
tabScroll.ScrollingDirection = Enum.ScrollingDirection.X
tabScroll.Parent = tabBar
local tabLayout = Instance.new("UIListLayout")
tabLayout.FillDirection = Enum.FillDirection.Horizontal
tabLayout.Padding = UDim.new(0,4); tabLayout.VerticalAlignment = Enum.VerticalAlignment.Center
tabLayout.Parent = tabScroll
local tabPad = Instance.new("UIPadding")
tabPad.PaddingLeft = UDim.new(0,4); tabPad.PaddingRight = UDim.new(0,4)
tabPad.Parent = tabScroll
local contentArea = Instance.new("Frame")
contentArea.Size = UDim2.new(1,0,1,-(42+TAB_H+4))
contentArea.Position = UDim2.new(0,0,0,42+TAB_H+4)
contentArea.BackgroundTransparency = 1; contentArea.Parent = main
local tabNames = { "📋 BINDS", "✂ CUTTER", "🎥 CAM", "🎬 MAKER", "👁 PREVIEW", "❤ FAVES", "🎶 LIST", "🎛 FX", "🎞 VIDEO", "🏆 RESULT", "⚙ SETTINGS" }
local pages = {}
local tabButtons = {}
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 = 3; page.ScrollBarImageTransparency = 0.5
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 = Instance.new("TextButton")
tb.Size = UDim2.new(0,0,0,30); tb.AutomaticSize = Enum.AutomaticSize.X
tb.BackgroundColor3 = Color3.fromRGB(24, 22, 33)
tb.BackgroundTransparency = i == 1 and 0.1 or 0.65
tb.Text = name
tb.TextColor3 = i == 1 and Color3.fromRGB(255,255,255) or Color3.fromRGB(150,150,170)
tb.Font = Enum.Font.GothamBold; tb.TextSize = 9
tb.BorderSizePixel = 0; tb.AutoButtonColor = false
tb.Parent = tabScroll
F.uiCorner(tb, UDim.new(0,7))
local tbTextPad = Instance.new("UIPadding")
tbTextPad.PaddingLeft = UDim.new(0,11); tbTextPad.PaddingRight = UDim.new(0,11)
tbTextPad.Parent = tb
-- accent underline marks the active tab
local tbLine = Instance.new("Frame")
tbLine.Name = "ActiveLine"
tbLine.Size = UDim2.new(1,-16,0,2); tbLine.Position = UDim2.new(0,8,1,-3)
tbLine.BackgroundColor3 = THEMES[currentThemeName].accent
tbLine.BackgroundTransparency = i == 1 and 0 or 1
tbLine.BorderSizePixel = 0; tbLine.Parent = tb
F.uiCorner(tbLine, UDim.new(1,0)); F.regBg(tbLine)
tabButtons[name] = tb
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)
TweenService:Create(btn, TweenInfo.new(0.18, Enum.EasingStyle.Quad), {
BackgroundTransparency = isActive and 0.1 or 0.65,
TextColor3 = isActive and Color3.fromRGB(255,255,255) or Color3.fromRGB(150,150,170),
}):Play()
local line = btn:FindFirstChild("ActiveLine")
if line then
TweenService:Create(line, TweenInfo.new(0.18), {
BackgroundTransparency = isActive and 0 or 1
}):Play()
end
end
end)
end
-- lets the tour (and anything else) switch tabs from code
F.tabButtons = tabButtons
F.selectTab = function(name)
if not pages[name] then return end
for n, p in pairs(pages) do p.Visible = (n == name) end
for n, btn in pairs(tabButtons) do
local isActive = (n == name)
TweenService:Create(btn, TweenInfo.new(0.18, Enum.EasingStyle.Quad), {
BackgroundTransparency = isActive and 0.1 or 0.65,
TextColor3 = isActive and Color3.fromRGB(255,255,255) or Color3.fromRGB(150,150,170),
}):Play()
local line = btn:FindFirstChild("ActiveLine")
if line then
TweenService:Create(line, TweenInfo.new(0.18), { BackgroundTransparency = isActive and 0 or 1 }):Play()
end
end
end
F.buildTab_Binds(pages)
F.buildTab_Cutter(pages)
F.buildTab_Cam(pages)
F.buildTab_Maker(pages)
F.buildTab_Preview(pages)
F.buildTab_Favorites(pages)
F.buildTab_Playlist(pages)
F.buildTab_FX(pages)
F.buildTab_Video(pages)
F.buildTab_Result(pages)
F.buildTab_Settings(pages)
toggleBtn.MouseButton1Click:Connect(function()
guiVisible = not guiVisible
toggleBtn.Text = guiVisible and "HIDE" or "SHOW"
local targetSize
if guiVisible then
targetSize = UDim2.new(0, MAIN_W, 0, MAIN_H)
else
targetSize = UDim2.new(0, MAIN_W, 0, 40)
end
TweenService:Create(main, TweenInfo.new(0.3, Enum.EasingStyle.Quint, Enum.EasingDirection.Out), {
Size = targetSize
}):Play()
end)
F.startVisualizerLoops()
F.notify("FNF Injector v6.2 loaded", "success")
return sg
end
-- ============================================================
-- LAUNCH
-- ============================================================
FNFCamera:BuildLetterbox(PlayerGui)
FNFCamera:Start()
-- load settings FIRST so every piece of UI below is built from them
pcall(Save.boot)
pcall(Vid.resolveApi)
pcall(Intro.startLoading)
F.buildArrowHud()
pcall(function() Chaos.build() end)
pcall(function() Ride.buildWind() end)
pcall(function() Health.build(); Health.start(); Health.setIcons(Health.playerIcon, Health.oppIcon) end)
pcall(function() Keys.build() end)
F.setIdleBPM(idleBPM)
F.buildGUI()
pcall(function() F.setTheme(currentThemeName) end)
Save.startAuto()
Notes.start()
pcall(Mobile.start)
pcall(Intro.finishBoot)
if LocalPlayer.Character then task.spawn(F.setupCharacter, LocalPlayer.Character) end
LocalPlayer.CharacterAdded:Connect(function(c) task.spawn(F.setupCharacter, c) end)
task.spawn(F.setupMusicRemote)
task.spawn(F.setupMatchRemote)
F.broadcastScriptPresence()
print("[FNF] Injector v6.2 fully loaded.")No comments yet for this script.

![[KEYLESS] Moondiety Script | 17+ supported Games](https://scriptblox.com/images/sb/0-1753093706828.png)

