--[[ Ultimate Wall Run + Climb System (with rich debug) ────────────────────────────────────────────── Controls • Hold W in mid‑air near a side wall → Wall‑Run • Press Space while wall‑running → Wall‑Jump boost • Hold E near a front wall → Climb straight up Debug Aids • Red ghost parts at every ray‑hit (auto‑delete 0.5 s) • System chat & console prints for all state changes and key events Config tunables live below for easy tweaking. ]] local Players = game:GetService("Players") local RunService = game:GetService("RunService") local UserInputService = game:GetService("UserInputService") local StarterGui = game:GetService("StarterGui") local player = Players.LocalPlayer local character, humanoid, rootPart, camera -- ── CONFIG ───────────────────────────────── local RUN_SPEED = 50 -- studs/sec along wall local CLIMB_SPEED = 28 -- studs/sec upward local GRAVITY_PULL = 20 -- downward pull during wall‑run local TILT_ANGLE = 10 -- max camera roll ° local TILT_SPEED = 8 -- lerp rate local DEBUG_LIFETIME = 0.5 -- seconds a debug part persists -- ─────────────────────────────────────────── -- State enum local State = {None = 0, WallRun = 1, Climb = 2} local currentState = State.None local wallNormal = Vector3.new() -- Helpers local bodyVelocity, bodyGyro local currentTilt = 0 local climbHeld = false local function chatMsg(text,color) local ok = pcall(StarterGui.SetCore, StarterGui, "ChatMakeSystemMessage",{Text=text,Color=color or Color3.new(1,1,1)}) if not ok then warn(text) end end local function makeDebugPart(hit,color) local p = Instance.new("Part") p.Anchored = true p.CanCollide = false p.Transparency = 0.5 p.Shape = Enum.PartType.Ball p.BrickColor = BrickColor.new(color or "Bright red") p.Size = Vector3.new(0.3,0.3,0.3) p.CFrame = CFrame.new(hit.Position) p.Parent = workspace task.delay(DEBUG_LIFETIME,function() if p.Parent then p:Destroy() end end) end -- Cleanup local function clearMovement() currentState = State.None if bodyVelocity then bodyVelocity:Destroy() bodyVelocity = nil end if bodyGyro then bodyGyro:Destroy() bodyGyro = nil end end -- Character load local function onCharacterAdded(char) character = char humanoid = char:WaitForChild("Humanoid") rootPart = char:WaitForChild("HumanoidRootPart") camera = workspace.CurrentCamera clearMovement() currentTilt = 0 climbHeld = false chatMsg("Character ready",Color3.new(0,1,0)) end player.CharacterAdded:Connect(onCharacterAdded) if player.Character then onCharacterAdded(player.Character) end -- Ray helpers local function sideWallNormal() if not rootPart then return nil end local params = RaycastParams.new() params.FilterDescendantsInstances = {character} params.FilterType = Enum.RaycastFilterType.Blacklist local origin = rootPart.Position for _,dir in ipairs({camera.CFrame.RightVector,-camera.CFrame.RightVector}) do local hit = workspace:Raycast(origin, dir*3, params) if hit and math.abs(hit.Normal.Y) < 0.2 then makeDebugPart(hit) return hit.Normal end end return nil end local function frontWallNormal() if not rootPart then return nil end local params = RaycastParams.new() params.FilterDescendantsInstances = {character} params.FilterType = Enum.RaycastFilterType.Blacklist local dir = camera.CFrame.LookVector local hit = workspace:Raycast(rootPart.Position, dir*3, params) if hit and math.abs(hit.Normal.Y) < 0.2 then makeDebugPart(hit,"Bright blue") return hit.Normal end return nil end -- Start behaviours local function beginWallRun(normal) clearMovement() currentState = State.WallRun wallNormal = normal bodyVelocity = Instance.new("BodyVelocity", rootPart) bodyVelocity.MaxForce = Vector3.new(1e5,1e5,1e5) bodyGyro = Instance.new("BodyGyro", rootPart) bodyGyro.MaxTorque = Vector3.new(1e5,1e5,1e5) chatMsg("Wall‑Run START",Color3.new(1,0.6,0)) end local function beginClimb(normal) clearMovement() currentState = State.Climb wallNormal = normal bodyVelocity = Instance.new("BodyVelocity", rootPart) bodyVelocity.MaxForce = Vector3.new(1e5,1e5,1e5) bodyGyro = Instance.new("BodyGyro", rootPart) bodyGyro.MaxTorque = Vector3.new(1e5,1e5,1e5) chatMsg("Climb START",Color3.new(0,1,1)) end -- Input UserInputService.InputBegan:Connect(function(inp,proc) if proc then return end if inp.KeyCode == Enum.KeyCode.Space and currentState == State.WallRun then local boostDir = (‑wallNormal + Vector3.new(0,1,0)).Unit rootPart.Velocity = boostDir*60 chatMsg("Wall‑Jump!",Color3.new(0.5,0.5,1)) clearMovement() elseif inp.KeyCode == Enum.KeyCode.E then climbHeld = true chatMsg("E held — climb attempt",Color3.new(1,1,0)) end end) UserInputService.InputEnded:Connect(function(inp,proc) if proc then return end if inp.KeyCode == Enum.KeyCode.E then climbHeld = false if currentState == State.Climb then chatMsg("Climb RELEASED",Color3.new(1,0.5,0)) clearMovement() end end end) -- Main loop RunService.RenderStepped:Connect(function(dt) if not (humanoid and rootPart and camera) then return end local forwardHeld = UserInputService:IsKeyDown(Enum.KeyCode.W) local onGround = humanoid.FloorMaterial ~= Enum.Material.Air -- Initiate states if currentState == State.None then if climbHeld then local n = frontWallNormal() if n then beginClimb(n) end elseif forwardHeld and not onGround then local n = sideWallNormal() if n then beginWallRun(n) end end end -- Maintain or cancel states if currentState == State.WallRun then if onGround or not forwardHeld then chatMsg("Wall‑Run END (ground/forward)",Color3.new(1,0,0)) clearMovement() else local n = sideWallNormal() if not n then chatMsg("Wall‑Run END (lost wall)",Color3.new(1,0,0)); clearMovement() else wallNormal = n end end elseif currentState == State.Climb then if onGround or not climbHeld then chatMsg("Climb END (ground/release)",Color3.new(1,0,0)) clearMovement() else local n = frontWallNormal() if not n then chatMsg("Climb END (lost wall)",Color3.new(1,0,0)); clearMovement() else wallNormal = n end end end -- Apply kinematics if currentState == State.WallRun then local runDir = wallNormal:Cross(Vector3.new(0,1,0)).Unit if runDir:Dot(camera.CFrame.LookVector) < 0 then runDir = -runDir end bodyVelocity.Velocity = runDir*RUN_SPEED + Vector3.new(0,-GRAVITY_PULL,0) bodyGyro.CFrame = CFrame.new(rootPart.Position, rootPart.Position + runDir) elseif currentState == State.Climb then bodyVelocity.Velocity = Vector3.new(0,CLIMB_SPEED,0) - wallNormal*10 bodyGyro.CFrame = CFrame.new(rootPart.Position, rootPart.Position - wallNormal) end -- Camera tilt local targetTilt = 0 if currentState == State.WallRun then targetTilt = math.rad(TILT_ANGLE * (wallNormal:Dot(camera.CFrame.RightVector) > 0 and 1 or -1)) elseif currentState == State.Climb then targetTilt = math.rad(TILT_ANGLE*0.5 * (wallNormal:Dot(camera.CFrame.RightVector) > 0 and 1 or -1)) end currentTilt += (targetTilt - currentTilt) * math.clamp(TILT_SPEED*dt,0,1) camera.CFrame = camera.CFrame * CFrame.Angles(0,0,currentTilt) end)