--[[ ╔══════════════════════════════════════════════════════╗ ║ 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.")