-- Wall Walk Controller MEJORADO V4 - LocalScript -- Colocar en: StarterPlayer > StarterPlayerScripts -- Tecla "-" activa/desactiva caminar por paredes. -- V4 agrega: -- 1) Auto-enganche al caminar hacia una pared. -- 2) W sube por la pared cuando estas pegado. -- 3) Estados internos: GROUND / APPROACHING_WALL / WALL_ATTACHED / CEILING_ATTACHED. -- 4) Menos pelea con Humanoid: usa Humanoid:Move() + VectorForce suave. -- 5) Limpieza completa al desactivar y soporte para respawn. local Players = game:GetService("Players") local UserInputService = game:GetService("UserInputService") local RunService = game:GetService("RunService") local CollectionService = game:GetService("CollectionService") local player = Players.LocalPlayer local camera = workspace.CurrentCamera --// CONFIGURACION local WALL_WALK_ENABLED = false local TOGGLE_KEY = Enum.KeyCode.Minus local ONLY_TAGGED_SURFACES = false local SURFACE_TAG = "WallWalk" local RAY_DISTANCE = 7.5 local FRONT_ATTACH_DISTANCE = 5.25 local FLOOR_CHECK_DISTANCE = 5.5 local SURFACE_OFFSET = 2.7 local MOVE_SPEED = 18 local WALL_UP_SPEED = 18 local MOVE_ACCELERATION = 34 local NO_INPUT_DAMPING = 14 local MAX_MOVE_FORCE = 95000 local STICK_ACCELERATION = 50 local SOFT_STICK_SPEED = 2.4 local JUMP_POWER = 58 local ALIGN_RESPONSIVENESS = 48 local NORMAL_LERP_SPEED = 18 local FAST_ATTACH_LERP_SPEED = 32 local CAMERA_DISTANCE = 12 local CAMERA_HEIGHT = 4 local MOUSE_SENSITIVITY = 0.25 local CAMERA_SMOOTHNESS = 0.2 local AUTO_ATTACH_WHEN_WALKING = true local DISABLE_WHEN_SURFACE_LOST_AFTER = 0.45 --// ESTADO INTERNO local yaw = 0 local pitch = 0 local character local humanoid local root local savedWalkSpeed local savedJumpPower local savedJumpHeight local savedUseJumpPower local attachment local alignOrientation local vectorForce local currentNormal = Vector3.yAxis local lastGoodNormal = Vector3.yAxis local lastSurfaceResult = nil local lostSurfaceTime = 0 local wallState = "GROUND" local function safeUnit(v, fallback) if typeof(v) == "Vector3" and v.Magnitude > 0.001 then return v.Unit end return fallback or Vector3.yAxis end local function clampMagnitude(v, maxMagnitude) if v.Magnitude > maxMagnitude then return v.Unit * maxMagnitude end return v end local function getCharacter() character = player.Character or player.CharacterAdded:Wait() humanoid = character:WaitForChild("Humanoid") root = character:WaitForChild("HumanoidRootPart") return character, humanoid, root end local function getRaycastParams() local params = RaycastParams.new() params.FilterType = Enum.RaycastFilterType.Exclude params.FilterDescendantsInstances = character and {character} or {} params.IgnoreWater = true return params end local function validSurface(result) if not result then return false end if not result.Instance then return false end if not result.Instance.CanCollide then return false end if ONLY_TAGGED_SURFACES then return CollectionService:HasTag(result.Instance, SURFACE_TAG) end return true end local function raycast(origin, direction, distance) local result = workspace:Raycast(origin, safeUnit(direction, -currentNormal) * distance, getRaycastParams()) if validSurface(result) then return result end return nil end local function cleanupForces() if alignOrientation then alignOrientation:Destroy() alignOrientation = nil end if vectorForce then vectorForce:Destroy() vectorForce = nil end if attachment then attachment:Destroy() attachment = nil end end local function createForces() if not root then return end cleanupForces() attachment = Instance.new("Attachment") attachment.Name = "WallWalkAttachment" attachment.Parent = root alignOrientation = Instance.new("AlignOrientation") alignOrientation.Name = "WallWalkAlignOrientation" alignOrientation.Mode = Enum.OrientationAlignmentMode.OneAttachment alignOrientation.Attachment0 = attachment alignOrientation.Responsiveness = ALIGN_RESPONSIVENESS alignOrientation.MaxTorque = math.huge alignOrientation.RigidityEnabled = false alignOrientation.Parent = root vectorForce = Instance.new("VectorForce") vectorForce.Name = "WallWalkMovementForce" vectorForce.Attachment0 = attachment vectorForce.RelativeTo = Enum.ActuatorRelativeTo.World vectorForce.ApplyAtCenterOfMass = true vectorForce.Force = Vector3.zero vectorForce.Parent = root end local function getInputMoveVector() local x = 0 local z = 0 if UserInputService:IsKeyDown(Enum.KeyCode.W) then z += 1 end if UserInputService:IsKeyDown(Enum.KeyCode.S) then z -= 1 end if UserInputService:IsKeyDown(Enum.KeyCode.D) then x += 1 end if UserInputService:IsKeyDown(Enum.KeyCode.A) then x -= 1 end local v = Vector3.new(x, 0, z) if v.Magnitude > 1 then v = v.Unit end return v end local function getPlanarCameraForward(upVector) local forward = camera.CFrame.LookVector forward = forward - upVector * forward:Dot(upVector) if forward.Magnitude < 0.001 and root then forward = root.CFrame.LookVector - upVector * root.CFrame.LookVector:Dot(upVector) end return safeUnit(forward, root and root.CFrame.LookVector or Vector3.zAxis) end local function getCameraRelativeMovement(upVector) local input = getInputMoveVector() if input.Magnitude <= 0 then return Vector3.zero end local forward = getPlanarCameraForward(upVector) local right = safeUnit(forward:Cross(upVector), root and root.CFrame.RightVector or Vector3.xAxis) local moveDirection = forward * input.Z + right * input.X moveDirection = moveDirection - upVector * moveDirection:Dot(upVector) return safeUnit(moveDirection, Vector3.zero) end local function findFrontWall(moveDirection) if not root or not camera then return nil end if not AUTO_ATTACH_WHEN_WALKING then return nil end local input = getInputMoveVector() if input.Z <= 0 then return nil end -- solo engancha automaticamente cuando caminas hacia adelante local origin = root.Position local forward = moveDirection if forward.Magnitude < 0.001 then forward = getPlanarCameraForward(currentNormal) end local cf = root.CFrame local origins = { origin, origin + currentNormal * 1.1, origin - currentNormal * 0.5, origin + cf.RightVector * 1.1, origin - cf.RightVector * 1.1, } local best local bestDistance = math.huge for _, testOrigin in ipairs(origins) do local result = raycast(testOrigin, forward, FRONT_ATTACH_DISTANCE) if result then local isWallLike = math.abs(result.Normal:Dot(Vector3.yAxis)) < 0.75 if isWallLike and result.Distance < bestDistance then best = result bestDistance = result.Distance end end end return best end local function findCurrentSurface(moveDirection) if not root then return nil end local origin = root.Position local cf = root.CFrame local directions = { -currentNormal, -lastGoodNormal, -cf.UpVector, Vector3.new(0, -1, 0), Vector3.new(0, 1, 0), cf.LookVector, -cf.LookVector, cf.RightVector, -cf.RightVector, } if moveDirection and moveDirection.Magnitude > 0.001 then table.insert(directions, moveDirection) table.insert(directions, -moveDirection) end local origins = { origin, origin + cf.LookVector * 1.2, origin - cf.LookVector * 1.2, origin + cf.RightVector * 1.2, origin - cf.RightVector * 1.2, origin + currentNormal * 0.75, origin - currentNormal * 0.75, } local bestResult local bestScore = -math.huge for _, testOrigin in ipairs(origins) do for _, direction in ipairs(directions) do local result = raycast(testOrigin, direction, RAY_DISTANCE) if result then local continuity = result.Normal:Dot(currentNormal) * 4 local distanceScore = -result.Distance * 0.65 local movementScore = 0 if moveDirection and moveDirection.Magnitude > 0.001 then movementScore = math.abs(result.Normal:Dot(moveDirection)) * 0.5 end local score = continuity + distanceScore + movementScore if score > bestScore then bestScore = score bestResult = result end end end end return bestResult end local function classifyState(normal) local y = normal:Dot(Vector3.yAxis) if y > 0.65 then return "GROUND" elseif y < -0.65 then return "CEILING_ATTACHED" else return "WALL_ATTACHED" end end local function getWallMovement(surfaceNormal) local input = getInputMoveVector() if input.Magnitude <= 0 then return Vector3.zero end local upVector = surfaceNormal.Unit local state = classifyState(upVector) if state == "WALL_ATTACHED" then -- En pared: W sube, S baja, A/D se mueve lateral. local worldUpOnWall = Vector3.yAxis - upVector * Vector3.yAxis:Dot(upVector) worldUpOnWall = safeUnit(worldUpOnWall, getPlanarCameraForward(upVector)) local side = safeUnit(worldUpOnWall:Cross(upVector), root and root.CFrame.RightVector or Vector3.xAxis) local move = worldUpOnWall * input.Z + side * input.X return safeUnit(move, Vector3.zero) end -- En piso/techo: movimiento relativo a camara. return getCameraRelativeMovement(upVector) end local function getSurfaceBasis(upVector, moveDirection) local forward if moveDirection and moveDirection.Magnitude > 0.001 then forward = moveDirection - upVector * moveDirection:Dot(upVector) else forward = camera.CFrame.LookVector - upVector * camera.CFrame.LookVector:Dot(upVector) end if forward.Magnitude < 0.001 and root then forward = root.CFrame.LookVector - upVector * root.CFrame.LookVector:Dot(upVector) end forward = safeUnit(forward, root and root.CFrame.LookVector or Vector3.zAxis) local right = safeUnit(forward:Cross(upVector), root and root.CFrame.RightVector or Vector3.xAxis) forward = safeUnit(upVector:Cross(right), root and root.CFrame.LookVector or Vector3.zAxis) return right, upVector, -forward end local function restoreHumanoid() if not humanoid then return end humanoid.AutoRotate = true humanoid.PlatformStand = false if savedWalkSpeed then humanoid.WalkSpeed = savedWalkSpeed end if savedUseJumpPower ~= nil then humanoid.UseJumpPower = savedUseJumpPower end if savedJumpPower then humanoid.JumpPower = savedJumpPower end if savedJumpHeight then humanoid.JumpHeight = savedJumpHeight end pcall(function() humanoid:ChangeState(Enum.HumanoidStateType.Running) end) end local function disableInternally(reason) WALL_WALK_ENABLED = false restoreHumanoid() cleanupForces() camera = workspace.CurrentCamera if camera then camera.CameraType = Enum.CameraType.Custom end UserInputService.MouseBehavior = Enum.MouseBehavior.Default UserInputService.MouseIconEnabled = true currentNormal = Vector3.yAxis lastGoodNormal = Vector3.yAxis lastSurfaceResult = nil lostSurfaceTime = 0 wallState = "GROUND" print(reason and ("[WALL WALK] Desactivado: " .. reason) or "[WALL WALK] Desactivado") end local function applyWallWalk(dt) if not WALL_WALK_ENABLED then return end if not character or not humanoid or not root or not root.Parent then getCharacter() createForces() end camera = workspace.CurrentCamera if not camera then return end local preliminaryMove = getCameraRelativeMovement(currentNormal) local frontWall = findFrontWall(preliminaryMove) local result = frontWall or findCurrentSurface(preliminaryMove) if result then lastSurfaceResult = result lostSurfaceTime = 0 local targetNormal = result.Normal.Unit local speed = frontWall and FAST_ATTACH_LERP_SPEED or NORMAL_LERP_SPEED local maxAlpha = frontWall and 0.85 or 0.45 local alpha = math.clamp(dt * speed, 0, maxAlpha) currentNormal = safeUnit(currentNormal:Lerp(targetNormal, alpha), targetNormal) lastGoodNormal = currentNormal wallState = frontWall and "APPROACHING_WALL" or classifyState(currentNormal) else lostSurfaceTime += dt if lostSurfaceTime < DISABLE_WHEN_SURFACE_LOST_AFTER then currentNormal = lastGoodNormal result = lastSurfaceResult else disableInternally("no encontro superficie") return end end local upVector = currentNormal.Unit local moveDirection = getWallMovement(upVector) if wallState == "APPROACHING_WALL" then wallState = classifyState(upVector) end humanoid.AutoRotate = false humanoid.PlatformStand = false humanoid.WalkSpeed = MOVE_SPEED humanoid:Move(moveDirection, false) local right, up, back = getSurfaceBasis(upVector, moveDirection) if alignOrientation then alignOrientation.CFrame = CFrame.fromMatrix(root.Position, right, up, back) end local mass = root.AssemblyMass local velocity = root.AssemblyLinearVelocity local normalVelocity = upVector * velocity:Dot(upVector) local tangentialVelocity = velocity - normalVelocity local targetSpeed = wallState == "WALL_ATTACHED" and WALL_UP_SPEED or MOVE_SPEED local desiredTangentialVelocity = moveDirection * targetSpeed local velocityError if moveDirection.Magnitude > 0.001 then velocityError = desiredTangentialVelocity - tangentialVelocity else velocityError = -tangentialVelocity end local accel = moveDirection.Magnitude > 0.001 and MOVE_ACCELERATION or NO_INPUT_DAMPING local moveForce = clampMagnitude(velocityError * mass * accel, MAX_MOVE_FORCE) local antiGravity = Vector3.new(0, workspace.Gravity * mass, 0) local stickForce = -upVector * STICK_ACCELERATION * mass local towardSurfaceSpeed = -velocity:Dot(upVector) if towardSurfaceSpeed > SOFT_STICK_SPEED then stickForce *= 0.35 end if vectorForce then vectorForce.Force = antiGravity + stickForce + moveForce end if result then local minDistance = SURFACE_OFFSET * 0.52 local maxDistance = SURFACE_OFFSET * 1.85 if result.Distance < minDistance then root.CFrame = root.CFrame + upVector * ((minDistance - result.Distance) * 0.14) elseif result.Distance > maxDistance then root.CFrame = root.CFrame - upVector * ((result.Distance - maxDistance) * 0.16) end end end local function updateCamera(dt) if not WALL_WALK_ENABLED then return end if not root or not camera then return end local upVector = currentNormal.Unit local rotacion = CFrame.Angles(0, math.rad(yaw), 0) * CFrame.Angles(math.rad(pitch), 0, 0) local offset = rotacion:VectorToWorldSpace(Vector3.new(0, CAMERA_HEIGHT, CAMERA_DISTANCE)) local targetPosition = root.Position + offset local targetLookAt = root.Position + upVector * 2 local targetCFrame = CFrame.new(targetPosition, targetLookAt) if camera.CameraType ~= Enum.CameraType.Scriptable then camera.CameraType = Enum.CameraType.Scriptable end camera.CFrame = camera.CFrame:Lerp(targetCFrame, math.clamp(CAMERA_SMOOTHNESS * 60 * dt, 0, 1)) end local function activateWallWalk() if WALL_WALK_ENABLED then return end getCharacter() savedWalkSpeed = humanoid.WalkSpeed savedJumpPower = humanoid.JumpPower savedJumpHeight = humanoid.JumpHeight savedUseJumpPower = humanoid.UseJumpPower createForces() WALL_WALK_ENABLED = true currentNormal = Vector3.yAxis lastGoodNormal = Vector3.yAxis lastSurfaceResult = nil lostSurfaceTime = 0 wallState = "GROUND" humanoid.PlatformStand = false humanoid.AutoRotate = false humanoid.WalkSpeed = MOVE_SPEED camera = workspace.CurrentCamera if camera then camera.CameraType = Enum.CameraType.Scriptable end UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter UserInputService.MouseIconEnabled = false print("[WALL WALK] Activado") end local function deactivateWallWalk() if not WALL_WALK_ENABLED then return end disableInternally() end local function toggleWallWalk() if WALL_WALK_ENABLED then deactivateWallWalk() else activateWallWalk() end end UserInputService.InputBegan:Connect(function(input, processed) if processed then return end if input.KeyCode == TOGGLE_KEY then toggleWallWalk() return end if WALL_WALK_ENABLED and input.KeyCode == Enum.KeyCode.Space and root then local upVector = currentNormal.Unit local velocity = root.AssemblyLinearVelocity local tangentialVelocity = velocity - upVector * velocity:Dot(upVector) root.AssemblyLinearVelocity = tangentialVelocity + upVector * JUMP_POWER if vectorForce then vectorForce.Force = Vector3.zero end end end) UserInputService.InputChanged:Connect(function(input) if not WALL_WALK_ENABLED then return end if input.UserInputType == Enum.UserInputType.MouseMovement then yaw -= input.Delta.X * MOUSE_SENSITIVITY pitch -= input.Delta.Y * MOUSE_SENSITIVITY pitch = math.clamp(pitch, -75, 75) end end) RunService.RenderStepped:Connect(function(dt) if WALL_WALK_ENABLED then applyWallWalk(dt) updateCamera(dt) end end) player.CharacterAdded:Connect(function() task.wait(1) character = nil humanoid = nil root = nil cleanupForces() if WALL_WALK_ENABLED then getCharacter() createForces() end end) print("[WALL WALK] Script V4 cargado. Presiona '-' para activar/desactivar.")