--[[
██████╗ █████╗ ██████╗ ██╗ ██╗ ██████╗ ██╗ ██╗██████╗
██╔══██╗██╔══██╗██╔══██╗██║ ██╔╝██╔═══██╗██║ ██║██╔══██╗
██████╔╝███████║██████╔╝█████╔╝ ██║ ██║██║ ██║██████╔╝
██╔═══╝ ██╔══██║██╔══██╗██╔═██╗ ██║ ██║██║ ██║██╔══██╗
██║ ██║ ██║██║ ██║██║ ██╗╚██████╔╝╚██████╔╝██║ ██║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
Advanced Parkour System v2.0
- Wall Run (Left/Right detection + gravity curve)
- Slide (Speed burst + hitbox shrink + camera tilt)
- Vault (Obstacle detection + lerp over)
- Custom procedural animations (no imports)
- Minecraft-style HUD (blocky stamina bar + move indicator)
Keybinds:
[LEFT SHIFT] while moving = Slide
[SPACE] near wall + moving = Wall Run
[SPACE] near low obstacle = Vault
]]
-- // Services
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local Player = Players.LocalPlayer
local Camera = workspace.CurrentCamera
-- // Wait for character
local Character = Player.Character or Player.CharacterAdded:Wait()
local Humanoid = Character:WaitForChild("Humanoid")
local RootPart = Character:WaitForChild("HumanoidRootPart")
local Animator = Humanoid:WaitForChild("Animator")
-- // Respawn handler
Player.CharacterAdded:Connect(function(char)
Character = char
Humanoid = char:WaitForChild("Humanoid")
RootPart = char:WaitForChild("HumanoidRootPart")
Animator = Humanoid:WaitForChild("Animator")
end)
-- // Config
local CONFIG = {
-- Wall Run
WallRunSpeed = 28,
WallRunDuration = 1.4,
WallRunGravity = 0.35,
WallRunRayDist = 3.2,
WallRunCooldown = 0.5,
WallRunMinSpeed = 8,
WallRunCameraTilt = 15,
-- Slide
SlideSpeed = 38,
SlideDuration = 1.0,
SlideCooldown = 0.6,
SlideHitboxScale = 0.5,
SlideCameraTilt = -8,
SlideMinSpeed = 10,
SlideFriction = 0.92,
-- Vault
VaultRayDist = 4.5,
VaultMaxHeight = 7.5,
VaultMinHeight = 1.5,
VaultSpeed = 0.3,
VaultCooldown = 0.4,
-- Stamina
MaxStamina = 100,
StaminaRegen = 18,
WallRunStaminaCost = 30,
SlideStaminaCost = 15,
VaultStaminaCost = 10,
-- General
DefaultWalkSpeed = 16,
SprintSpeed = 24,
}
-- // State
local State = {
isWallRunning = false,
wallRunSide = nil, -- "Left" or "Right"
wallRunTime = 0,
wallRunCooldown = 0,
isSliding = false,
slideTime = 0,
slideCooldown = 0,
slideDir = nil,
isVaulting = false,
vaultCooldown = 0,
stamina = CONFIG.MaxStamina,
isSprinting = false,
shiftHeld = false,
spaceHeld = false,
}
------------------------------------------------------------------------
-- MINECRAFT-STYLE HUD
------------------------------------------------------------------------
local function CreateHUD()
-- Destroy old GUI if re-running
local old = Player.PlayerGui:FindFirstChild("ParkourHUD")
if old then old:Destroy() end
local ScreenGui = Instance.new("ScreenGui")
ScreenGui.Name = "ParkourHUD"
ScreenGui.ResetOnSpawn = false
ScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
ScreenGui.Parent = Player.PlayerGui
-- Main container (bottom center)
local Container = Instance.new("Frame")
Container.Name = "Container"
Container.Size = UDim2.new(0, 280, 0, 64)
Container.Position = UDim2.new(0.5, -140, 1, -90)
Container.BackgroundColor3 = Color3.fromRGB(30, 30, 30)
Container.BorderSizePixel = 0
Container.Parent = ScreenGui
-- Blocky border (MC style - 3px dark outline)
local Border = Instance.new("UIStroke")
Border.Thickness = 3
Border.Color = Color3.fromRGB(12, 12, 12)
Border.Parent = Container
-- Inner border highlight
local InnerBorder = Instance.new("Frame")
InnerBorder.Name = "InnerBorder"
InnerBorder.Size = UDim2.new(1, -6, 1, -6)
InnerBorder.Position = UDim2.new(0, 3, 0, 3)
InnerBorder.BackgroundColor3 = Color3.fromRGB(48, 48, 48)
InnerBorder.BorderSizePixel = 0
InnerBorder.Parent = Container
local InnerStroke = Instance.new("UIStroke")
InnerStroke.Thickness = 1
InnerStroke.Color = Color3.fromRGB(60, 60, 60)
InnerStroke.Parent = InnerBorder
-- Stamina label (pixely look using Code font)
local StaminaLabel = Instance.new("TextLabel")
StaminaLabel.Name = "StaminaLabel"
StaminaLabel.Size = UDim2.new(0, 70, 0, 16)
StaminaLabel.Position = UDim2.new(0, 10, 0, 6)
StaminaLabel.BackgroundTransparency = 1
StaminaLabel.Text = "STAMINA"
StaminaLabel.TextColor3 = Color3.fromRGB(180, 180, 180)
StaminaLabel.TextSize = 11
StaminaLabel.Font = Enum.Font.Code
StaminaLabel.TextXAlignment = Enum.TextXAlignment.Left
StaminaLabel.Parent = InnerBorder
-- Stamina bar background
local StaminaBG = Instance.new("Frame")
StaminaBG.Name = "StaminaBG"
StaminaBG.Size = UDim2.new(1, -20, 0, 14)
StaminaBG.Position = UDim2.new(0, 10, 0, 22)
StaminaBG.BackgroundColor3 = Color3.fromRGB(20, 20, 20)
StaminaBG.BorderSizePixel = 0
StaminaBG.Parent = InnerBorder
local BarStroke = Instance.new("UIStroke")
BarStroke.Thickness = 2
BarStroke.Color = Color3.fromRGB(10, 10, 10)
BarStroke.Parent = StaminaBG
-- Stamina bar fill
local StaminaFill = Instance.new("Frame")
StaminaFill.Name = "StaminaFill"
StaminaFill.Size = UDim2.new(1, 0, 1, 0)
StaminaFill.BackgroundColor3 = Color3.fromRGB(50, 205, 50)
StaminaFill.BorderSizePixel = 0
StaminaFill.Parent = StaminaBG
-- Pixelated shine on stamina bar (MC hearts style)
local Shine = Instance.new("Frame")
Shine.Name = "Shine"
Shine.Size = UDim2.new(1, 0, 0.35, 0)
Shine.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
Shine.BackgroundTransparency = 0.8
Shine.BorderSizePixel = 0
Shine.Parent = StaminaFill
-- Move indicator label
local MoveLabel = Instance.new("TextLabel")
MoveLabel.Name = "MoveLabel"
MoveLabel.Size = UDim2.new(1, -20, 0, 18)
MoveLabel.Position = UDim2.new(0, 10, 0, 38)
MoveLabel.BackgroundTransparency = 1
MoveLabel.Text = "[ IDLE ]"
MoveLabel.TextColor3 = Color3.fromRGB(255, 255, 85)
MoveLabel.TextSize = 13
MoveLabel.Font = Enum.Font.Code
MoveLabel.TextXAlignment = Enum.TextXAlignment.Center
MoveLabel.Parent = InnerBorder
-- Crosshair (MC style - small + in center)
local Crosshair = Instance.new("TextLabel")
Crosshair.Name = "Crosshair"
Crosshair.Size = UDim2.new(0, 20, 0, 20)
Crosshair.Position = UDim2.new(0.5, -10, 0.5, -10)
Crosshair.BackgroundTransparency = 1
Crosshair.Text = "+"
Crosshair.TextColor3 = Color3.fromRGB(255, 255, 255)
Crosshair.TextSize = 22
Crosshair.Font = Enum.Font.Code
Crosshair.TextStrokeTransparency = 0.5
Crosshair.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
Crosshair.Parent = ScreenGui
-- Keybind hint (top left, MC-style tooltip)
local HintFrame = Instance.new("Frame")
HintFrame.Name = "HintFrame"
HintFrame.Size = UDim2.new(0, 220, 0, 80)
HintFrame.Position = UDim2.new(0, 12, 0, 12)
HintFrame.BackgroundColor3 = Color3.fromRGB(20, 10, 30)
HintFrame.BackgroundTransparency = 0.25
HintFrame.BorderSizePixel = 0
HintFrame.Parent = ScreenGui
local HintStroke = Instance.new("UIStroke")
HintStroke.Thickness = 2
HintStroke.Color = Color3.fromRGB(80, 40, 120)
HintStroke.Parent = HintFrame
local HintText = Instance.new("TextLabel")
HintText.Size = UDim2.new(1, -12, 1, -8)
HintText.Position = UDim2.new(0, 6, 0, 4)
HintText.BackgroundTransparency = 1
HintText.RichText = true
HintText.Text = '<font color="#FFFF55">[SHIFT]</font> Slide\n<font color="#FFFF55">[SPACE]</font> Wall Run / Vault\n<font color="#FFFF55">[W+SHIFT]</font> Sprint'
HintText.TextColor3 = Color3.fromRGB(200, 200, 200)
HintText.TextSize = 12
HintText.Font = Enum.Font.Code
HintText.TextXAlignment = Enum.TextXAlignment.Left
HintText.TextYAlignment = Enum.TextYAlignment.Top
HintText.Parent = HintFrame
-- Fade hint after 8 seconds
task.delay(8, function()
local tw = TweenService:Create(HintFrame, TweenInfo.new(1.5), {BackgroundTransparency = 1})
local tw2 = TweenService:Create(HintStroke, TweenInfo.new(1.5), {Transparency = 1})
local tw3 = TweenService:Create(HintText, TweenInfo.new(1.5), {TextTransparency = 1})
tw:Play() tw2:Play() tw3:Play()
end)
return {
StaminaFill = StaminaFill,
MoveLabel = MoveLabel,
}
end
local HUD = CreateHUD()
------------------------------------------------------------------------
-- CUSTOM PROCEDURAL ANIMATIONS (no imported anims)
------------------------------------------------------------------------
local AnimationModule = {}
-- Create a KeyframeSequence programmatically, register as animation
local function BuildAnimation(name, length, looped, keyframes)
local kfs = Instance.new("KeyframeSequence")
kfs.Name = name
kfs.Loop = looped
for _, kfData in ipairs(keyframes) do
local kf = Instance.new("Keyframe")
kf.Time = kfData.Time
for _, poseData in ipairs(kfData.Poses) do
local pose = Instance.new("Pose")
pose.Name = poseData.Name
pose.CFrame = poseData.CFrame
pose.EasingStyle = poseData.EasingStyle or Enum.PoseEasingStyle.Linear
pose.EasingDirection = poseData.EasingDirection or Enum.PoseEasingDirection.InOut
-- Handle sub-poses (limb hierarchy)
if poseData.SubPoses then
for _, subData in ipairs(poseData.SubPoses) do
local subPose = Instance.new("Pose")
subPose.Name = subData.Name
subPose.CFrame = subData.CFrame
subPose.EasingStyle = subData.EasingStyle or Enum.PoseEasingStyle.Linear
subPose.Parent = pose
end
end
pose.Parent = kf
end
kf.Parent = kfs
end
-- Register with animator
local animId = Animator:LoadAnimation(
(function()
local anim = Instance.new("Animation")
local reg = game:GetService("KeyframeSequenceProvider"):RegisterKeyframeSequence(kfs)
anim.AnimationId = reg
return anim
end)()
)
return animId
end
-- WALL RUN ANIMATION (body tilts sideways, legs cycling)
local function CreateWallRunAnim(side)
local tiltAngle = side == "Right" and math.rad(-35) or math.rad(35)
local armReach = side == "Right" and math.rad(-60) or math.rad(60)
return BuildAnimation("WallRun_" .. side, 0.6, true, {
{
Time = 0,
Poses = {
{
Name = "HumanoidRootPart",
CFrame = CFrame.Angles(0, 0, tiltAngle * 0.3),
SubPoses = {
{
Name = "LowerTorso",
CFrame = CFrame.Angles(math.rad(10), 0, 0),
}
}
},
{
Name = "LeftUpperLeg",
CFrame = CFrame.Angles(math.rad(-45), 0, 0),
},
{
Name = "RightUpperLeg",
CFrame = CFrame.Angles(math.rad(35), 0, 0),
},
{
Name = "LeftUpperArm",
CFrame = CFrame.Angles(math.rad(20), 0, math.rad(-15)),
},
{
Name = "RightUpperArm",
CFrame = CFrame.Angles(armReach, 0, math.rad(15)),
},
}
},
{
Time = 0.3,
Poses = {
{
Name = "HumanoidRootPart",
CFrame = CFrame.Angles(0, 0, tiltAngle * 0.3),
},
{
Name = "LeftUpperLeg",
CFrame = CFrame.Angles(math.rad(35), 0, 0),
},
{
Name = "RightUpperLeg",
CFrame = CFrame.Angles(math.rad(-45), 0, 0),
},
{
Name = "LeftUpperArm",
CFrame = CFrame.Angles(math.rad(-30), 0, math.rad(-15)),
},
{
Name = "RightUpperArm",
CFrame = CFrame.Angles(math.rad(10), 0, math.rad(15)),
},
}
},
})
end
-- SLIDE ANIMATION (crouched, one leg forward, arms back)
local SlideAnim = BuildAnimation("Slide", 0.4, false, {
{
Time = 0,
Poses = {
{
Name = "HumanoidRootPart",
CFrame = CFrame.Angles(math.rad(-5), 0, 0),
SubPoses = {
{
Name = "LowerTorso",
CFrame = CFrame.Angles(math.rad(-25), 0, 0),
}
}
},
{
Name = "LeftUpperLeg",
CFrame = CFrame.Angles(math.rad(70), 0, 0),
},
{
Name = "RightUpperLeg",
CFrame = CFrame.Angles(math.rad(15), 0, math.rad(10)),
},
{
Name = "LeftUpperArm",
CFrame = CFrame.Angles(math.rad(30), 0, math.rad(-25)),
},
{
Name = "RightUpperArm",
CFrame = CFrame.Angles(math.rad(30), 0, math.rad(25)),
},
{
Name = "UpperTorso",
CFrame = CFrame.Angles(math.rad(-30), 0, 0),
},
{
Name = "Head",
CFrame = CFrame.Angles(math.rad(20), 0, 0),
},
}
},
})
-- VAULT ANIMATION (hands reach forward, body lifts up and over)
local VaultAnim = BuildAnimation("Vault", 0.35, false, {
{
Time = 0,
Poses = {
{
Name = "HumanoidRootPart",
CFrame = CFrame.Angles(math.rad(15), 0, 0),
},
{
Name = "LeftUpperArm",
CFrame = CFrame.Angles(math.rad(-120), 0, math.rad(-10)),
},
{
Name = "RightUpperArm",
CFrame = CFrame.Angles(math.rad(-120), 0, math.rad(10)),
},
{
Name = "LeftUpperLeg",
CFrame = CFrame.Angles(math.rad(-40), 0, 0),
},
{
Name = "RightUpperLeg",
CFrame = CFrame.Angles(math.rad(-40), 0, 0),
},
}
},
{
Time = 0.2,
Poses = {
{
Name = "HumanoidRootPart",
CFrame = CFrame.Angles(math.rad(-20), 0, 0),
},
{
Name = "LeftUpperArm",
CFrame = CFrame.Angles(math.rad(40), 0, math.rad(-20)),
},
{
Name = "RightUpperArm",
CFrame = CFrame.Angles(math.rad(40), 0, math.rad(20)),
},
{
Name = "LeftUpperLeg",
CFrame = CFrame.Angles(math.rad(50), 0, 0),
},
{
Name = "RightUpperLeg",
CFrame = CFrame.Angles(math.rad(50), 0, 0),
},
}
},
})
-- Pre-build wall run anims
local WallRunLeftAnim = CreateWallRunAnim("Left")
local WallRunRightAnim = CreateWallRunAnim("Right")
local currentAnim = nil
local function PlayAnim(track)
if currentAnim and currentAnim.IsPlaying then
currentAnim:Stop(0.15)
end
currentAnim = track
track:Play(0.15)
end
local function StopAnim()
if currentAnim and currentAnim.IsPlaying then
currentAnim:Stop(0.2)
currentAnim = nil
end
end
------------------------------------------------------------------------
-- RAYCAST HELPERS
------------------------------------------------------------------------
local RayParams = RaycastParams.new()
RayParams.FilterType = Enum.RaycastFilterType.Exclude
local function UpdateRayFilter()
RayParams.FilterDescendantsInstances = {Character}
end
UpdateRayFilter()
Player.CharacterAdded:Connect(function()
task.wait(0.5)
UpdateRayFilter()
end)
local function CastRay(origin, direction)
return workspace:Raycast(origin, direction, RayParams)
end
-- Check for wall on a specific side
local function CheckWall(side)
if not RootPart or not RootPart.Parent then return nil end
local dir = side == "Right" and RootPart.CFrame.RightVector or -RootPart.CFrame.RightVector
local result = CastRay(RootPart.Position, dir * CONFIG.WallRunRayDist)
if result and result.Instance and result.Instance.CanCollide then
-- Verify it's a roughly vertical surface
local dot = math.abs(result.Normal:Dot(Vector3.new(0, 1, 0)))
if dot < 0.3 then
return result
end
end
return nil
end
-- Check for vaultable obstacle ahead
local function CheckVault()
if not RootPart or not RootPart.Parent then return nil, nil end
local lookDir = RootPart.CFrame.LookVector
-- Cast forward at waist height
local waistResult = CastRay(
RootPart.Position + Vector3.new(0, -1, 0),
lookDir * CONFIG.VaultRayDist
)
if not waistResult or not waistResult.Instance or not waistResult.Instance.CanCollide then
return nil, nil
end
-- Cast downward from above the obstacle to find the top
local topOrigin = waistResult.Position + lookDir * 1.5 + Vector3.new(0, CONFIG.VaultMaxHeight, 0)
local topResult = CastRay(topOrigin, Vector3.new(0, -CONFIG.VaultMaxHeight * 2, 0))
if topResult then
local obstacleHeight = topResult.Position.Y - (RootPart.Position.Y - 3)
if obstacleHeight >= CONFIG.VaultMinHeight and obstacleHeight <= CONFIG.VaultMaxHeight then
return waistResult, topResult
end
end
return nil, nil
end
-- Check if character is grounded
local function IsGrounded()
if not RootPart or not RootPart.Parent then return false end
local result = CastRay(RootPart.Position, Vector3.new(0, -3.5, 0))
return result ~= nil
end
-- Get movement direction
local function GetMoveDirection()
if not Humanoid then return Vector3.zero end
return Humanoid.MoveDirection
end
local function IsMoving()
return GetMoveDirection().Magnitude > 0.1
end
------------------------------------------------------------------------
-- CAMERA TILT
------------------------------------------------------------------------
local currentTilt = 0
local targetTilt = 0
local function SetCameraTilt(degrees)
targetTilt = math.rad(degrees)
end
------------------------------------------------------------------------
-- WALL RUN SYSTEM
------------------------------------------------------------------------
local wallRunBV = nil
local function StartWallRun(side, wallResult)
if State.isWallRunning or State.isSliding or State.isVaulting then return end
if State.stamina < CONFIG.WallRunStaminaCost * 0.3 then return end
if tick() - State.wallRunCooldown < CONFIG.WallRunCooldown then return end
State.isWallRunning = true
State.wallRunSide = side
State.wallRunTime = 0
-- Calculate wall-parallel direction (upward + forward along wall)
local wallNormal = wallResult.Normal
local wallForward = wallNormal:Cross(Vector3.new(0, 1, 0)).Unit
if RootPart.CFrame.LookVector:Dot(wallForward) < 0 then
wallForward = -wallForward
end
-- Create BodyVelocity for wall run movement
wallRunBV = Instance.new("BodyVelocity")
wallRunBV.MaxForce = Vector3.new(50000, 50000, 50000)
wallRunBV.P = 10000
wallRunBV.Parent = RootPart
-- Play animation
local animTrack = side == "Right" and WallRunRightAnim or WallRunLeftAnim
PlayAnim(animTrack)
-- Camera tilt
SetCameraTilt(side == "Right" and CONFIG.WallRunCameraTilt or -CONFIG.WallRunCameraTilt)
-- Wall run update loop
local startTick = tick()
local conn
conn = RunService.Heartbeat:Connect(function(dt)
if not State.isWallRunning then
if conn then conn:Disconnect() end
return
end
State.wallRunTime = tick() - startTick
State.stamina = math.max(0, State.stamina - CONFIG.WallRunStaminaCost * dt)
-- Gravity curve: starts going up, gradually arcs down
local t = State.wallRunTime / CONFIG.WallRunDuration
local upForce = CONFIG.WallRunSpeed * (1 - t * CONFIG.WallRunGravity * 2.5)
local forwardForce = CONFIG.WallRunSpeed * (1 - t * 0.3)
if wallRunBV and wallRunBV.Parent then
wallRunBV.Velocity = wallForward * forwardForce + Vector3.new(0, math.max(upForce, -10), 0)
end
-- Re-check wall
local newWall = CheckWall(side)
-- End conditions
if State.wallRunTime >= CONFIG.WallRunDuration
or State.stamina <= 0
or not newWall
or IsGrounded() and State.wallRunTime > 0.2
or not State.spaceHeld then
StopWallRun()
if conn then conn:Disconnect() end
end
end)
end
function StopWallRun()
if not State.isWallRunning then return end
State.isWallRunning = false
State.wallRunCooldown = tick()
if wallRunBV then
wallRunBV:Destroy()
wallRunBV = nil
end
-- Small upward boost on release (wall jump)
if RootPart and RootPart.Parent then
local jumpBV = Instance.new("BodyVelocity")
jumpBV.MaxForce = Vector3.new(30000, 30000, 30000)
jumpBV.Velocity = RootPart.CFrame.LookVector * 18 + Vector3.new(0, 28, 0)
jumpBV.Parent = RootPart
game:GetService("Debris"):AddItem(jumpBV, 0.15)
end
SetCameraTilt(0)
StopAnim()
end
------------------------------------------------------------------------
-- SLIDE SYSTEM
------------------------------------------------------------------------
local slideBV = nil
local originalHipHeight = nil
local function StartSlide()
if State.isSliding or State.isWallRunning or State.isVaulting then return end
if not IsGrounded() or not IsMoving() then return end
if State.stamina < CONFIG.SlideStaminaCost * 0.5 then return end
if tick() - State.slideCooldown < CONFIG.SlideCooldown then return end
local moveDir = GetMoveDirection()
if moveDir.Magnitude < 0.1 then return end
-- Check minimum speed for slide
if RootPart.AssemblyLinearVelocity.Magnitude < CONFIG.SlideMinSpeed then return end
State.isSliding = true
State.slideTime = 0
State.slideDir = moveDir.Unit
State.stamina = math.max(0, State.stamina - CONFIG.SlideStaminaCost)
-- Shrink hitbox
originalHipHeight = Humanoid.HipHeight
Humanoid.HipHeight = originalHipHeight * CONFIG.SlideHitboxScale
-- Slide velocity
slideBV = Instance.new("BodyVelocity")
slideBV.MaxForce = Vector3.new(40000, 0, 40000)
slideBV.P = 8000
slideBV.Velocity = State.slideDir * CONFIG.SlideSpeed
slideBV.Parent = RootPart
-- Animation + camera
PlayAnim(SlideAnim)
SetCameraTilt(CONFIG.SlideCameraTilt)
local startTick = tick()
local conn
conn = RunService.Heartbeat:Connect(function(dt)
if not State.isSliding then
if conn then conn:Disconnect() end
return
end
State.slideTime = tick() - startTick
-- Apply friction
if slideBV and slideBV.Parent then
local speed = slideBV.Velocity.Magnitude * CONFIG.SlideFriction
slideBV.Velocity = State.slideDir * speed
end
-- End conditions
if State.slideTime >= CONFIG.SlideDuration or not State.shiftHeld then
StopSlide()
if conn then conn:Disconnect() end
end
end)
end
function StopSlide()
if not State.isSliding then return end
State.isSliding = false
State.slideCooldown = tick()
if slideBV then
slideBV:Destroy()
slideBV = nil
end
if originalHipHeight and Humanoid then
Humanoid.HipHeight = originalHipHeight
originalHipHeight = nil
end
SetCameraTilt(0)
StopAnim()
end
------------------------------------------------------------------------
-- VAULT SYSTEM
------------------------------------------------------------------------
local function StartVault()
if State.isVaulting or State.isWallRunning or State.isSliding then return end
if not IsMoving() then return end
if State.stamina < CONFIG.VaultStaminaCost then return end
if tick() - State.vaultCooldown < CONFIG.VaultCooldown then return end
local waistHit, topHit = CheckVault()
if not waistHit or not topHit then return end
State.isVaulting = true
State.stamina = math.max(0, State.stamina - CONFIG.VaultStaminaCost)
-- Play vault animation
PlayAnim(VaultAnim)
-- Calculate vault path
local startPos = RootPart.Position
local topPos = topHit.Position + Vector3.new(0, 3.5, 0)
local endPos = topPos + RootPart.CFrame.LookVector * 4
-- Disable default physics during vault
local bg = Instance.new("BodyGyro")
bg.MaxTorque = Vector3.new(100000, 100000, 100000)
bg.CFrame = RootPart.CFrame
bg.Parent = RootPart
local bv = Instance.new("BodyVelocity")
bv.MaxForce = Vector3.new(100000, 100000, 100000)
bv.P = 15000
bv.Parent = RootPart
-- Smooth vault lerp
local elapsed = 0
local duration = CONFIG.VaultSpeed
local conn
conn = RunService.Heartbeat:Connect(function(dt)
if not State.isVaulting then
if conn then conn:Disconnect() end
return
end
elapsed = elapsed + dt
local alpha = math.min(elapsed / duration, 1)
-- Bezier curve: start -> top -> end
local t = alpha
local p0 = startPos
local p1 = topPos + Vector3.new(0, 2, 0) -- control point (arc peak)
local p2 = endPos
local pos = (1 - t)^2 * p0 + 2 * (1 - t) * t * p1 + t^2 * p2
if bv and bv.Parent and RootPart and RootPart.Parent then
bv.Velocity = (pos - RootPart.Position) * 30
end
if alpha >= 1 then
State.isVaulting = false
State.vaultCooldown = tick()
if bv then bv:Destroy() end
if bg then bg:Destroy() end
StopAnim()
if conn then conn:Disconnect() end
end
end)
end
------------------------------------------------------------------------
-- INPUT HANDLING
------------------------------------------------------------------------
UserInputService.InputBegan:Connect(function(input, gpe)
if gpe then return end
if input.KeyCode == Enum.KeyCode.LeftShift then
State.shiftHeld = true
if IsMoving() and IsGrounded() then
-- If already moving fast, slide; otherwise sprint
if RootPart and RootPart.AssemblyLinearVelocity.Magnitude >= CONFIG.SlideMinSpeed then
StartSlide()
else
State.isSprinting = true
Humanoid.WalkSpeed = CONFIG.SprintSpeed
end
else
State.isSprinting = true
Humanoid.WalkSpeed = CONFIG.SprintSpeed
end
end
if input.KeyCode == Enum.KeyCode.Space then
State.spaceHeld = true
if not IsGrounded() and not State.isWallRunning then
-- Try wall run
local rightWall = CheckWall("Right")
local leftWall = CheckWall("Left")
if rightWall and IsMoving() then
StartWallRun("Right", rightWall)
elseif leftWall and IsMoving() then
StartWallRun("Left", leftWall)
end
elseif IsGrounded() and IsMoving() then
-- Try vault
StartVault()
end
end
end)
UserInputService.InputEnded:Connect(function(input, gpe)
if input.KeyCode == Enum.KeyCode.LeftShift then
State.shiftHeld = false
State.isSprinting = false
if not State.isSliding and not State.isWallRunning then
Humanoid.WalkSpeed = CONFIG.DefaultWalkSpeed
end
end
if input.KeyCode == Enum.KeyCode.Space then
State.spaceHeld = false
end
end)
------------------------------------------------------------------------
-- MAIN UPDATE LOOP (Stamina, HUD, Camera Tilt)
------------------------------------------------------------------------
RunService.RenderStepped:Connect(function(dt)
if not Character or not Character.Parent then return end
if not Humanoid or Humanoid.Health <= 0 then return end
-- Stamina regen (when not doing parkour moves)
if not State.isWallRunning and not State.isSliding and not State.isVaulting then
State.stamina = math.min(CONFIG.MaxStamina, State.stamina + CONFIG.StaminaRegen * dt)
end
-- Walk speed reset guard
if not State.isSliding and not State.isWallRunning and not State.isVaulting then
if State.isSprinting then
Humanoid.WalkSpeed = CONFIG.SprintSpeed
else
Humanoid.WalkSpeed = CONFIG.DefaultWalkSpeed
end
end
-- Update HUD
if HUD.StaminaFill and HUD.StaminaFill.Parent then
local pct = State.stamina / CONFIG.MaxStamina
HUD.StaminaFill.Size = UDim2.new(math.max(pct, 0), 0, 1, 0)
-- Color shift: green -> yellow -> red
if pct > 0.5 then
HUD.StaminaFill.BackgroundColor3 = Color3.fromRGB(50, 205, 50)
elseif pct > 0.25 then
HUD.StaminaFill.BackgroundColor3 = Color3.fromRGB(255, 200, 40)
else
HUD.StaminaFill.BackgroundColor3 = Color3.fromRGB(220, 50, 50)
end
end
-- Move label
if HUD.MoveLabel and HUD.MoveLabel.Parent then
local label = "[ IDLE ]"
local color = Color3.fromRGB(150, 150, 150)
if State.isWallRunning then
label = ">> WALL RUN [" .. (State.wallRunSide or "?") .. "] <<"
color = Color3.fromRGB(85, 255, 255)
elseif State.isSliding then
label = ">> SLIDE <<"
color = Color3.fromRGB(255, 170, 50)
elseif State.isVaulting then
label = ">> VAULT <<"
color = Color3.fromRGB(170, 85, 255)
elseif State.isSprinting and IsMoving() then
label = ">> SPRINT <<"
color = Color3.fromRGB(255, 255, 85)
elseif IsMoving() then
label = "[ MOVING ]"
color = Color3.fromRGB(200, 200, 200)
end
HUD.MoveLabel.Text = label
HUD.MoveLabel.TextColor3 = color
end
-- Smooth camera tilt
currentTilt = currentTilt + (targetTilt - currentTilt) * math.min(dt * 12, 1)
Camera.CFrame = Camera.CFrame * CFrame.Angles(0, 0, currentTilt)
end)
------------------------------------------------------------------------
-- CLEANUP ON DEATH
------------------------------------------------------------------------
Humanoid.Died:Connect(function()
StopWallRun()
StopSlide()
State.isVaulting = false
StopAnim()
SetCameraTilt(0)
end)
print("[PARKOUR] System loaded! | SHIFT=Slide/Sprint | SPACE=WallRun/Vault")