Quick actions

cmd+k|ctrl+k

Navigation

Languages

ParkourSystem_v3

Snippet info

Language

Lua

Visibility

public

Author

panickyhospital950

Created

2026-04-04T01:04:59.004696Z

Updated

2026-04-04T01:04:59.004696Z

--[[
    ██████╗  █████╗ ██████╗ ██╗  ██╗ ██████╗ ██╗   ██╗██████╗ 
    ██╔══██╗██╔══██╗██╔══██╗██║ ██╔╝██╔═══██╗██║   ██║██╔══██╗
    ██████╔╝███████║██████╔╝█████╔╝ ██║   ██║██║   ██║██████╔╝
    ██╔═══╝ ██╔══██║██╔══██╗██╔═██╗ ██║   ██║██║   ██║██╔══██╗
    ██║     ██║  ██║██║  ██║██║  ██╗╚██████╔╝╚██████╔╝██║  ██║
    ╚═╝     ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝  ╚═════╝ ╚═╝  ╚═╝
    
    Advanced Parkour System v3.0
    Motor6D CFrame animation (no server APIs needed)
    
    Keybinds:
      [LEFT SHIFT] while sprinting = Slide
      [SPACE] near wall + moving   = Wall Run
      [SPACE] near low obstacle    = Vault
      [LEFT SHIFT] + moving        = Sprint
]]

-- // Services
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")

local Player = Players.LocalPlayer
local Camera = workspace.CurrentCamera

-- // CONFIG
local CFG = {
    WallRunSpeed      = 26,
    WallRunDuration   = 1.3,
    WallRunGravity    = 0.4,
    WallRunRayDist    = 3.5,
    WallRunCooldown   = 0.4,
    WallRunCamTilt    = 14,

    SlideSpeed        = 36,
    SlideDuration     = 0.9,
    SlideCooldown     = 0.5,
    SlideCamTilt      = -7,
    SlideFriction     = 0.93,
    SlideMinSpeed     = 8,

    VaultRayDist      = 4.5,
    VaultMaxHeight    = 7,
    VaultMinHeight    = 1.2,
    VaultDuration     = 0.28,
    VaultCooldown     = 0.35,

    MaxStamina        = 100,
    StaminaRegen      = 20,
    WallRunDrain      = 28,
    SlideCost         = 12,
    VaultCost         = 8,

    WalkSpeed         = 16,
    SprintSpeed       = 22,
}

-- // CHARACTER SETUP
local Character, Humanoid, RootPart
local Motors = {} -- Store Motor6D references + their original C0

local function CacheMotors()
    Motors = {}
    if not Character then return end
    for _, desc in ipairs(Character:GetDescendants()) do
        if desc:IsA("Motor6D") then
            Motors[desc.Name] = {
                Motor = desc,
                OriginalC0 = desc.C0,
            }
        end
    end
end

local function SetupCharacter(char)
    Character = char
    Humanoid = char:WaitForChild("Humanoid")
    RootPart = char:WaitForChild("HumanoidRootPart")
    char:WaitForChild("Head") -- ensure loaded
    task.wait(0.1)
    CacheMotors()
end

if Player.Character then
    SetupCharacter(Player.Character)
end
Player.CharacterAdded:Connect(function(char)
    SetupCharacter(char)
end)

-- // STATE
local State = {
    isWallRunning = false,
    wallSide      = nil,
    wallTime      = 0,
    wallCD        = 0,

    isSliding     = false,
    slideTime     = 0,
    slideDir      = Vector3.zero,
    slideCD       = 0,

    isVaulting    = false,
    vaultCD       = 0,

    stamina       = CFG.MaxStamina,
    sprinting     = false,
    shiftHeld     = false,
    spaceHeld     = false,
}

------------------------------------------------------------------------
-- MOTOR6D ANIMATION SYSTEM (client-side, no server APIs)
------------------------------------------------------------------------
local AnimTargets = {} -- {motorName = targetCFrame}
local AnimAlpha = 0
local AnimSpeed = 8

local function ResetAnimTargets()
    AnimTargets = {}
end

-- Smoothly blend Motor6D C0 toward targets each frame
local function UpdateMotorAnimation(dt)
    for name, data in pairs(Motors) do
        local target = AnimTargets[name]
        if target then
            data.Motor.C0 = data.Motor.C0:Lerp(data.OriginalC0 * target, math.min(dt * AnimSpeed, 1))
        else
            -- Return to original
            data.Motor.C0 = data.Motor.C0:Lerp(data.OriginalC0, math.min(dt * AnimSpeed, 1))
        end
    end
end

-- Pose presets
local function SetWallRunPose(side, phase)
    local tilt = side == "Right" and -30 or 30
    local legSwing = math.sin(phase * math.pi * 2) * 50

    AnimTargets = {
        ["RootJoint"]       = CFrame.Angles(0, 0, math.rad(tilt * 0.4)),
        ["Left Hip"]        = CFrame.Angles(math.rad(-legSwing), 0, 0),
        ["Right Hip"]       = CFrame.Angles(math.rad(legSwing), 0, 0),
        ["Left Shoulder"]   = CFrame.Angles(math.rad(legSwing * 0.6), 0, math.rad(-10)),
        ["Right Shoulder"]  = CFrame.Angles(math.rad(-legSwing * 0.6), 0, math.rad(10)),
        ["Neck"]            = CFrame.Angles(math.rad(5), math.rad(side == "Right" and -15 or 15), 0),

        -- R15 equivalents
        ["Root"]            = CFrame.Angles(0, 0, math.rad(tilt * 0.4)),
        ["LeftHip"]         = CFrame.Angles(math.rad(-legSwing), 0, 0),
        ["RightHip"]        = CFrame.Angles(math.rad(legSwing), 0, 0),
        ["LeftShoulder"]    = CFrame.Angles(math.rad(legSwing * 0.6), 0, math.rad(-10)),
        ["RightShoulder"]   = CFrame.Angles(math.rad(-legSwing * 0.6), 0, math.rad(10)),
    }
    AnimSpeed = 12
end

local function SetSlidePose()
    AnimTargets = {
        -- R6
        ["RootJoint"]       = CFrame.Angles(math.rad(-15), 0, 0),
        ["Left Hip"]        = CFrame.Angles(math.rad(60), 0, 0),
        ["Right Hip"]       = CFrame.Angles(math.rad(10), 0, math.rad(8)),
        ["Left Shoulder"]   = CFrame.Angles(math.rad(25), 0, math.rad(-30)),
        ["Right Shoulder"]  = CFrame.Angles(math.rad(25), 0, math.rad(30)),
        ["Neck"]            = CFrame.Angles(math.rad(15), 0, 0),

        -- R15
        ["Root"]            = CFrame.Angles(math.rad(-15), 0, 0),
        ["LeftHip"]         = CFrame.Angles(math.rad(60), 0, 0),
        ["RightHip"]        = CFrame.Angles(math.rad(10), 0, math.rad(8)),
        ["LeftShoulder"]    = CFrame.Angles(math.rad(25), 0, math.rad(-30)),
        ["RightShoulder"]   = CFrame.Angles(math.rad(25), 0, math.rad(30)),
        ["Waist"]           = CFrame.Angles(math.rad(-25), 0, 0),
    }
    AnimSpeed = 14
end

local function SetVaultPose(phase)
    local armAngle = phase < 0.5 and -110 or 40
    local legAngle = phase < 0.5 and -35 or 45
    local torsoAngle = phase < 0.5 and 12 or -18

    AnimTargets = {
        -- R6
        ["RootJoint"]       = CFrame.Angles(math.rad(torsoAngle), 0, 0),
        ["Left Hip"]        = CFrame.Angles(math.rad(legAngle), 0, 0),
        ["Right Hip"]       = CFrame.Angles(math.rad(legAngle), 0, 0),
        ["Left Shoulder"]   = CFrame.Angles(math.rad(armAngle), 0, math.rad(-12)),
        ["Right Shoulder"]  = CFrame.Angles(math.rad(armAngle), 0, math.rad(12)),

        -- R15
        ["Root"]            = CFrame.Angles(math.rad(torsoAngle), 0, 0),
        ["LeftHip"]         = CFrame.Angles(math.rad(legAngle), 0, 0),
        ["RightHip"]        = CFrame.Angles(math.rad(legAngle), 0, 0),
        ["LeftShoulder"]    = CFrame.Angles(math.rad(armAngle), 0, math.rad(-12)),
        ["RightShoulder"]   = CFrame.Angles(math.rad(armAngle), 0, math.rad(12)),
        ["Waist"]           = CFrame.Angles(math.rad(torsoAngle * 0.5), 0, 0),
    }
    AnimSpeed = 18
end

------------------------------------------------------------------------
-- RAYCAST HELPERS
------------------------------------------------------------------------
local RayParams = RaycastParams.new()
RayParams.FilterType = Enum.RaycastFilterType.Exclude

local function UpdateFilter()
    if Character then
        RayParams.FilterDescendantsInstances = {Character}
    end
end

local function Cast(origin, dir)
    UpdateFilter()
    return workspace:Raycast(origin, dir, RayParams)
end

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 hit = Cast(RootPart.Position, dir * CFG.WallRunRayDist)
    if hit and hit.Instance and hit.Instance.CanCollide then
        local dot = math.abs(hit.Normal:Dot(Vector3.yAxis))
        if dot < 0.3 then return hit end
    end
    return nil
end

local function CheckVault()
    if not RootPart or not RootPart.Parent then return nil, nil end
    local look = RootPart.CFrame.LookVector
    local waist = Cast(RootPart.Position + Vector3.new(0, -1, 0), look * CFG.VaultRayDist)
    if not waist or not waist.Instance or not waist.Instance.CanCollide then return nil, nil end

    local topOrigin = waist.Position + look * 1.5 + Vector3.new(0, CFG.VaultMaxHeight, 0)
    local top = Cast(topOrigin, Vector3.new(0, -CFG.VaultMaxHeight * 2, 0))
    if top then
        local h = top.Position.Y - (RootPart.Position.Y - 3)
        if h >= CFG.VaultMinHeight and h <= CFG.VaultMaxHeight then
            return waist, top
        end
    end
    return nil, nil
end

local function IsGrounded()
    if not RootPart or not RootPart.Parent then return false end
    return Cast(RootPart.Position, Vector3.new(0, -3.5, 0)) ~= nil
end

local function MoveDir()
    if not Humanoid then return Vector3.zero end
    return Humanoid.MoveDirection
end

local function IsMoving()
    return MoveDir().Magnitude > 0.1
end

------------------------------------------------------------------------
-- HUD (Minecraft-style, blocky, Code font only)
------------------------------------------------------------------------
local function CreateHUD()
    local old = Player.PlayerGui:FindFirstChild("ParkourHUD")
    if old then old:Destroy() end

    local Gui = Instance.new("ScreenGui")
    Gui.Name = "ParkourHUD"
    Gui.ResetOnSpawn = false
    Gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
    Gui.Parent = Player.PlayerGui

    -- Bottom bar container
    local Bar = Instance.new("Frame")
    Bar.Name = "Bar"
    Bar.Size = UDim2.new(0, 280, 0, 58)
    Bar.Position = UDim2.new(0.5, -140, 1, -82)
    Bar.BackgroundColor3 = Color3.fromRGB(28, 28, 28)
    Bar.BorderSizePixel = 0
    Bar.Parent = Gui

    local stroke = Instance.new("UIStroke")
    stroke.Thickness = 3
    stroke.Color = Color3.fromRGB(10, 10, 10)
    stroke.Parent = Bar

    -- Inner panel
    local Inner = Instance.new("Frame")
    Inner.Size = UDim2.new(1, -8, 1, -8)
    Inner.Position = UDim2.new(0, 4, 0, 4)
    Inner.BackgroundColor3 = Color3.fromRGB(42, 42, 42)
    Inner.BorderSizePixel = 0
    Inner.Parent = Bar

    Instance.new("UIStroke", Inner).Color = Color3.fromRGB(55, 55, 55)

    -- Stamina text
    local sLabel = Instance.new("TextLabel")
    sLabel.Size = UDim2.new(0, 60, 0, 12)
    sLabel.Position = UDim2.new(0, 8, 0, 3)
    sLabel.BackgroundTransparency = 1
    sLabel.Text = "STAMINA"
    sLabel.TextColor3 = Color3.fromRGB(170, 170, 170)
    sLabel.TextSize = 10
    sLabel.Font = Enum.Font.Code
    sLabel.TextXAlignment = Enum.TextXAlignment.Left
    sLabel.Parent = Inner

    -- Stamina bar bg
    local sBG = Instance.new("Frame")
    sBG.Size = UDim2.new(1, -16, 0, 12)
    sBG.Position = UDim2.new(0, 8, 0, 16)
    sBG.BackgroundColor3 = Color3.fromRGB(18, 18, 18)
    sBG.BorderSizePixel = 0
    sBG.Parent = Inner

    Instance.new("UIStroke", sBG).Color = Color3.fromRGB(8, 8, 8)

    -- Stamina fill
    local sFill = Instance.new("Frame")
    sFill.Name = "Fill"
    sFill.Size = UDim2.new(1, 0, 1, 0)
    sFill.BackgroundColor3 = Color3.fromRGB(50, 205, 50)
    sFill.BorderSizePixel = 0
    sFill.Parent = sBG

    -- Shine
    local shine = Instance.new("Frame")
    shine.Size = UDim2.new(1, 0, 0.3, 0)
    shine.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
    shine.BackgroundTransparency = 0.82
    shine.BorderSizePixel = 0
    shine.Parent = sFill

    -- Action label
    local aLabel = Instance.new("TextLabel")
    aLabel.Name = "Action"
    aLabel.Size = UDim2.new(1, -16, 0, 16)
    aLabel.Position = UDim2.new(0, 8, 0, 30)
    aLabel.BackgroundTransparency = 1
    aLabel.Text = "[ IDLE ]"
    aLabel.TextColor3 = Color3.fromRGB(150, 150, 150)
    aLabel.TextSize = 12
    aLabel.Font = Enum.Font.Code
    aLabel.Parent = Inner

    -- Crosshair
    local cross = Instance.new("TextLabel")
    cross.Size = UDim2.new(0, 20, 0, 20)
    cross.Position = UDim2.new(0.5, -10, 0.5, -10)
    cross.BackgroundTransparency = 1
    cross.Text = "+"
    cross.TextColor3 = Color3.fromRGB(255, 255, 255)
    cross.TextSize = 20
    cross.Font = Enum.Font.Code
    cross.TextStrokeTransparency = 0.4
    cross.Parent = Gui

    -- Keybind hints
    local hints = Instance.new("Frame")
    hints.Size = UDim2.new(0, 210, 0, 70)
    hints.Position = UDim2.new(0, 10, 0, 10)
    hints.BackgroundColor3 = Color3.fromRGB(18, 8, 28)
    hints.BackgroundTransparency = 0.2
    hints.BorderSizePixel = 0
    hints.Parent = Gui

    local hs = Instance.new("UIStroke")
    hs.Color = Color3.fromRGB(75, 35, 115)
    hs.Thickness = 2
    hs.Parent = hints

    local ht = Instance.new("TextLabel")
    ht.Size = UDim2.new(1, -10, 1, -6)
    ht.Position = UDim2.new(0, 5, 0, 3)
    ht.BackgroundTransparency = 1
    ht.RichText = true
    ht.Text = '<font color="#FFFF55">[SHIFT]</font> Slide / Sprint\n<font color="#FFFF55">[SPACE]</font> Wall Run / Vault'
    ht.TextColor3 = Color3.fromRGB(190, 190, 190)
    ht.TextSize = 12
    ht.Font = Enum.Font.Code
    ht.TextXAlignment = Enum.TextXAlignment.Left
    ht.TextYAlignment = Enum.TextYAlignment.Top
    ht.Parent = hints

    task.delay(6, function()
        pcall(function()
            TweenService:Create(hints, TweenInfo.new(1.5), {BackgroundTransparency = 1}):Play()
            TweenService:Create(hs, TweenInfo.new(1.5), {Transparency = 1}):Play()
            TweenService:Create(ht, TweenInfo.new(1.5), {TextTransparency = 1}):Play()
        end)
    end)

    return {Fill = sFill, Action = aLabel}
end

local HUD = CreateHUD()

------------------------------------------------------------------------
-- CAMERA TILT
------------------------------------------------------------------------
local camTiltCurrent = 0
local camTiltTarget = 0

------------------------------------------------------------------------
-- WALL RUN
------------------------------------------------------------------------
local wrBV = nil
local wrConn = nil

local function StopWallRun()
    if not State.isWallRunning then return end
    State.isWallRunning = false
    State.wallCD = tick()

    if wrBV then pcall(function() wrBV:Destroy() end) wrBV = nil end
    if wrConn then wrConn:Disconnect() wrConn = nil end

    -- Wall jump boost
    if RootPart and RootPart.Parent then
        local bv = Instance.new("BodyVelocity")
        bv.MaxForce = Vector3.new(30000, 30000, 30000)
        bv.Velocity = RootPart.CFrame.LookVector * 16 + Vector3.new(0, 26, 0)
        bv.Parent = RootPart
        Debris:AddItem(bv, 0.15)
    end

    camTiltTarget = 0
    ResetAnimTargets()
end

local function StartWallRun(side, wallHit)
    if State.isWallRunning or State.isSliding or State.isVaulting then return end
    if State.stamina < 8 then return end
    if tick() - State.wallCD < CFG.WallRunCooldown then return end

    State.isWallRunning = true
    State.wallSide = side
    State.wallTime = 0

    local wallNormal = wallHit.Normal
    local wallFwd = wallNormal:Cross(Vector3.yAxis).Unit
    if RootPart.CFrame.LookVector:Dot(wallFwd) < 0 then wallFwd = -wallFwd end

    wrBV = Instance.new("BodyVelocity")
    wrBV.MaxForce = Vector3.new(50000, 50000, 50000)
    wrBV.P = 10000
    wrBV.Parent = RootPart

    camTiltTarget = side == "Right" and CFG.WallRunCamTilt or -CFG.WallRunCamTilt

    local start = tick()
    local phase = 0
    wrConn = RunService.Heartbeat:Connect(function(dt)
        if not State.isWallRunning then return end

        State.wallTime = tick() - start
        State.stamina = math.max(0, State.stamina - CFG.WallRunDrain * dt)

        local t = State.wallTime / CFG.WallRunDuration
        local up = CFG.WallRunSpeed * (1 - t * CFG.WallRunGravity * 2.5)
        local fwd = CFG.WallRunSpeed * (1 - t * 0.3)

        if wrBV and wrBV.Parent then
            wrBV.Velocity = wallFwd * fwd + Vector3.new(0, math.max(up, -12), 0)
        end

        -- Leg cycling animation
        phase = phase + dt * 4
        SetWallRunPose(side, phase)

        local wall = CheckWall(side)
        if State.wallTime >= CFG.WallRunDuration
            or State.stamina <= 0
            or not wall
            or (IsGrounded() and State.wallTime > 0.2)
            or not State.spaceHeld then
            StopWallRun()
        end
    end)
end

------------------------------------------------------------------------
-- SLIDE
------------------------------------------------------------------------
local slBV = nil
local slConn = nil
local origHipHeight = nil

local function StopSlide()
    if not State.isSliding then return end
    State.isSliding = false
    State.slideCD = tick()

    if slBV then pcall(function() slBV:Destroy() end) slBV = nil end
    if slConn then slConn:Disconnect() slConn = nil end

    if origHipHeight and Humanoid then
        Humanoid.HipHeight = origHipHeight
        origHipHeight = nil
    end

    camTiltTarget = 0
    ResetAnimTargets()
end

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 < CFG.SlideCost then return end
    if tick() - State.slideCD < CFG.SlideCooldown then return end

    local vel = RootPart.AssemblyLinearVelocity
    local hSpeed = Vector3.new(vel.X, 0, vel.Z).Magnitude
    if hSpeed < CFG.SlideMinSpeed then return end

    State.isSliding = true
    State.slideTime = 0
    State.slideDir = MoveDir().Unit
    State.stamina = math.max(0, State.stamina - CFG.SlideCost)

    origHipHeight = Humanoid.HipHeight
    Humanoid.HipHeight = origHipHeight * 0.45

    slBV = Instance.new("BodyVelocity")
    slBV.MaxForce = Vector3.new(40000, 0, 40000)
    slBV.P = 8000
    slBV.Velocity = State.slideDir * CFG.SlideSpeed
    slBV.Parent = RootPart

    SetSlidePose()
    camTiltTarget = CFG.SlideCamTilt

    local start = tick()
    slConn = RunService.Heartbeat:Connect(function(dt)
        if not State.isSliding then return end
        State.slideTime = tick() - start

        if slBV and slBV.Parent then
            slBV.Velocity = slBV.Velocity * CFG.SlideFriction
        end

        if State.slideTime >= CFG.SlideDuration or not State.shiftHeld then
            StopSlide()
        end
    end)
end

------------------------------------------------------------------------
-- VAULT
------------------------------------------------------------------------
local function StartVault()
    if State.isVaulting or State.isWallRunning or State.isSliding then return end
    if not IsMoving() then return end
    if State.stamina < CFG.VaultCost then return end
    if tick() - State.vaultCD < CFG.VaultCooldown then return end

    local _, topHit = CheckVault()
    if not topHit then return end

    State.isVaulting = true
    State.stamina = math.max(0, State.stamina - CFG.VaultCost)

    local startPos = RootPart.Position
    local topPos = topHit.Position + Vector3.new(0, 3.5, 0)
    local endPos = topPos + RootPart.CFrame.LookVector * 4

    local bg = Instance.new("BodyGyro")
    bg.MaxTorque = Vector3.new(1e5, 1e5, 1e5)
    bg.CFrame = RootPart.CFrame
    bg.Parent = RootPart

    local bv = Instance.new("BodyVelocity")
    bv.MaxForce = Vector3.new(1e5, 1e5, 1e5)
    bv.P = 15000
    bv.Parent = RootPart

    local elapsed = 0
    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 a = math.min(elapsed / CFG.VaultDuration, 1)

        SetVaultPose(a)

        -- Quadratic bezier
        local p0, p1, p2 = startPos, topPos + Vector3.new(0, 2, 0), endPos
        local pos = (1 - a)^2 * p0 + 2 * (1 - a) * a * p1 + a^2 * p2

        if bv and bv.Parent and RootPart and RootPart.Parent then
            bv.Velocity = (pos - RootPart.Position) * 30
        end

        if a >= 1 then
            State.isVaulting = false
            State.vaultCD = tick()
            pcall(function() bv:Destroy() end)
            pcall(function() bg:Destroy() end)
            ResetAnimTargets()
            conn:Disconnect()
        end
    end)
end

------------------------------------------------------------------------
-- INPUT
------------------------------------------------------------------------
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
            local vel = RootPart.AssemblyLinearVelocity
            local hSpeed = Vector3.new(vel.X, 0, vel.Z).Magnitude
            if hSpeed >= CFG.SlideMinSpeed then
                StartSlide()
            else
                State.sprinting = true
                if Humanoid then Humanoid.WalkSpeed = CFG.SprintSpeed end
            end
        else
            State.sprinting = true
            if Humanoid then Humanoid.WalkSpeed = CFG.SprintSpeed end
        end
    end

    if input.KeyCode == Enum.KeyCode.Space then
        State.spaceHeld = true

        if not IsGrounded() and not State.isWallRunning then
            local rWall = CheckWall("Right")
            local lWall = CheckWall("Left")
            if rWall and IsMoving() then
                StartWallRun("Right", rWall)
            elseif lWall and IsMoving() then
                StartWallRun("Left", lWall)
            end
        elseif IsGrounded() and IsMoving() then
            StartVault()
        end
    end
end)

UserInputService.InputEnded:Connect(function(input, gpe)
    if input.KeyCode == Enum.KeyCode.LeftShift then
        State.shiftHeld = false
        State.sprinting = false
        if not State.isSliding and not State.isWallRunning and Humanoid then
            Humanoid.WalkSpeed = CFG.WalkSpeed
        end
    end
    if input.KeyCode == Enum.KeyCode.Space then
        State.spaceHeld = false
    end
end)

------------------------------------------------------------------------
-- MAIN LOOP
------------------------------------------------------------------------
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
    if not State.isWallRunning and not State.isSliding and not State.isVaulting then
        State.stamina = math.min(CFG.MaxStamina, State.stamina + CFG.StaminaRegen * dt)
    end

    -- Speed guard
    if not State.isSliding and not State.isWallRunning and not State.isVaulting then
        Humanoid.WalkSpeed = State.sprinting and CFG.SprintSpeed or CFG.WalkSpeed
    end

    -- Motor6D animation blending
    UpdateMotorAnimation(dt)

    -- HUD update
    pcall(function()
        local pct = State.stamina / CFG.MaxStamina
        HUD.Fill.Size = UDim2.new(math.max(pct, 0), 0, 1, 0)
        if pct > 0.5 then
            HUD.Fill.BackgroundColor3 = Color3.fromRGB(50, 205, 50)
        elseif pct > 0.25 then
            HUD.Fill.BackgroundColor3 = Color3.fromRGB(255, 200, 40)
        else
            HUD.Fill.BackgroundColor3 = Color3.fromRGB(220, 50, 50)
        end

        local txt, col = "[ IDLE ]", Color3.fromRGB(150, 150, 150)
        if State.isWallRunning then
            txt = ">> WALL RUN [" .. (State.wallSide or "?") .. "] <<"
            col = Color3.fromRGB(85, 255, 255)
        elseif State.isSliding then
            txt = ">> SLIDE <<"
            col = Color3.fromRGB(255, 170, 50)
        elseif State.isVaulting then
            txt = ">> VAULT <<"
            col = Color3.fromRGB(170, 85, 255)
        elseif State.sprinting and IsMoving() then
            txt = ">> SPRINT <<"
            col = Color3.fromRGB(255, 255, 85)
        elseif IsMoving() then
            txt = "[ MOVING ]"
            col = Color3.fromRGB(200, 200, 200)
        end
        HUD.Action.Text = txt
        HUD.Action.TextColor3 = col
    end)

    -- Camera tilt
    camTiltCurrent = camTiltCurrent + (camTiltTarget - camTiltCurrent) * math.min(dt * 10, 1)
    Camera.CFrame = Camera.CFrame * CFrame.Angles(0, 0, math.rad(camTiltCurrent))
end)

------------------------------------------------------------------------
-- DEATH CLEANUP
------------------------------------------------------------------------
local function OnDeath()
    StopWallRun()
    StopSlide()
    State.isVaulting = false
    camTiltTarget = 0
    ResetAnimTargets()
end

if Humanoid then
    Humanoid.Died:Connect(OnDeath)
end
Player.CharacterAdded:Connect(function(char)
    task.wait(0.5)
    if Humanoid then
        Humanoid.Died:Connect(OnDeath)
    end
    HUD = CreateHUD()
end)

print("[PARKOUR v3] Loaded! SHIFT=Slide/Sprint | SPACE=WallRun/Vault")
INFO