-- PathDash: hold Z to draw a path, release Z to rush along it. -- You walk the path (with a walking animation) and your character is aligned -- to whatever surface the path is on: floors, walls, ceilings. -- Put this in a LocalScript inside StarterPlayer > StarterPlayerScripts. local Players = game:GetService("Players") local UserInputService = game:GetService("UserInputService") local RunService = game:GetService("RunService") local TweenService = game:GetService("TweenService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local player = Players.LocalPlayer --// SETTINGS local KEY = Enum.KeyCode.Z local DRAW_MODE = "Mouse" -- "Mouse" = draw by moving your cursor over surfaces -- "Walk" = draw by walking around while holding Z local FOLLOW_SPEED = 250 -- studs per second while following the path (default walk is 16) local MIN_POINT_SPACING = .25 -- studs between recorded points local MAX_POINTS = 1000 -- cap on path length local SPEED_UI_KEY = Enum.KeyCode.P -- toggles the speed slider local SPEED_MIN = 1 -- lowest speed the slider / text box allows local SPEED_SLIDER_MAX = 500 -- speed at the far right of the slider local SPEED_MAX = 5000 -- highest speed you can type in local FREECAM_KEY = Enum.KeyCode.X -- enter freecam / press again to dash along the path local FREECAM_SPEED = 80 -- freecam fly speed (studs/sec); scroll wheel changes it live local FREECAM_BOOST = 3 -- speed multiplier while holding Shift local FREECAM_START_DISTANCE = 60 -- how far above/behind you the camera swoops to local FREECAM_START_PITCH = -55 -- degrees; negative looks down at your character local FREECAM_TRANSITION = 0.9 -- seconds for the camera swoop in/out local FREECAM_FOV_PUNCH = 18 -- extra field of view at the middle of the swoop local FREECAM_LOOK_SENS = 0.003 -- mouse look sensitivity (hold right mouse) local FREECAM_LOOK_SMOOTHING = 20 -- higher = snappier look local FREECAM_MOVE_SMOOTHING = 8 -- higher = snappier acceleration local STAY_IN_FREECAM = false -- if true, you stay in freecam after dashing: tap X to dash, -- HOLD X to exit (can also be toggled live in the speed panel, press P) local FREECAM_EXIT_HOLD = 0.5 -- seconds you hold X to exit freecam in that mode local SCREEN_FLASH = true -- white flash + blur + color pop when entering/leaving freecam. -- Turn OFF for photosensitivity (also a switch in the speed panel, press P) local PATH_COLOR = Color3.fromRGB(0, 200, 255) local PATH_WIDTH = 0.5 local GROUND_CLEARANCE = 0.1 -- extra studs between your feet and the surface local LINE_OFFSET = 0.15 -- how far the drawn line floats off the surface local ANIM_BASE_SPEED = 16 -- follow speed at which the walk animation plays at normal (1x) speed. -- animation speed = FOLLOW_SPEED / ANIM_BASE_SPEED local NORMAL_SMOOTHING = 25 -- higher = snaps to new surfaces faster (corners, wall <-> floor) --// STATE local points = {} -- each entry: { pos = Vector3 (on the line), normal = Vector3 (surface up) } local drawing = false local following = false local followToken = 0 local segmentParts = {} local freecamActive = false -- in freecam (drawing mode) local freecamBusy = false -- camera swoop in progress local freecamDrawing = false -- left mouse held while in freecam local pathFolder = Instance.new("Folder") pathFolder.Name = "PathDashVisuals" pathFolder.Parent = workspace local function getCharacter() local char = player.Character if not char then return end local hrp = char:FindFirstChild("HumanoidRootPart") local hum = char:FindFirstChildOfClass("Humanoid") if hrp and hum and hum.Health > 0 then return char, hrp, hum end end -- Everything raycasts should ignore: your character, the path line, and ALL other players local function getIgnoreList(char) local list = { char, pathFolder } for _, other in ipairs(Players:GetPlayers()) do if other ~= player and other.Character then table.insert(list, other.Character) end end return list end -- Distance from the HumanoidRootPart's center down to the bottom of the feet. -- Works for R6 and R15. local function getFootDistance(char, hrp, hum) if hum.FloorMaterial ~= Enum.Material.Air then local params = RaycastParams.new() params.FilterType = Enum.RaycastFilterType.Exclude params.FilterDescendantsInstances = getIgnoreList(char) local result = workspace:Raycast(hrp.Position, Vector3.new(0, -15, 0), params) if result then return hrp.Position.Y - result.Position.Y end end if hum.RigType == Enum.HumanoidRigType.R6 then return 3 end return hum.HipHeight + hrp.Size.Y / 2 end -- Finds (or falls back to) the character's own walk/run animation local function loadWalkTrack(char, hum) local animator = hum:FindFirstChildOfClass("Animator") if not animator then animator = Instance.new("Animator") animator.Parent = hum end local id local animate = char:FindFirstChild("Animate") if animate then local folder = animate:FindFirstChild("run") or animate:FindFirstChild("walk") local anim = folder and (folder:FindFirstChildOfClass("Animation")) if anim and anim.AnimationId ~= "" then id = anim.AnimationId end end if not id then if hum.RigType == Enum.HumanoidRigType.R6 then id = "rbxassetid://180426354" else id = "rbxassetid://507767714" end end local anim = Instance.new("Animation") anim.AnimationId = id local track = animator:LoadAnimation(anim) track.Looped = true track.Priority = Enum.AnimationPriority.Action return track end local function clearVisuals() pathFolder:ClearAllChildren() table.clear(segmentParts) end local function addSegment(a, b) local dist = (b - a).Magnitude if dist < 0.05 then return end local part = Instance.new("Part") part.Anchored = true part.CanCollide = false part.CanQuery = false part.CanTouch = false part.CastShadow = false part.Material = Enum.Material.Neon part.Color = PATH_COLOR part.Size = Vector3.new(PATH_WIDTH, PATH_WIDTH, dist) part.CFrame = CFrame.lookAt((a + b) / 2, b) part.Parent = pathFolder table.insert(segmentParts, part) end -- Returns a point on a surface plus that surface's normal local function sampleSurfacePoint() local char, hrp, hum = getCharacter() if not char then return end local params = RaycastParams.new() params.FilterType = Enum.RaycastFilterType.Exclude params.FilterDescendantsInstances = getIgnoreList(char) if DRAW_MODE == "Walk" and not freecamActive then local result = workspace:Raycast(hrp.Position, -hrp.CFrame.UpVector * 15, params) if result then return result.Position + result.Normal * LINE_OFFSET, result.Normal end return end local cam = workspace.CurrentCamera local mousePos = UserInputService:GetMouseLocation() local ray = cam:ViewportPointToRay(mousePos.X, mousePos.Y) local result = workspace:Raycast(ray.Origin, ray.Direction * 1000, params) if result then return result.Position + result.Normal * LINE_OFFSET, result.Normal end end local function addPoint(p, n) if #points >= MAX_POINTS then return end local last = points[#points] if last and (p - last.pos).Magnitude < MIN_POINT_SPACING then return end table.insert(points, { pos = p, normal = n }) if last then addSegment(last.pos, p) end end local function startDrawing() if following then return end drawing = true table.clear(points) clearVisuals() local p, n = sampleSurfacePoint() if p then addPoint(p, n) end end local function projectOnPlane(v, normal) return v - normal * v:Dot(normal) end local function followPath(onFinished) local char, hrp, hum = getCharacter() if not char or #points < 1 then clearVisuals() return end following = true followToken += 1 local token = followToken local footDistance = getFootDistance(char, hrp, hum) + GROUND_CLEARANCE -- Convert the recorded surface points into where the root part should be local path, normals = {}, {} for i, p in ipairs(points) do path[i] = p.pos - p.normal * LINE_OFFSET + p.normal * footDistance normals[i] = p.normal end local groundParams = RaycastParams.new() groundParams.FilterType = Enum.RaycastFilterType.Exclude groundParams.FilterDescendantsInstances = getIgnoreList(char) groundParams.RespectCanCollide = true -- ignore non-solid parts (decor, triggers, etc.) local oldPlatformStand = hum.PlatformStand hum.PlatformStand = true -- stops the humanoid fighting our orientation -- Turn off collisions on our character while dashing so we can't snag on -- walls, props or other players. Original values are restored afterwards. local savedCollide = {} for _, d in ipairs(char:GetDescendants()) do if d:IsA("BasePart") then savedCollide[d] = d.CanCollide end end local track = loadWalkTrack(char, hum) track:Play(0.1) track:AdjustSpeed(FOLLOW_SPEED / ANIM_BASE_SPEED) local idx = 1 local erased = 0 -- how many line segments behind us have been erased so far local current = hrp.Position local up = hrp.CFrame.UpVector local look = hrp.CFrame.LookVector local moveDir = look local conn local function finish(upright) if conn then conn:Disconnect() end track:Stop(0.1) for part, canCollide in pairs(savedCollide) do if part.Parent then part.CanCollide = canCollide end end if char.Parent and hum.Parent then hum.PlatformStand = oldPlatformStand end if token == followToken then following = false clearVisuals() table.clear(points) end if upright and hrp.Parent then -- stand back up (gravity takes over from here) local flat = Vector3.new(look.X, 0, look.Z) if flat.Magnitude < 0.01 then flat = Vector3.new(0, 0, -1) end hrp.CFrame = CFrame.lookAt(current, current + flat.Unit) end if onFinished then task.defer(onFinished) -- runs when the dash ends for ANY reason (finished, cancelled, died) end end local function step(dt) -- The humanoid keeps turning collisions back on, so switch them off every frame for part in pairs(savedCollide) do if part.Parent then part.CanCollide = false end end if not hrp.Parent or hum.Health <= 0 then finish(false) return end if token ~= followToken then -- cancelled (keybind pressed again): stand back up where we are finish(true) return end -- Speed can change mid-dash from the slider, so keep the animation in sync track:AdjustSpeed(FOLLOW_SPEED / ANIM_BASE_SPEED) -- Move along the path local remaining = FOLLOW_SPEED * dt while remaining > 0 and idx <= #path do local target = path[idx] local toTarget = target - current local d = toTarget.Magnitude if d <= remaining then current = target remaining -= d idx += 1 else moveDir = toTarget.Unit current += moveDir * remaining remaining = 0 end end -- Erase the line behind us as we go: segments we've fully passed disappear, -- and the one we're on shrinks so it ends right where the character is. -- (segmentParts[k] is the line between points[k] and points[k + 1]) while erased < idx - 2 do erased += 1 local seg = segmentParts[erased] if seg then seg:Destroy() end end if idx >= 2 and idx <= #path then local seg = segmentParts[idx - 1] if seg and seg.Parent then local a, b = path[idx - 1], path[idx] local ab = b - a local len2 = ab:Dot(ab) local t = len2 > 0 and math.clamp((current - a):Dot(ab) / len2, 0, 1) or 1 local startPos = points[idx - 1].pos:Lerp(points[idx].pos, t) local endPos = points[idx].pos local dist = (endPos - startPos).Magnitude if dist < 0.02 then seg.Transparency = 1 else seg.Size = Vector3.new(PATH_WIDTH, PATH_WIDTH, dist) seg.CFrame = CFrame.lookAt((startPos + endPos) / 2, endPos) end end end -- Smoothly rotate "up" toward the surface we're heading to local targetNormal = normals[math.min(idx, #normals)] local blended = up:Lerp(targetNormal, 1 - math.exp(-NORMAL_SMOOTHING * dt)) if blended.Magnitude < 0.01 then up = targetNormal else up = blended.Unit end if up ~= up then up = targetNormal end -- NaN guard -- Face the direction of travel, flattened onto the current surface local flatLook = projectOnPlane(moveDir, up) if flatLook.Magnitude < 0.01 then flatLook = projectOnPlane(look, up) end if flatLook.Magnitude > 0.01 then look = flatLook.Unit end -- Keep the feet on the surface (never sink into it). -- This only changes where the character is drawn this frame, NOT the -- path position (`current`), so it can never fight the movement and stall you. local renderPos = current groundParams.FilterDescendantsInstances = getIgnoreList(char) -- players come and go local hit = workspace:Raycast(current + up * 1, -up * (footDistance + 10), groundParams) if hit then local height = (current - hit.Position):Dot(up) if height < footDistance then renderPos = current + up * (footDistance - height) end end local right = look:Cross(up) hrp.CFrame = CFrame.fromMatrix(renderPos, right, up, -look) hrp.AssemblyLinearVelocity = Vector3.zero hrp.AssemblyAngularVelocity = Vector3.zero if idx > #path then finish(true) end end -- If anything ever errors mid-dash, end the dash cleanly instead of leaving -- the character frozen with the walk animation still playing. conn = RunService.Heartbeat:Connect(function(dt) local ok, err = pcall(step, dt) if not ok then warn("PathDash error: " .. tostring(err)) finish(true) end end) end --// SAVED SETTINGS: speed + both switches are remembered between sessions. -- Needs the PathDashSaveServer script in ServerScriptService (a LocalScript can't save by itself). local saveEvent = nil -- set once the server script is found (see bottom of this file) local settingsTouched = false -- true once you've changed something, so a late load can't overwrite it local saveToken = 0 local function requestSave() settingsTouched = true saveToken += 1 local token = saveToken -- wait a moment so dragging the slider doesn't send dozens of saves task.delay(1.5, function() if token ~= saveToken or not saveEvent then return end saveEvent:FireServer({ speed = FOLLOW_SPEED, stayInFreecam = STAY_IN_FREECAM, screenFlash = SCREEN_FLASH, }) end) end --// SPEED UI (press P): slider + text box that control FOLLOW_SPEED local playerGui = player:WaitForChild("PlayerGui") local speedGui = Instance.new("ScreenGui") speedGui.Name = "PathDashSpeedUI" speedGui.ResetOnSpawn = false speedGui.IgnoreGuiInset = true speedGui.Enabled = false speedGui.Parent = playerGui -- CanvasGroup lets the whole panel fade in/out smoothly local panel = Instance.new("CanvasGroup") panel.AnchorPoint = Vector2.new(0.5, 1) panel.Position = UDim2.new(0.5, 0, 1, -30) panel.Size = UDim2.fromOffset(320, 158) panel.BackgroundColor3 = Color3.fromRGB(20, 22, 30) panel.BackgroundTransparency = 0.45 -- see-through "glass" look panel.BorderSizePixel = 0 panel.GroupTransparency = 1 -- starts hidden, fades in panel.Parent = speedGui Instance.new("UICorner", panel).CornerRadius = UDim.new(0, 14) local panelStroke = Instance.new("UIStroke") panelStroke.Color = Color3.new(1, 1, 1) panelStroke.Transparency = 0.8 panelStroke.Thickness = 1 panelStroke.Parent = panel -- Empty space on the panel acts as the drag handle local dragHandle = Instance.new("TextButton") dragHandle.Size = UDim2.fromScale(1, 1) dragHandle.BackgroundTransparency = 1 dragHandle.Text = "" dragHandle.AutoButtonColor = false dragHandle.Parent = panel local title = Instance.new("TextLabel") title.BackgroundTransparency = 1 title.Position = UDim2.fromOffset(16, 10) title.Size = UDim2.fromOffset(160, 24) title.Font = Enum.Font.GothamBold title.TextSize = 16 title.TextColor3 = Color3.new(1, 1, 1) title.TextXAlignment = Enum.TextXAlignment.Left title.Text = "Follow Speed" title.Parent = panel local speedBox = Instance.new("TextBox") speedBox.AnchorPoint = Vector2.new(1, 0) speedBox.Position = UDim2.new(1, -16, 0, 10) speedBox.Size = UDim2.fromOffset(90, 24) speedBox.BackgroundColor3 = Color3.fromRGB(255, 255, 255) speedBox.BackgroundTransparency = 0.9 speedBox.BorderSizePixel = 0 speedBox.Font = Enum.Font.Gotham speedBox.TextSize = 14 speedBox.TextColor3 = Color3.new(1, 1, 1) speedBox.ClearTextOnFocus = false speedBox.Text = "" speedBox.Parent = panel Instance.new("UICorner", speedBox).CornerRadius = UDim.new(0, 8) local boxStroke = Instance.new("UIStroke") boxStroke.Color = Color3.new(1, 1, 1) boxStroke.Transparency = 0.85 boxStroke.Parent = speedBox speedBox.Focused:Connect(function() TweenService:Create(boxStroke, TweenInfo.new(0.15), { Color = PATH_COLOR, Transparency = 0.2 }):Play() end) speedBox.FocusLost:Connect(function() TweenService:Create(boxStroke, TweenInfo.new(0.15), { Color = Color3.new(1, 1, 1), Transparency = 0.85 }):Play() end) local track = Instance.new("Frame") track.Position = UDim2.new(0, 16, 0, 60) track.Size = UDim2.new(1, -32, 0, 8) track.BackgroundColor3 = Color3.fromRGB(255, 255, 255) track.BackgroundTransparency = 0.85 track.BorderSizePixel = 0 track.Parent = panel Instance.new("UICorner", track).CornerRadius = UDim.new(1, 0) local fill = Instance.new("Frame") fill.Size = UDim2.new(0, 0, 1, 0) fill.BackgroundColor3 = PATH_COLOR fill.BorderSizePixel = 0 fill.Parent = track Instance.new("UICorner", fill).CornerRadius = UDim.new(1, 0) local fillGradient = Instance.new("UIGradient") fillGradient.Color = ColorSequence.new(PATH_COLOR, Color3.fromRGB(150, 235, 255)) fillGradient.Parent = fill local knob = Instance.new("Frame") knob.AnchorPoint = Vector2.new(0.5, 0.5) knob.Size = UDim2.fromOffset(18, 18) knob.Position = UDim2.new(0, 0, 0.5, 0) knob.BackgroundColor3 = Color3.new(1, 1, 1) knob.BorderSizePixel = 0 knob.Parent = track Instance.new("UICorner", knob).CornerRadius = UDim.new(1, 0) -- Invisible, taller hit area so the slider is easy to grab local hit = Instance.new("TextButton") hit.AnchorPoint = Vector2.new(0, 0.5) hit.Position = UDim2.new(0, 0, 0.5, 0) hit.Size = UDim2.new(1, 0, 0, 28) hit.BackgroundTransparency = 1 hit.Text = "" hit.AutoButtonColor = false hit.Parent = track -- "Stay in freecam" toggle local stayLabel = Instance.new("TextLabel") stayLabel.BackgroundTransparency = 1 stayLabel.Position = UDim2.fromOffset(16, 92) stayLabel.Size = UDim2.fromOffset(230, 22) stayLabel.Font = Enum.Font.Gotham stayLabel.TextSize = 13 stayLabel.TextColor3 = Color3.new(1, 1, 1) stayLabel.TextTransparency = 0.15 stayLabel.TextXAlignment = Enum.TextXAlignment.Left stayLabel.Text = "Stay in freecam (hold X to exit)" stayLabel.Parent = panel local stayPill = Instance.new("TextButton") stayPill.AnchorPoint = Vector2.new(1, 0) stayPill.Position = UDim2.new(1, -16, 0, 92) stayPill.Size = UDim2.fromOffset(42, 22) stayPill.BackgroundColor3 = Color3.new(1, 1, 1) stayPill.BackgroundTransparency = 0.85 stayPill.BorderSizePixel = 0 stayPill.Text = "" stayPill.AutoButtonColor = false stayPill.Parent = panel Instance.new("UICorner", stayPill).CornerRadius = UDim.new(1, 0) local stayKnob = Instance.new("Frame") stayKnob.AnchorPoint = Vector2.new(0.5, 0.5) stayKnob.Size = UDim2.fromOffset(16, 16) stayKnob.Position = UDim2.new(0, 11, 0.5, 0) stayKnob.BackgroundColor3 = Color3.new(1, 1, 1) stayKnob.BorderSizePixel = 0 stayKnob.Parent = stayPill Instance.new("UICorner", stayKnob).CornerRadius = UDim.new(1, 0) local function refreshStayToggle() local on = STAY_IN_FREECAM local info = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) TweenService:Create(stayPill, info, { BackgroundColor3 = on and PATH_COLOR or Color3.new(1, 1, 1), BackgroundTransparency = on and 0.2 or 0.85, }):Play() TweenService:Create(stayKnob, info, { Position = UDim2.new(0, on and 31 or 11, 0.5, 0) }):Play() end stayPill.MouseButton1Click:Connect(function() STAY_IN_FREECAM = not STAY_IN_FREECAM refreshStayToggle() requestSave() end) refreshStayToggle() -- "Screen flash" toggle (turn off for photosensitivity) local flashLabel = Instance.new("TextLabel") flashLabel.BackgroundTransparency = 1 flashLabel.Position = UDim2.fromOffset(16, 122) flashLabel.Size = UDim2.fromOffset(230, 22) flashLabel.Font = Enum.Font.Gotham flashLabel.TextSize = 13 flashLabel.TextColor3 = Color3.new(1, 1, 1) flashLabel.TextTransparency = 0.15 flashLabel.TextXAlignment = Enum.TextXAlignment.Left flashLabel.Text = "Screen flash effect" flashLabel.Parent = panel local flashPill = Instance.new("TextButton") flashPill.AnchorPoint = Vector2.new(1, 0) flashPill.Position = UDim2.new(1, -16, 0, 122) flashPill.Size = UDim2.fromOffset(42, 22) flashPill.BackgroundColor3 = Color3.new(1, 1, 1) flashPill.BackgroundTransparency = 0.85 flashPill.BorderSizePixel = 0 flashPill.Text = "" flashPill.AutoButtonColor = false flashPill.Parent = panel Instance.new("UICorner", flashPill).CornerRadius = UDim.new(1, 0) local flashKnob = Instance.new("Frame") flashKnob.AnchorPoint = Vector2.new(0.5, 0.5) flashKnob.Size = UDim2.fromOffset(16, 16) flashKnob.Position = UDim2.new(0, 11, 0.5, 0) flashKnob.BackgroundColor3 = Color3.new(1, 1, 1) flashKnob.BorderSizePixel = 0 flashKnob.Parent = flashPill Instance.new("UICorner", flashKnob).CornerRadius = UDim.new(1, 0) local function refreshFlashToggle() local on = SCREEN_FLASH local info = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) TweenService:Create(flashPill, info, { BackgroundColor3 = on and PATH_COLOR or Color3.new(1, 1, 1), BackgroundTransparency = on and 0.2 or 0.85, }):Play() TweenService:Create(flashKnob, info, { Position = UDim2.new(0, on and 31 or 11, 0.5, 0) }):Play() end flashPill.MouseButton1Click:Connect(function() SCREEN_FLASH = not SCREEN_FLASH refreshFlashToggle() requestSave() end) refreshFlashToggle() local function setSpeed(value) value = math.clamp(value, SPEED_MIN, SPEED_MAX) FOLLOW_SPEED = value speedBox.Text = string.format("%g", math.floor(value * 100 + 0.5) / 100) local alpha = math.clamp((value - SPEED_MIN) / (SPEED_SLIDER_MAX - SPEED_MIN), 0, 1) local info = TweenInfo.new(0.08, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) TweenService:Create(fill, info, { Size = UDim2.new(alpha, 0, 1, 0) }):Play() TweenService:Create(knob, info, { Position = UDim2.new(alpha, 0, 0.5, 0) }):Play() end local function setKnobGrown(grown) local s = grown and 24 or 18 TweenService:Create(knob, TweenInfo.new(0.12, Enum.EasingStyle.Quad), { Size = UDim2.fromOffset(s, s) }):Play() end local function setSpeedFromX(x) local alpha = math.clamp((x - track.AbsolutePosition.X) / track.AbsoluteSize.X, 0, 1) setSpeed(math.floor(SPEED_MIN + alpha * (SPEED_SLIDER_MAX - SPEED_MIN) + 0.5)) requestSave() end local sliderDragging = false local panelDragging = false hit.InputBegan:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then sliderDragging = true setKnobGrown(true) setSpeedFromX(input.Position.X) end end) UserInputService.InputChanged:Connect(function(input) if sliderDragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then setSpeedFromX(input.Position.X) end end) UserInputService.InputEnded:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then if sliderDragging then sliderDragging = false setKnobGrown(false) end panelDragging = false end end) -- Typing a speed: press Enter or click away to apply it speedBox.FocusLost:Connect(function() local n = tonumber(speedBox.Text) setSpeed(n or FOLLOW_SPEED) requestSave() end) setSpeed(FOLLOW_SPEED) -- Dragging the panel around local dragStart, dragStartPos dragHandle.InputBegan:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then panelDragging = true dragStart = input.Position dragStartPos = panel.Position end end) UserInputService.InputChanged:Connect(function(input) if panelDragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then local delta = input.Position - dragStart local vp = speedGui.AbsoluteSize local size = panel.AbsoluteSize -- panel is anchored bottom-center, so clamp its center-x and bottom-y to stay on screen local cx = dragStartPos.X.Scale * vp.X + dragStartPos.X.Offset + delta.X local by = dragStartPos.Y.Scale * vp.Y + dragStartPos.Y.Offset + delta.Y cx = math.clamp(cx, size.X / 2, vp.X - size.X / 2) by = math.clamp(by, size.Y, vp.Y) panel.Position = UDim2.new( dragStartPos.X.Scale, cx - dragStartPos.X.Scale * vp.X, dragStartPos.Y.Scale, by - dragStartPos.Y.Scale * vp.Y ) end end) -- Smooth fade in/out local fadeInfo = TweenInfo.new(0.18, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) local uiOpen = false local function toggleSpeedUI() uiOpen = not uiOpen if uiOpen then setSpeed(FOLLOW_SPEED) speedGui.Enabled = true TweenService:Create(panel, fadeInfo, { GroupTransparency = 0 }):Play() else local fadeOut = TweenService:Create(panel, fadeInfo, { GroupTransparency = 1 }) fadeOut.Completed:Connect(function() if not uiOpen then speedGui.Enabled = false end end) fadeOut:Play() end end --// FREECAM MODE (press X): swoop up, fly around, draw with left mouse, press X to dash local Lighting = game:GetService("Lighting") local cam = workspace.CurrentCamera local savedCamType = Enum.CameraType.Custom local savedCamCF = CFrame.new() local baseFOV = 70 local camPos = Vector3.zero local yaw, pitch, targetYaw, targetPitch = 0, 0, 0, 0 local velocity = Vector3.zero local lookHeld = false local xHoldStart = nil -- os.clock() when X was pressed in "stay" mode local transition = nil -- { from, to, t, dur, onDone } while the camera swoops local freecamSpeed = FREECAM_SPEED -- Screen effects for the GTA-style "character swap" flash local swapGui = Instance.new("ScreenGui") swapGui.Name = "PathDashSwapFX" swapGui.ResetOnSpawn = false swapGui.IgnoreGuiInset = true swapGui.DisplayOrder = 100 swapGui.Parent = playerGui local flash = Instance.new("Frame") flash.Size = UDim2.fromScale(1, 1) flash.BackgroundColor3 = Color3.new(1, 1, 1) flash.BackgroundTransparency = 1 flash.BorderSizePixel = 0 flash.Active = false flash.Parent = swapGui local hint = Instance.new("TextLabel") hint.AnchorPoint = Vector2.new(0.5, 0) hint.Position = UDim2.new(0.5, 0, 0, 24) hint.Size = UDim2.new(0.9, 0, 0, 40) hint.BackgroundTransparency = 1 hint.Font = Enum.Font.GothamMedium hint.TextSize = 15 hint.TextWrapped = true hint.TextColor3 = Color3.new(1, 1, 1) hint.TextStrokeColor3 = Color3.new(0, 0, 0) hint.TextTransparency = 1 hint.TextStrokeTransparency = 1 hint.Text = "FREECAM • Left click: draw • Right click + mouse: look • WASD / Q / E: move • Shift: boost • Scroll: speed • C: clear • X: dash" hint.Parent = swapGui local swapBlur = Instance.new("BlurEffect") swapBlur.Size = 0 swapBlur.Parent = Lighting local swapColor = Instance.new("ColorCorrectionEffect") swapColor.Parent = Lighting local function setHint(visible) TweenService:Create(hint, TweenInfo.new(0.4), { TextTransparency = visible and 0.1 or 1, TextStrokeTransparency = visible and 0.6 or 1, }):Play() end local function playSwapFlash() if not SCREEN_FLASH then return end -- photosensitivity option: no flash, blur or color pop task.spawn(function() -- quick white pop... local popIn = TweenService:Create(flash, TweenInfo.new(0.06, Enum.EasingStyle.Quad), { BackgroundTransparency = 0.05 }) swapBlur.Size = 32 swapColor.Brightness = 0.25 swapColor.Saturation = -0.7 swapColor.Contrast = 0.4 popIn:Play() popIn.Completed:Wait() -- ...then everything relaxes back to normal local fade = TweenInfo.new(0.75, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) TweenService:Create(flash, TweenInfo.new(0.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), { BackgroundTransparency = 1 }):Play() TweenService:Create(swapBlur, fade, { Size = 0 }):Play() TweenService:Create(swapColor, fade, { Brightness = 0, Saturation = 0, Contrast = 0 }):Play() end) end local function setControlsEnabled(enabled) pcall(function() local scripts = player:FindFirstChild("PlayerScripts") local module = scripts and scripts:FindFirstChild("PlayerModule") if module then local controls = require(module):GetControls() if enabled then controls:Enable() else controls:Disable() end end end) end local function startSwoop(fromCF, toCF, duration, onDone) transition = { from = fromCF, to = toCF, t = 0, dur = duration, onDone = onDone } end -- Runs every frame, right after Roblox's own camera, so we stay in control of it local function camStep(dt) if not (transition or freecamActive) then return end if transition then transition.t += dt local a = math.clamp(transition.t / transition.dur, 0, 1) local e = TweenService:GetValue(a, Enum.EasingStyle.Quart, Enum.EasingDirection.InOut) cam.CFrame = transition.from:Lerp(transition.to, e) cam.FieldOfView = baseFOV + math.sin(math.pi * a) * FREECAM_FOV_PUNCH if a >= 1 then local done = transition.onDone transition = nil cam.FieldOfView = baseFOV if done then done() end end return end -- Smooth look local lookAlpha = 1 - math.exp(-FREECAM_LOOK_SMOOTHING * dt) yaw += (targetYaw - yaw) * lookAlpha pitch += (targetPitch - pitch) * lookAlpha local rot = CFrame.fromEulerAnglesYXZ(pitch, yaw, 0) -- Smooth movement local move = Vector3.zero if not UserInputService:GetFocusedTextBox() then if UserInputService:IsKeyDown(Enum.KeyCode.W) then move += Vector3.new(0, 0, -1) end if UserInputService:IsKeyDown(Enum.KeyCode.S) then move += Vector3.new(0, 0, 1) end if UserInputService:IsKeyDown(Enum.KeyCode.A) then move += Vector3.new(-1, 0, 0) end if UserInputService:IsKeyDown(Enum.KeyCode.D) then move += Vector3.new(1, 0, 0) end if UserInputService:IsKeyDown(Enum.KeyCode.E) then move += Vector3.new(0, 1, 0) end if UserInputService:IsKeyDown(Enum.KeyCode.Q) then move += Vector3.new(0, -1, 0) end end local worldMove = rot:VectorToWorldSpace(Vector3.new(move.X, 0, move.Z)) + Vector3.new(0, move.Y, 0) if worldMove.Magnitude > 0 then worldMove = worldMove.Unit end local speed = freecamSpeed if UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then speed *= FREECAM_BOOST end velocity = velocity:Lerp(worldMove * speed, 1 - math.exp(-FREECAM_MOVE_SMOOTHING * dt)) camPos += velocity * dt cam.CFrame = CFrame.new(camPos) * rot end RunService:BindToRenderStep("PathDashFreecam", Enum.RenderPriority.Camera.Value + 1, camStep) local function enterFreecam() local char, hrp = getCharacter() if not char then return end cam = workspace.CurrentCamera freecamBusy = true freecamActive = true savedCamType = cam.CameraType savedCamCF = cam.CFrame baseFOV = cam.FieldOfView velocity = Vector3.zero setControlsEnabled(false) cam.CameraType = Enum.CameraType.Scriptable cam.CFrame = savedCamCF UserInputService.MouseBehavior = Enum.MouseBehavior.Default -- Swoop up above the character, looking down at them local look = savedCamCF.LookVector targetYaw = math.atan2(-look.X, -look.Z) targetPitch = math.rad(FREECAM_START_PITCH) yaw, pitch = targetYaw, targetPitch local rot = CFrame.fromEulerAnglesYXZ(pitch, yaw, 0) local toCF = CFrame.new(hrp.Position - rot.LookVector * FREECAM_START_DISTANCE) * rot playSwapFlash() startSwoop(savedCamCF, toCF, FREECAM_TRANSITION, function() camPos = toCF.Position freecamBusy = false setHint(true) end) end -- startFollowing: dash along the path once the camera is back -- retarget: the character has moved since we entered, so swoop to a view behind them -- instead of the camera position we saved local function exitFreecam(startFollowing, retarget) freecamBusy = true freecamActive = false freecamDrawing = false lookHeld = false UserInputService.MouseBehavior = Enum.MouseBehavior.Default setHint(false) local toCF = savedCamCF if retarget then local _, hrp = getCharacter() if hrp then local flat = Vector3.new(hrp.CFrame.LookVector.X, 0, hrp.CFrame.LookVector.Z) if flat.Magnitude < 0.01 then flat = Vector3.new(0, 0, -1) end flat = flat.Unit local focus = hrp.Position + Vector3.new(0, 1.5, 0) toCF = CFrame.lookAt(focus - flat * 12 + Vector3.new(0, 4, 0), focus) end end playSwapFlash() startSwoop(cam.CFrame, toCF, FREECAM_TRANSITION * 0.8, function() cam.CameraType = savedCamType setControlsEnabled(true) freecamBusy = false if startFollowing then followPath() -- does nothing if no path was drawn end end) end -- newChar: the freshly spawned character, so the camera can be pointed at it local function forceResetFreecam(newChar) transition = nil freecamActive = false freecamBusy = false freecamDrawing = false lookHeld = false velocity = Vector3.zero UserInputService.MouseBehavior = Enum.MouseBehavior.Default cam = workspace.CurrentCamera cam.CameraType = Enum.CameraType.Custom cam.FieldOfView = baseFOV -- While we were Scriptable the camera never followed the respawn, so it was still -- pointed at the dead character's humanoid. Hand it the new one. if newChar then task.spawn(function() local newHum = newChar:WaitForChild("Humanoid", 5) if newHum then cam.CameraSubject = newHum end cam.CameraType = Enum.CameraType.Custom end) end setControlsEnabled(true) setHint(false) end local function startFreecamStroke() freecamDrawing = true local p, n = sampleSurfacePoint() if p then addPoint(p, n) end end UserInputService.InputEnded:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 then freecamDrawing = false elseif input.UserInputType == Enum.UserInputType.MouseButton2 then if lookHeld then lookHeld = false UserInputService.MouseBehavior = Enum.MouseBehavior.Default end end end) UserInputService.InputChanged:Connect(function(input, processed) if not freecamActive or freecamBusy then return end if input.UserInputType == Enum.UserInputType.MouseMovement then if lookHeld then targetYaw -= input.Delta.X * FREECAM_LOOK_SENS targetPitch = math.clamp(targetPitch - input.Delta.Y * FREECAM_LOOK_SENS, math.rad(-89), math.rad(89)) end elseif input.UserInputType == Enum.UserInputType.MouseWheel and not processed then freecamSpeed = math.clamp(freecamSpeed * (1.15 ^ input.Position.Z), 5, 1000) end end) --// "ENDING FREECAM" hold-to-exit bar (used when "Stay in freecam" is on) local HINT_NORMAL = hint.Text local HINT_STAY = "FREECAM • Left click: draw • Right click + mouse: look • WASD / Q / E: move • Shift: boost • Scroll: speed • C: clear • X: dash • Hold X: exit freecam" local holdGroup = Instance.new("CanvasGroup") holdGroup.AnchorPoint = Vector2.new(0.5, 0.5) holdGroup.Position = UDim2.fromScale(0.5, 0.7) holdGroup.Size = UDim2.fromOffset(240, 58) holdGroup.BackgroundColor3 = Color3.fromRGB(20, 22, 30) holdGroup.BackgroundTransparency = 1 -- everything starts invisible; setHoldBarVisible fades each part holdGroup.BorderSizePixel = 0 holdGroup.Parent = swapGui Instance.new("UICorner", holdGroup).CornerRadius = UDim.new(0, 14) local holdStroke = Instance.new("UIStroke") holdStroke.Color = Color3.new(1, 1, 1) holdStroke.Transparency = 1 holdStroke.Parent = holdGroup local holdLabel = Instance.new("TextLabel") holdLabel.BackgroundTransparency = 1 holdLabel.Position = UDim2.fromOffset(0, 8) holdLabel.Size = UDim2.new(1, 0, 0, 22) holdLabel.Font = Enum.Font.GothamBold holdLabel.TextSize = 15 holdLabel.TextColor3 = Color3.new(1, 1, 1) holdLabel.TextTransparency = 1 holdLabel.Text = "Ending freecam" holdLabel.Parent = holdGroup local holdTrack = Instance.new("Frame") holdTrack.Position = UDim2.new(0, 20, 0, 38) holdTrack.Size = UDim2.new(1, -40, 0, 6) holdTrack.BackgroundColor3 = Color3.new(1, 1, 1) holdTrack.BackgroundTransparency = 1 holdTrack.BorderSizePixel = 0 holdTrack.Parent = holdGroup Instance.new("UICorner", holdTrack).CornerRadius = UDim.new(1, 0) local holdFill = Instance.new("Frame") holdFill.Size = UDim2.fromScale(0, 1) holdFill.BackgroundColor3 = PATH_COLOR holdFill.BackgroundTransparency = 1 holdFill.BorderSizePixel = 0 holdFill.Parent = holdTrack Instance.new("UICorner", holdFill).CornerRadius = UDim.new(1, 0) local holdShown = false -- The whole bar is invisible until X has been held for a moment, then it fades in smoothly local function setHoldBarVisible(visible) if holdShown == visible then return end holdShown = visible -- fade every part explicitly (box, outline, text, bar track, bar fill) so they all move together local info = TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) TweenService:Create(holdGroup, info, { BackgroundTransparency = visible and 0.45 or 1 }):Play() TweenService:Create(holdStroke, info, { Transparency = visible and 0.8 or 1 }):Play() TweenService:Create(holdLabel, info, { TextTransparency = visible and 0 or 1 }):Play() TweenService:Create(holdTrack, info, { BackgroundTransparency = visible and 0.85 or 1 }):Play() TweenService:Create(holdFill, info, { BackgroundTransparency = visible and 0 or 1 }):Play() end -- On release the bar drains back down to empty while it fades away local drainTween = nil local function drainHoldBar() setHoldBarVisible(false) if drainTween then drainTween:Cancel() end drainTween = TweenService:Create(holdFill, TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), { Size = UDim2.fromScale(0, 1), }) drainTween:Play() end local function cancelDashNow() followToken += 1 following = false clearVisuals() table.clear(points) end RunService.RenderStepped:Connect(function() -- keep the on-screen hint in sync with the "Stay in freecam" switch local wantHint = STAY_IN_FREECAM and HINT_STAY or HINT_NORMAL if hint.Text ~= wantHint then hint.Text = wantHint end if not xHoldStart then return end if not freecamActive or freecamBusy then -- freecam ended some other way (respawn etc.) xHoldStart = nil drainHoldBar() return end -- a new hold started while the old bar was still draining if drainTween then drainTween:Cancel() drainTween = nil end local elapsed = os.clock() - xHoldStart holdFill.Size = UDim2.fromScale(math.clamp(elapsed / FREECAM_EXIT_HOLD, 0, 1), 1) -- only show the bar once it's clearly a hold, so quick taps don't flash it if elapsed >= 0.2 then setHoldBarVisible(true) end if elapsed >= FREECAM_EXIT_HOLD then xHoldStart = nil drainHoldBar() if following then cancelDashNow() end exitFreecam(false, true) end end) UserInputService.InputEnded:Connect(function(input) if input.KeyCode ~= FREECAM_KEY or not xHoldStart then return end -- released before the hold finished: that was a tap xHoldStart = nil drainHoldBar() if freecamActive and not freecamBusy then if following then cancelDashNow() -- tap during a dash cancels it (you stay in freecam) elseif #points >= 1 then freecamDrawing = false followPath() -- dash, and keep flying the freecam end end end) local function stopDrawing() if not drawing then return end drawing = false followPath() end UserInputService.InputBegan:Connect(function(input, processed) if processed then return end if input.KeyCode == KEY then if following then -- pressing the key again cancels the dash followToken += 1 following = false clearVisuals() table.clear(points) return end if freecamActive or freecamBusy then return end startDrawing() elseif input.KeyCode == SPEED_UI_KEY then toggleSpeedUI() elseif input.KeyCode == FREECAM_KEY then if freecamBusy or drawing then return end if freecamActive and STAY_IN_FREECAM then -- "stay" mode: decided on release. Tap = dash (or cancel the dash), -- hold for FREECAM_EXIT_HOLD seconds = exit freecam. xHoldStart = os.clock() elseif following then -- pressing the key while dashing cancels the dash followToken += 1 following = false clearVisuals() table.clear(points) elseif freecamActive then exitFreecam(true) -- fly back and dash along the path else enterFreecam() end elseif input.KeyCode == Enum.KeyCode.C then if freecamActive and not freecamBusy and not following then table.clear(points) clearVisuals() end elseif input.UserInputType == Enum.UserInputType.MouseButton1 then if freecamActive and not freecamBusy and not following then startFreecamStroke() end elseif input.UserInputType == Enum.UserInputType.MouseButton2 then if freecamActive and not freecamBusy then lookHeld = true UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition end end end) UserInputService.InputEnded:Connect(function(input) if input.KeyCode == KEY then stopDrawing() end end) RunService.RenderStepped:Connect(function() if drawing or freecamDrawing then local p, n = sampleSurfacePoint() if p then addPoint(p, n) end end end) -- Cancel everything if the character dies / respawns player.CharacterAdded:Connect(function(newChar) if freecamActive or freecamBusy then forceResetFreecam(newChar) end drawing = false following = false followToken += 1 table.clear(points) clearVisuals() end) --// LOAD SAVED SETTINGS (runs in the background so it never delays startup) task.spawn(function() local loadFunction = ReplicatedStorage:WaitForChild("PathDashLoad", 10) local serverSaveEvent = ReplicatedStorage:WaitForChild("PathDashSave", 10) if not loadFunction or not serverSaveEvent then warn("PathDash: settings won't be remembered. Put the PathDashSaveServer script in ServerScriptService.") return end saveEvent = serverSaveEvent local ok, data = pcall(function() return loadFunction:InvokeServer() end) if not ok or typeof(data) ~= "table" or settingsTouched then return end if typeof(data.speed) == "number" then setSpeed(data.speed) end if typeof(data.stayInFreecam) == "boolean" then STAY_IN_FREECAM = data.stayInFreecam refreshStayToggle() end if typeof(data.screenFlash) == "boolean" then SCREEN_FLASH = data.screenFlash refreshFlashToggle() end end)