Something is wrong with my source engine movement

Help, I am sliding, something is wrong with my source engine movement.

Local Script:

-- SourceMovementController LocalScript
-- Handles player input and applies SourceMovement physics, with bhop fix, surfing, and crouch sliding

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local SourceMovement = require(game.ReplicatedStorage:WaitForChild("SourceMovement"))

local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local rootPart = character:WaitForChild("HumanoidRootPart")

local state = {
	vel = Vector3.new(),
	onGround = true,
	groundNormal = Vector3.new(0,1,0),
	inWater = false,
	onLadder = false,
	crouching = false,
	jumpQueued = false,
	jumpHeld = false,
	canJump = true,
	surf = false,
	crouchSliding = false,
	crouchSlideTimer = 0,
	fallVel = 0,
	landed = false,
	lastLandTime = 0,
	prevCrouch = false,
	prevMoveDir = Vector3.new(), -- Added for counter-strafe
	-- For counter-strafe tap logic:
	axisHoldStart = {X = 0, Z = 0}, -- When did we start holding a direction on each axis
	axisLastDir = {X = 0, Z = 0},   -- Last nonzero direction on each axis
	axisTapTime = {X = 0, Z = 0},   -- How long the opposite was held before release
	wasOnGround = true, -- Track previous onGround state for bunnyhop/counter-strafe logic
	-- Bunnyhop grace period
	bhopGraceTimer = 0,
	bhopShouldPreserve = false,
}

local input = {
	moveDir = Vector3.new(),
	jump = false,
	crouch = false,
}

-- Input handling
local moveVector = Vector3.new()
local jumpPressed = false
local jumpPressedLast = false -- Track previous jump key state
local jumpConsumed = false    -- Prevents holding space for auto-hop
local crouchPressed = false

UserInputService.InputBegan:Connect(function(inputObj, processed)
	if processed then return end
	if inputObj.KeyCode == Enum.KeyCode.Space then
		jumpPressed = true
	elseif inputObj.KeyCode == Enum.KeyCode.LeftControl or inputObj.KeyCode == Enum.KeyCode.C then
		crouchPressed = true
	end
end)

UserInputService.InputEnded:Connect(function(inputObj, processed)
	if inputObj.KeyCode == Enum.KeyCode.Space then
		jumpPressed = false
		jumpConsumed = false -- Allow jump again after key is released
	elseif inputObj.KeyCode == Enum.KeyCode.LeftControl or inputObj.KeyCode == Enum.KeyCode.C then
		crouchPressed = false
	end
end)

local function getMoveDirection()
	local dir = Vector3.new()
	if UserInputService:IsKeyDown(Enum.KeyCode.W) then dir = dir + Vector3.new(0,0,-1) end
	if UserInputService:IsKeyDown(Enum.KeyCode.S) then dir = dir + Vector3.new(0,0,1) end
	if UserInputService:IsKeyDown(Enum.KeyCode.A) then dir = dir + Vector3.new(-1,0,0) end
	if UserInputService:IsKeyDown(Enum.KeyCode.D) then dir = dir + Vector3.new(1,0,0) end
	-- Camera relative
	local cam = workspace.CurrentCamera
	if cam then
		local cf = cam.CFrame
		local move = (cf.LookVector * -dir.Z + cf.RightVector * dir.X)
		move = Vector3.new(move.X, 0, move.Z)
		if move.Magnitude > 0 then
			return move.Unit
		end
	end
	return Vector3.new()
end

-- Main movement loop
local lastGrounded = true
local lastY = rootPart.Position.Y

-- For velocity smoothing
local prevAppliedVelocity = Vector3.new()
local velocityLerpAlpha = 1 -- Smoothing factor (1 = instant, 0.5 = fast, 0.25 = slow)

-- Counter-strafe settings
local COUNTERSTRAFE_TAP_WINDOW = 0.18 -- seconds, max time for a tap to count as counter-strafe

RunService.RenderStepped:Connect(function(dt)
	-- Update input
	input.moveDir = getMoveDirection()
	input.crouch = crouchPressed

	-- Jump logic: Only allow jump on the frame the key is pressed (no hold-to-bhop)
	if jumpPressed and not jumpPressedLast and not jumpConsumed then
		input.jump = true
		jumpConsumed = true -- Block further jumps until key is released
	else
		input.jump = false
	end
	jumpPressedLast = jumpPressed

	-- Store previous move direction for counter-strafe
	state.prevMoveDir = state.moveDir or Vector3.new()
	state.moveDir = input.moveDir

	-- Counter-strafe tap logic: Track axis hold times and tap durations
	local now = tick()
	for _, axis in {"X", "Z"} do
		local prev = state.axisLastDir[axis] or 0
		local curr = input.moveDir[axis]
		-- If we just started holding a direction (from 0 to nonzero)
		if math.abs(prev) < 0.01 and math.abs(curr) > 0.5 then
			state.axisHoldStart[axis] = now
		end
		-- If we just released a direction (from nonzero to 0)
		if math.abs(prev) > 0.5 and math.abs(curr) < 0.01 then
			state.axisTapTime[axis] = now - (state.axisHoldStart[axis] or now)
		end
		state.axisLastDir[axis] = curr
	end

	-- Check ground
	local ray = Ray.new(rootPart.Position, Vector3.new(0, -3, 0))
	local hit, pos, norm = workspace:FindPartOnRay(ray, character)
	state.onGround = hit ~= nil and norm.Y > 0.7
	state.groundNormal = norm or Vector3.new(0,1,0)

	-- Water/Ladder detection (simple placeholder)
	state.inWater = false
	state.onLadder = false

	-- Fall velocity
	state.fallVel = rootPart.Velocity.Y

	-- Landing detection
	state.landed = (not lastGrounded) and state.onGround
	lastGrounded = state.onGround

	-- Track previous onGround state for bunnyhop/counter-strafe logic
	state.wasOnGround = state.wasOnGround or true
	state.wasOnGround = state.onGround

	-- Crouch slide logic
	state.prevCrouch = state.crouching
	state.crouching = input.crouch
	SourceMovement:TryStartCrouchSlide(state, input)

	-- Apply movement
	state.vel = SourceMovement:Move(state, input, dt)

	-- --- COUNTER-STRAFE INSTANT STOP LOGIC ---
	-- If the player quickly switches direction (counter-strafe) on X or Z axis, and is on ground,
	-- forcibly zero velocity on that axis for one frame to prevent sliding.
	if state.onGround then
		for _, axis in {"X", "Z"} do
			local prev = state.prevMoveDir[axis]
			local curr = input.moveDir[axis]
			-- If the player just switched from moving in one direction to the opposite (counter-strafe)
			if math.abs(prev) > 0.5 and math.abs(curr) > 0.5 and (prev * curr) < 0 then
				-- Only trigger if the tap was quick enough (within COUNTERSTRAFE_TAP_WINDOW)
				if state.axisTapTime[axis] > 0 and state.axisTapTime[axis] <= COUNTERSTRAFE_TAP_WINDOW then
					-- Zero velocity on this axis
					state.vel = Vector3.new(
						axis == "X" and 0 or state.vel.X,
						state.vel.Y,
						axis == "Z" and 0 or state.vel.Z
					)
					-- Also forcibly zero rootPart velocity on this axis for this frame
					rootPart.Velocity = Vector3.new(
						axis == "X" and 0 or rootPart.Velocity.X,
						rootPart.Velocity.Y,
						axis == "Z" and 0 or rootPart.Velocity.Z
					)
					-- Reset tap time so it doesn't trigger again until next tap
					state.axisTapTime[axis] = 0
				end
			end
		end
	end

	-- --- INSTANT STOP OVERRIDE ---
	-- If there is no input and the player is on the ground, forcibly zero velocity to prevent sliding.
	if input.moveDir.Magnitude < 0.01 and state.onGround then
		state.vel = Vector3.new(0, 0, 0)
		rootPart.Velocity = Vector3.new(0, rootPart.Velocity.Y, 0)
		prevAppliedVelocity = rootPart.Velocity
	else
		-- Instantly set velocity for snappy control (no smoothing)
		local targetVel
		if state.surf then
			targetVel = Vector3.new(state.vel.X, 0, state.vel.Z)
		else
			targetVel = Vector3.new(state.vel.X, rootPart.Velocity.Y, state.vel.Z)
			if input.jump and state.onGround and state.canJump then
				targetVel = Vector3.new(state.vel.X, state.vel.Y, state.vel.Z)
			end
		end
		-- Lerp previous velocity to target velocity for smoothness (set alpha=1 for instant)
		prevAppliedVelocity = prevAppliedVelocity:Lerp(targetVel, velocityLerpAlpha)
		rootPart.Velocity = prevAppliedVelocity
	end

	-- Crouch
	if input.crouch then
		humanoid.CameraOffset = Vector3.new(0, -2, 0)
	else
		humanoid.CameraOffset = Vector3.new(0, 0, 0)
	end

	-- Clamp speed for sanity
	local flatVel = Vector3.new(rootPart.Velocity.X, 0, rootPart.Velocity.Z)
	if flatVel.Magnitude > 60 then
		local y = rootPart.Velocity.Y
		rootPart.Velocity = flatVel.Unit * 60 + Vector3.new(0, y, 0)
	end
end)

Module:

-- SourceMovement ModuleScript
-- Implements Source-engine-like movement physics for Roblox characters, with bhop, surfing, and crouch sliding

local SourceMovement = {}

-- Tweakable parameters
local WALK_SPEED = 16
local CROUCH_SPEED = 8
local JUMP_POWER = 50
local CROUCH_JUMP_BOOST = 1.2
local AIR_ACCEL = 34 -- Increased for more bhop speed gain
local GROUND_ACCEL = 64
local FRICTION = 8
local AIR_FRICTION = 0.5
local SLOPE_SLIDE_ANGLE = 40
local SLOPE_SLOW_ANGLE = 20
local SURF_MIN_ANGLE = 10
local SURF_MAX_ANGLE = 80
local SURF_FRICTION = 0.01
local SURF_AIR_ACCEL = 60
local SURF_MAX_SPEED = 60
local LANDING_PENALTY_VEL = 60
local LANDING_PENALTY_TIME = 0.4
local WATER_SPEED = 8
local LADDER_SPEED = 10
local MAX_AIR_SPEED = 32 -- Increased for more bhop speed gain
local CROUCH_SLIDE_SPEED = 30
local CROUCH_SLIDE_TIME = 0.5
local CROUCH_SLIDE_FRICTION = 1

local COUNTERSTRAFE_TAP_THRESHOLD = 0.15 -- seconds, must tap opposite key within this time to trigger instant stop

-- Bunnyhop grace period settings
local BHOP_GRACE_PERIOD = 0.12 -- seconds to jump after landing to keep velocity
local BHOP_MAX_MULTIPLIER = 1.25 -- max velocity multiplier allowed from bunnyhop

local function getSlopeAngle(normal)
	if normal.Y == 0 then return 90 end
	return math.deg(math.acos(normal.Y))
end

function SourceMovement:IsSurfing(groundNormal, onGround, onLadder, inWater)
	if not onGround or onLadder or inWater then return false end
	local angle = getSlopeAngle(groundNormal)
	return angle > SURF_MIN_ANGLE and angle < SURF_MAX_ANGLE
end

function SourceMovement:ApplyFriction(vel, dt, onGround, frictionOverride)
	local friction = frictionOverride or (onGround and FRICTION or AIR_FRICTION)
	local speed = vel.Magnitude
	if speed ~= 0 then
		local drop = speed * friction * dt
		local newSpeed = math.max(speed - drop, 0)
		return vel.Unit * newSpeed
	end
	return vel
end

function SourceMovement:Accelerate(vel, wishDir, wishSpeed, accel, dt, maxSpeed)
	local currentSpeed = vel:Dot(wishDir)
	local addSpeed = wishSpeed - currentSpeed
	if addSpeed <= 0 then return vel end
	local accelSpeed = accel * dt * wishSpeed
	if accelSpeed > addSpeed then accelSpeed = addSpeed end
	local newVel = vel + wishDir * accelSpeed
	if maxSpeed and newVel.Magnitude > maxSpeed then
		newVel = newVel.Unit * maxSpeed
	end
	return newVel
end

function SourceMovement:HandleSlope(vel, groundNormal)
	local angle = getSlopeAngle(groundNormal)
	if angle > SLOPE_SLIDE_ANGLE then
		-- Slide down steep slopes
		local slideDir = Vector3.new(groundNormal.X, 0, groundNormal.Z)
		if slideDir.Magnitude > 0 then
			slideDir = slideDir.Unit
			return vel + slideDir * (angle - SLOPE_SLIDE_ANGLE)
		end
	elseif angle > SLOPE_SLOW_ANGLE then
		-- Slow uphill
		return vel * 0.7
	end
	return vel
end

function SourceMovement:HandleLandingPenalty(vel, fallVel, landed, lastLandTime, now)
	if landed and fallVel < -LANDING_PENALTY_VEL and (now - lastLandTime) > 0.1 then
		return vel * 0.5, now
	end
	return vel, lastLandTime
end

-- Counter-strafe helper
local function counterStrafeVelocity(vel, moveDir, prevMoveDir, axisTapTime, canCounterStrafe)
	local newVel = vel

	if not canCounterStrafe then
		return newVel
	end

	if prevMoveDir and moveDir then
		-- Counter-strafe left/right (A/D)
		if prevMoveDir.X > 0.5 and moveDir.X < -0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
			newVel = Vector3.new(0, newVel.Y, newVel.Z)
		elseif prevMoveDir.X < -0.5 and moveDir.X > 0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
			newVel = Vector3.new(0, newVel.Y, newVel.Z)
		end

		-- Counter-strafe forward/back (W/S)
		if prevMoveDir.Z > 0.5 and moveDir.Z < -0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
			newVel = Vector3.new(newVel.X, newVel.Y, 0)
		elseif prevMoveDir.Z < -0.5 and moveDir.Z > 0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
			newVel = Vector3.new(newVel.X, newVel.Y, 0)
		end

		-- NEW: If you release the opposite key after counter-strafe (i.e. go from input to zero), stop instantly
		-- Only if the opposite key was tapped (held for less than threshold)
		-- X axis
		if math.abs(prevMoveDir.X) > 0.5 and math.abs(moveDir.X) <= 0.01 then
			if axisTapTime and axisTapTime.X and axisTapTime.X > 0 and axisTapTime.X <= COUNTERSTRAFE_TAP_THRESHOLD then
				newVel = Vector3.new(0, newVel.Y, newVel.Z)
			end
		end

		-- Z axis
		if math.abs(prevMoveDir.Z) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
			if axisTapTime and axisTapTime.Z and axisTapTime.Z > 0 and axisTapTime.Z <= COUNTERSTRAFE_TAP_THRESHOLD then
				newVel = Vector3.new(newVel.X, newVel.Y, 0)
			end
		end
	end

	return newVel
end

function SourceMovement:Move(state, input, dt)
	-- state: {vel, onGround, groundNormal, inWater, onLadder, crouching, jumpQueued, jumpHeld, canJump, surf, crouchSliding, crouchSlideTimer, fallVel, landed, lastLandTime, prevMoveDir, axisTapTime, wasOnGround, bhopGraceTimer, bhopShouldPreserve}
	-- input: {moveDir, jump, crouch}
	local vel = state.vel
	local onGround = state.onGround
	local groundNormal = state.groundNormal
	local inWater = state.inWater
	local onLadder = state.onLadder
	local crouching = input.crouch
	local jump = input.jump
	local moveDir = input.moveDir
	local dt = dt
	local now = tick()
	local surf = self:IsSurfing(groundNormal, onGround, onLadder, inWater)
	state.surf = surf

	-- Counter-strafe logic (only on ground and not bunnyhopping)
	local canCounterStrafe = false
	if onGround and state.wasOnGround then
		canCounterStrafe = true
	end

	if canCounterStrafe then
		vel = counterStrafeVelocity(vel, moveDir, state.prevMoveDir, state.axisTapTime, canCounterStrafe)
		-- Reset axisTapTime after using it, so it only applies once per tap
		if state.axisTapTime then
			if math.abs(state.prevMoveDir and state.prevMoveDir.X or 0) > 0.5 and math.abs(moveDir.X) <= 0.01 then
				state.axisTapTime.X = 0
			end
			if math.abs(state.prevMoveDir and state.prevMoveDir.Z or 0) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
				state.axisTapTime.Z = 0
			end
		end
	end

	-- Water/Ladder
	if inWater then
		vel = moveDir * WATER_SPEED
		if jump then vel = vel + Vector3.new(0, JUMP_POWER * 0.5, 0) end
		return vel
	elseif onLadder then
		vel = moveDir * LADDER_SPEED
		if jump then vel = vel + Vector3.new(0, LADDER_SPEED, 0) end
		return vel
	end

	-- Crouch sliding
	if state.crouchSliding then
		vel = self:ApplyFriction(vel, dt, true, CROUCH_SLIDE_FRICTION)
		vel = self:Accelerate(vel, moveDir, CROUCH_SLIDE_SPEED, GROUND_ACCEL, dt, CROUCH_SLIDE_SPEED)
		state.crouchSlideTimer = state.crouchSlideTimer - dt
		if state.crouchSlideTimer <= 0 or not crouching then
			state.crouchSliding = false
		end
		return vel
	end

	-- Surfing
	if surf then
		-- Frictionless, high air acceleration, can't jump
		vel = self:ApplyFriction(vel, dt, true, SURF_FRICTION)
		if moveDir.Magnitude > 0 then
			vel = self:Accelerate(vel, moveDir.Unit, SURF_MAX_SPEED, SURF_AIR_ACCEL, dt, SURF_MAX_SPEED)
		end
		-- Project velocity onto surf plane
		local surfNormal = groundNormal
		vel = vel - surfNormal * vel:Dot(surfNormal)
		return vel
	end

	-- Bunnyhop grace period logic
	state.bhopGraceTimer = state.bhopGraceTimer or 0
	state.bhopShouldPreserve = state.bhopShouldPreserve or false

	-- If just landed, start grace period
	if state.landed then
		state.bhopGraceTimer = now + BHOP_GRACE_PERIOD
		state.bhopShouldPreserve = false
	end

	-- Friction
	vel = self:ApplyFriction(vel, dt, onGround)

	-- Slope
	if onGround then
		vel = self:HandleSlope(vel, groundNormal)
	end

	-- Acceleration
	local wishSpeed = crouching and CROUCH_SPEED or WALK_SPEED
	local accel = onGround and GROUND_ACCEL or AIR_ACCEL
	if not onGround then wishSpeed = math.min(wishSpeed, MAX_AIR_SPEED) end
	if moveDir.Magnitude > 0 then
		vel = self:Accelerate(vel, moveDir.Unit, wishSpeed, accel, dt, (onGround and nil or MAX_AIR_SPEED))
	end

	-- Jump (no auto-hop: must release and repress jump)
	if jump and onGround and state.canJump then
		-- Bunnyhop grace: If jumping within grace period, preserve velocity (up to cap)
		if now <= (state.bhopGraceTimer or 0) then
			state.bhopShouldPreserve = true
		else
			state.bhopShouldPreserve = false
		end

		-- Apply jump
		vel = Vector3.new(vel.X, JUMP_POWER, vel.Z)
		if crouching then
			vel = vel * CROUCH_JUMP_BOOST
		end
		state.canJump = false
	elseif not jump and onGround then
		state.canJump = true
	end

	-- Air control (strafe jumping)
	if not onGround and moveDir.Magnitude > 0 then
		local airControl = 0.4 -- Slightly increased for more air control
		local proj = vel:Dot(moveDir.Unit)
		if proj < wishSpeed then
			vel = vel + moveDir.Unit * AIR_ACCEL * dt * airControl
		end
	end

	-- Inertia (momentum preserved)
	-- Already handled by not resetting velocity

	-- Landing penalty
	vel, state.lastLandTime = self:HandleLandingPenalty(vel, state.fallVel, state.landed, state.lastLandTime, now)

	-- Bunnyhop velocity cap and reset
	if onGround then
		local flatVel = Vector3.new(vel.X, 0, vel.Z)
		local maxBhopSpeed = WALK_SPEED * BHOP_MAX_MULTIPLIER
		if state.bhopShouldPreserve then
			-- If bunnyhopped within grace period, preserve velocity but cap it
			if flatVel.Magnitude > maxBhopSpeed then
				flatVel = flatVel.Unit * maxBhopSpeed
				vel = Vector3.new(flatVel.X, vel.Y, flatVel.Z)
			end
			-- Reset flag so it only applies once per jump
			state.bhopShouldPreserve = false
		else
			-- If landed normally (not bunnyhopping), reset velocity to max walking speed
			if flatVel.Magnitude > WALK_SPEED then
				flatVel = flatVel.Unit * WALK_SPEED
				vel = Vector3.new(flatVel.X, vel.Y, flatVel.Z)
			end
		end
	end

	return vel
end

function SourceMovement:TryStartCrouchSlide(state, input)
	-- Start crouch slide if moving fast, on ground, and just pressed crouch
	if state.onGround and not state.crouchSliding and input.crouch and not state.prevCrouch and state.vel.Magnitude > WALK_SPEED * 1.2 then
		state.crouchSliding = true
		state.crouchSlideTimer = CROUCH_SLIDE_TIME
	end
end

return SourceMovement

Video:

1 Like

Could you please explain the issue in more detail?

When I start strafing in circles for example, I start gathering speed for some reason, like I am surfing on the baseplate and start sliding, as you can see there’s text that shows hrp’s velocity in top left, that might help you too

1 Like

In the vid it seems like your speed becomes permanently increased after you do the circle movement. Is this the case? If so, maybe a variable you’re using to determine speed somehow increases by mistake?

It’s worth strategically placing Prints to monitor these variables, or create a debug UI to track your variables so it’s easier to read.

??? Did i do it right?

Nothing changes at all, also check my scripts I showed in the first post.

1 Like

What is the original purpose of these scripts?

To recreate source engine movement

1 Like

Did you write the codes yourself?

Yes, i did, lmao, why would you even ask?
Reusing my own past code constitutes authorship.

1 Like

Update friction to be something like

if speed < 0.01 then
    return Vector3.zero
end
1 Like

I still slide for some reason..

Oh, I tried to make an environment for testing in studio using your scripts but the only thing I saw is that friction stops the character instantly unless in third person, where it will be so low that it takes a few seconds for the velocity to stop, so it’s probably something with that

1 Like

How about using this codes:

-- SourceMovement ModuleScript
-- Implements Source-engine-like movement physics for Roblox characters, with bhop, surfing, and crouch sliding

local SourceMovement = {}

-- Tweakable parameters
local WALK_SPEED = 16
local CROUCH_SPEED = 8
local JUMP_POWER = 50
local CROUCH_JUMP_BOOST = 1.2
local AIR_ACCEL = 34 -- Increased for more bhop speed gain
local GROUND_ACCEL = 64
local FRICTION = 8
local AIR_FRICTION = 0.5
local SLOPE_SLIDE_ANGLE = 40
local SLOPE_SLOW_ANGLE = 20
local SURF_MIN_ANGLE = 10
local SURF_MAX_ANGLE = 80
local SURF_FRICTION = 0.01
local SURF_AIR_ACCEL = 60
local SURF_MAX_SPEED = 60
local LANDING_PENALTY_VEL = 60
local LANDING_PENALTY_TIME = 0.4
local WATER_SPEED = 8
local LADDER_SPEED = 10
local MAX_AIR_SPEED = 32 -- Increased for more bhop speed gain
local CROUCH_SLIDE_SPEED = 30
local CROUCH_SLIDE_TIME = 0.5
local CROUCH_SLIDE_FRICTION = 1

local COUNTERSTRAFE_TAP_THRESHOLD = 0.15 -- seconds, must tap opposite key within this time to trigger instant stop

-- Bunnyhop grace period settings
local BHOP_GRACE_PERIOD = 0.12 -- seconds to jump after landing to keep velocity
local BHOP_MAX_MULTIPLIER = 1.25 -- max velocity multiplier allowed from bunnyhop

local function getSlopeAngle(normal)
	if normal.Y == 0 then return 90 end
	return math.deg(math.acos(normal.Y))
end

function SourceMovement:IsSurfing(groundNormal, onGround, onLadder, inWater)
	if not onGround or onLadder or inWater then return false end
	if groundNormal.Magnitude < 0.99 then return false end
	local angle = getSlopeAngle(groundNormal)
	return angle > SURF_MIN_ANGLE and angle < SURF_MAX_ANGLE
end

function SourceMovement:ApplyFriction(vel, dt, onGround, frictionOverride)
	local friction = frictionOverride or (onGround and FRICTION or AIR_FRICTION)
	local speed = vel.Magnitude
	if speed ~= 0 then
		local drop = speed * friction * dt
		local newSpeed = math.max(speed - drop, 0)
		return vel.Unit * newSpeed
	end
	return vel
end

function SourceMovement:Accelerate(vel, wishDir, wishSpeed, accel, dt, maxSpeed)
	local currentSpeed = vel:Dot(wishDir)
	local addSpeed = wishSpeed - currentSpeed
	if addSpeed <= 0 then return vel end
	local accelSpeed = accel * dt * wishSpeed
	if accelSpeed > addSpeed then accelSpeed = addSpeed end
	local newVel = vel + wishDir * accelSpeed
	if maxSpeed and newVel.Magnitude > maxSpeed then
		newVel = newVel.Unit * maxSpeed
	end
	return newVel
end

function SourceMovement:HandleSlope(vel, groundNormal)
	local angle = getSlopeAngle(groundNormal)
	if angle > SLOPE_SLIDE_ANGLE then
		-- Slide down steep slopes
		local slideDir = Vector3.new(groundNormal.X, 0, groundNormal.Z)
		if slideDir.Magnitude > 0 then
			slideDir = slideDir.Unit
			return vel + slideDir * (angle - SLOPE_SLIDE_ANGLE)
		end
	elseif angle > SLOPE_SLOW_ANGLE then
		-- Slow uphill
		return vel * 0.7
	end
	return vel
end

function SourceMovement:HandleLandingPenalty(vel, fallVel, landed, lastLandTime, now)
	if landed and fallVel < -LANDING_PENALTY_VEL and (now - lastLandTime) > 0.1 then
		return vel * 0.5, now
	end
	return vel, lastLandTime
end

-- Counter-strafe helper
local function counterStrafeVelocity(vel, moveDir, prevMoveDir, axisTapTime, canCounterStrafe)
	local newVel = vel

	if not canCounterStrafe then
		return newVel
	end

	if prevMoveDir and moveDir then
		-- Counter-strafe left/right (A/D)
		if prevMoveDir.X > 0.5 and moveDir.X < -0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
			newVel = Vector3.new(0, newVel.Y, newVel.Z)
		elseif prevMoveDir.X < -0.5 and moveDir.X > 0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
			newVel = Vector3.new(0, newVel.Y, newVel.Z)
		end

		-- Counter-strafe forward/back (W/S)
		if prevMoveDir.Z > 0.5 and moveDir.Z < -0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
			newVel = Vector3.new(newVel.X, newVel.Y, 0)
		elseif prevMoveDir.Z < -0.5 and moveDir.Z > 0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
			newVel = Vector3.new(newVel.X, newVel.Y, 0)
		end

		-- NEW: If you release the opposite key after counter-strafe (i.e. go from input to zero), stop instantly
		-- Only if the opposite key was tapped (held for less than threshold)
		-- X axis
		if math.abs(prevMoveDir.X) > 0.5 and math.abs(moveDir.X) <= 0.01 then
			if axisTapTime and axisTapTime.X and axisTapTime.X > 0 and axisTapTime.X <= COUNTERSTRAFE_TAP_THRESHOLD then
				newVel = Vector3.new(0, newVel.Y, newVel.Z)
			end
		end

		-- Z axis
		if math.abs(prevMoveDir.Z) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
			if axisTapTime and axisTapTime.Z and axisTapTime.Z > 0 and axisTapTime.Z <= COUNTERSTRAFE_TAP_THRESHOLD then
				newVel = Vector3.new(newVel.X, newVel.Y, 0)
			end
		end
	end

	return newVel
end

function SourceMovement:Move(state, input, dt)
	-- state: {vel, onGround, groundNormal, inWater, onLadder, crouching, jumpQueued, jumpHeld, canJump, surf, crouchSliding, crouchSlideTimer, fallVel, landed, lastLandTime, prevMoveDir, axisTapTime, wasOnGround, bhopGraceTimer, bhopShouldPreserve}
	-- input: {moveDir, jump, crouch}
	local vel = state.vel
	local onGround = state.onGround
	local groundNormal = state.groundNormal
	local inWater = state.inWater
	local onLadder = state.onLadder
	local crouching = input.crouch
	local jump = input.jump
	local moveDir = input.moveDir
	local dt = dt
	local now = tick()
	local surf = self:IsSurfing(groundNormal, onGround, onLadder, inWater)
	state.surf = surf

	-- Counter-strafe logic (only on ground and not bunnyhopping)
	local canCounterStrafe = false
	if onGround and state.wasOnGround then
		canCounterStrafe = true
	end

	if canCounterStrafe then
		vel = counterStrafeVelocity(vel, moveDir, state.prevMoveDir, state.axisTapTime, canCounterStrafe)
		-- Reset axisTapTime after using it, so it only applies once per tap
		if state.axisTapTime then
			if math.abs(state.prevMoveDir and state.prevMoveDir.X or 0) > 0.5 and math.abs(moveDir.X) <= 0.01 then
				state.axisTapTime.X = 0
			end
			if math.abs(state.prevMoveDir and state.prevMoveDir.Z or 0) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
				state.axisTapTime.Z = 0
			end
		end
	end

	-- Water/Ladder
	if inWater then
		vel = moveDir * WATER_SPEED
		if jump then vel = vel + Vector3.new(0, JUMP_POWER * 0.5, 0) end
		return vel
	elseif onLadder then
		vel = moveDir * LADDER_SPEED
		if jump then vel = vel + Vector3.new(0, LADDER_SPEED, 0) end
		return vel
	end

	-- Crouch sliding
	if state.crouchSliding then
		vel = self:ApplyFriction(vel, dt, true, CROUCH_SLIDE_FRICTION)
		vel = self:Accelerate(vel, moveDir, CROUCH_SLIDE_SPEED, GROUND_ACCEL, dt, CROUCH_SLIDE_SPEED)
		state.crouchSlideTimer = state.crouchSlideTimer - dt
		if state.crouchSlideTimer <= 0 or not crouching then
			state.crouchSliding = false
		end
		return vel
	end

	-- Surfing
	if surf then
		-- Frictionless, high air acceleration, can't jump
		vel = self:ApplyFriction(vel, dt, true, SURF_FRICTION)
		if moveDir.Magnitude > 0 then
			vel = self:Accelerate(vel, moveDir.Unit, SURF_MAX_SPEED, SURF_AIR_ACCEL, dt, SURF_MAX_SPEED)
		end
		-- Project velocity onto surf plane
		local surfNormal = groundNormal
		vel = vel - surfNormal * vel:Dot(surfNormal)
		return vel
	end

	-- Bunnyhop grace period logic
	state.bhopGraceTimer = state.bhopGraceTimer or 0
	state.bhopShouldPreserve = state.bhopShouldPreserve or false

	-- If just landed, start grace period
	if state.landed then
		state.bhopGraceTimer = now + BHOP_GRACE_PERIOD
		state.bhopShouldPreserve = false
	end

	-- Friction
	vel = self:ApplyFriction(vel, dt, onGround)

	-- Slope
	if onGround then
		vel = self:HandleSlope(vel, groundNormal)
	end

	-- Acceleration
	local maxSpeed = onGround and WALK_SPEED * 1.25 or MAX_AIR_SPEED
	local wishSpeed = crouching and CROUCH_SPEED or WALK_SPEED
	local accel = onGround and GROUND_ACCEL or AIR_ACCEL
	if not onGround then wishSpeed = math.min(wishSpeed, MAX_AIR_SPEED) end
	if moveDir.Magnitude > 0 then
		vel = self:Accelerate(vel, moveDir.Unit, wishSpeed, accel, dt, maxSpeed)
	end

	-- Jump (no auto-hop: must release and repress jump)
	if jump and onGround and state.canJump then
		-- Bunnyhop grace: If jumping within grace period, preserve velocity (up to cap)
		if now <= (state.bhopGraceTimer or 0) then
			state.bhopShouldPreserve = true
		else
			state.bhopShouldPreserve = false
		end

		-- Apply jump
		vel = Vector3.new(vel.X, JUMP_POWER, vel.Z)
		if crouching then
			vel = vel * CROUCH_JUMP_BOOST
		end
		state.canJump = false
	elseif not jump and onGround then
		state.canJump = true
	end

	-- Air control (strafe jumping)
	if not onGround and moveDir.Magnitude > 0 then
		local airControl = 0.4 -- Slightly increased for more air control
		local proj = vel:Dot(moveDir.Unit)
		if proj < wishSpeed then
			vel = vel + moveDir.Unit * AIR_ACCEL * dt * airControl
		end
	end

	-- Inertia (momentum preserved)
	-- Already handled by not resetting velocity

	-- Landing penalty
	vel, state.lastLandTime = self:HandleLandingPenalty(vel, state.fallVel, state.landed, state.lastLandTime, now)

	-- Bunnyhop velocity cap and reset
	if onGround then
		local flatVel = Vector3.new(vel.X, 0, vel.Z)
		local maxBhopSpeed = WALK_SPEED * BHOP_MAX_MULTIPLIER
		if state.bhopShouldPreserve then
			-- If bunnyhopped within grace period, preserve velocity but cap it
			if flatVel.Magnitude > maxBhopSpeed then
				flatVel = flatVel.Unit * maxBhopSpeed
				vel = Vector3.new(flatVel.X, vel.Y, flatVel.Z)
			end
			-- Reset flag so it only applies once per jump
			state.bhopShouldPreserve = false
		else
			-- If landed normally (not bunnyhopping), reset velocity to max walking speed
			if flatVel.Magnitude > WALK_SPEED then
				flatVel = flatVel.Unit * WALK_SPEED
				vel = Vector3.new(flatVel.X, vel.Y, flatVel.Z)
			end
		end
	end

	return vel
end

function SourceMovement:TryStartCrouchSlide(state, input)
	-- Start crouch slide if moving fast, on ground, and just pressed crouch
	if state.onGround and not state.crouchSliding and input.crouch and not state.prevCrouch and state.vel.Magnitude > WALK_SPEED * 1.2 then
		state.crouchSliding = true
		state.crouchSlideTimer = CROUCH_SLIDE_TIME
	end
end

return SourceMovement
-- SourceMovementController LocalScript
-- Handles player input and applies SourceMovement physics, with bhop fix, surfing, and crouch sliding

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local SourceMovement = require(game.ReplicatedStorage:WaitForChild("SourceMovement"))

local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local rootPart = character:WaitForChild("HumanoidRootPart")

local state = {
	vel = Vector3.new(),
	onGround = true,
	groundNormal = Vector3.new(0,1,0),
	inWater = false,
	onLadder = false,
	crouching = false,
	jumpQueued = false,
	jumpHeld = false,
	canJump = true,
	surf = false,
	crouchSliding = false,
	crouchSlideTimer = 0,
	fallVel = 0,
	landed = false,
	lastLandTime = 0,
	prevCrouch = false,
	prevMoveDir = Vector3.new(), -- Added for counter-strafe
	-- For counter-strafe tap logic:
	axisHoldStart = {X = 0, Z = 0}, -- When did we start holding a direction on each axis
	axisLastDir = {X = 0, Z = 0},   -- Last nonzero direction on each axis
	axisTapTime = {X = 0, Z = 0},   -- How long the opposite was held before release
	wasOnGround = true, -- Track previous onGround state for bunnyhop/counter-strafe logic
	-- Bunnyhop grace period
	bhopGraceTimer = 0,
	bhopShouldPreserve = false,
}

local input = {
	moveDir = Vector3.new(),
	jump = false,
	crouch = false,
}

-- Input handling
local moveVector = Vector3.new()
local jumpPressed = false
local jumpPressedLast = false -- Track previous jump key state
local jumpConsumed = false    -- Prevents holding space for auto-hop
local crouchPressed = false

UserInputService.InputBegan:Connect(function(inputObj, processed)
	if processed then return end
	if inputObj.KeyCode == Enum.KeyCode.Space then
		jumpPressed = true
	elseif inputObj.KeyCode == Enum.KeyCode.LeftControl or inputObj.KeyCode == Enum.KeyCode.C then
		crouchPressed = true
	end
end)

UserInputService.InputEnded:Connect(function(inputObj, processed)
	if inputObj.KeyCode == Enum.KeyCode.Space then
		jumpPressed = false
		jumpConsumed = false -- Allow jump again after key is released
	elseif inputObj.KeyCode == Enum.KeyCode.LeftControl or inputObj.KeyCode == Enum.KeyCode.C then
		crouchPressed = false
	end
end)

local function getMoveDirection()
	local dir = Vector3.new()
	if UserInputService:IsKeyDown(Enum.KeyCode.W) then dir = dir + Vector3.new(0,0,-1) end
	if UserInputService:IsKeyDown(Enum.KeyCode.S) then dir = dir + Vector3.new(0,0,1) end
	if UserInputService:IsKeyDown(Enum.KeyCode.A) then dir = dir + Vector3.new(-1,0,0) end
	if UserInputService:IsKeyDown(Enum.KeyCode.D) then dir = dir + Vector3.new(1,0,0) end
	-- Camera relative
	local cam = workspace.CurrentCamera
	if cam then
		local cf = cam.CFrame
		local move = (cf.LookVector * -dir.Z + cf.RightVector * dir.X)
		move = Vector3.new(move.X, 0, move.Z)
		if move.Magnitude > 0 then
			return move.Unit
		end
	end
	return Vector3.new()
end

-- Main movement loop
local lastGrounded = true
local lastY = rootPart.Position.Y

-- For velocity smoothing
local prevAppliedVelocity = Vector3.new()
local velocityLerpAlpha = 1 -- Smoothing factor (1 = instant, 0.5 = fast, 0.25 = slow)

-- Counter-strafe settings
local COUNTERSTRAFE_TAP_WINDOW = 0.18 -- seconds, max time for a tap to count as counter-strafe

RunService.RenderStepped:Connect(function(dt)
	-- Update input
	input.moveDir = getMoveDirection()
	input.crouch = crouchPressed

	-- Jump logic: Only allow jump on the frame the key is pressed (no hold-to-bhop)
	if jumpPressed and not jumpPressedLast and not jumpConsumed then
		input.jump = true
		jumpConsumed = true -- Block further jumps until key is released
	else
		input.jump = false
	end
	jumpPressedLast = jumpPressed

	-- Store previous move direction for counter-strafe
	state.prevMoveDir = state.moveDir or Vector3.new()
	state.moveDir = input.moveDir

	-- Counter-strafe tap logic: Track axis hold times and tap durations
	local now = tick()
	for _, axis in {"X", "Z"} do
		local prev = state.axisLastDir[axis] or 0
		local curr = input.moveDir[axis]
		-- If we just started holding a direction (from 0 to nonzero)
		if math.abs(prev) < 0.01 and math.abs(curr) > 0.5 then
			state.axisHoldStart[axis] = now
		end
		-- If we just released a direction (from nonzero to 0)
		if math.abs(prev) > 0.5 and math.abs(curr) < 0.01 then
			state.axisTapTime[axis] = now - (state.axisHoldStart[axis] or now)
		end
		state.axisLastDir[axis] = curr
	end

	-- Check ground
	local rayOrigin = rootPart.Position
	local rayDir = Vector3.new(0, -3, 0)
	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Exclude
	params.FilterDescendantsInstances = {character}
	params.IgnoreWater = true

	local result = workspace:Raycast(rayOrigin, rayDir, params)
	
	if result and result.Normal.Magnitude > 0.5 then
		state.onGround = result.Normal.Y > 0.7
		state.groundNormal = result.Normal
	else
		state.onGround = false
		state.groundNormal = norm or Vector3.new(0,1,0)
	end

	-- Water/Ladder detection (simple placeholder)
	state.inWater = false
	state.onLadder = false

	-- Fall velocity
	state.fallVel = rootPart.Velocity.Y

	-- Landing detection
	state.landed = (not lastGrounded) and state.onGround
	lastGrounded = state.onGround

	-- Track previous onGround state for bunnyhop/counter-strafe logic
	state.wasOnGround = state.wasOnGround or true
	state.wasOnGround = state.onGround

	-- Crouch slide logic
	state.prevCrouch = state.crouching
	state.crouching = input.crouch
	SourceMovement:TryStartCrouchSlide(state, input)

	-- Apply movement
	state.vel = SourceMovement:Move(state, input, dt)

	-- --- COUNTER-STRAFE INSTANT STOP LOGIC ---
	-- If the player quickly switches direction (counter-strafe) on X or Z axis, and is on ground,
	-- forcibly zero velocity on that axis for one frame to prevent sliding.
	if state.onGround then
		for _, axis in {"X", "Z"} do
			local prev = state.prevMoveDir[axis]
			local curr = input.moveDir[axis]
			-- If the player just switched from moving in one direction to the opposite (counter-strafe)
			if math.abs(prev) > 0.5 and math.abs(curr) > 0.5 and (prev * curr) < 0 then
				-- Only trigger if the tap was quick enough (within COUNTERSTRAFE_TAP_WINDOW)
				if state.axisTapTime[axis] > 0 and state.axisTapTime[axis] <= COUNTERSTRAFE_TAP_WINDOW then
					-- Zero velocity on this axis
					state.vel = Vector3.new(
						axis == "X" and 0 or state.vel.X,
						state.vel.Y,
						axis == "Z" and 0 or state.vel.Z
					)
					-- Also forcibly zero rootPart velocity on this axis for this frame
					rootPart.Velocity = Vector3.new(
						axis == "X" and 0 or rootPart.Velocity.X,
						rootPart.Velocity.Y,
						axis == "Z" and 0 or rootPart.Velocity.Z
					)
					-- Reset tap time so it doesn't trigger again until next tap
					state.axisTapTime[axis] = 0
				end
			end
		end
	end

	-- --- INSTANT STOP OVERRIDE ---
	-- If there is no input and the player is on the ground, forcibly zero velocity to prevent sliding.
	if input.moveDir.Magnitude < 0.01 and state.onGround then
		state.vel = Vector3.new(0, 0, 0)
		rootPart.Velocity = Vector3.new(0, rootPart.Velocity.Y, 0)
		prevAppliedVelocity = rootPart.Velocity
	else
		-- Instantly set velocity for snappy control (no smoothing)
		local targetVel
		if state.surf then
			targetVel = Vector3.new(state.vel.X, 0, state.vel.Z)
		else
			targetVel = Vector3.new(state.vel.X, rootPart.Velocity.Y, state.vel.Z)
			if input.jump and state.onGround and state.canJump then
				targetVel = Vector3.new(state.vel.X, state.vel.Y, state.vel.Z)
			end
		end
		-- Lerp previous velocity to target velocity for smoothness (set alpha=1 for instant)
		prevAppliedVelocity = prevAppliedVelocity:Lerp(targetVel, velocityLerpAlpha)
		rootPart.Velocity = prevAppliedVelocity
	end

	-- Crouch
	if input.crouch then
		humanoid.CameraOffset = Vector3.new(0, -2, 0)
	else
		humanoid.CameraOffset = Vector3.new(0, 0, 0)
	end

	-- Clamp speed for sanity
	local flatVel = Vector3.new(rootPart.Velocity.X, 0, rootPart.Velocity.Z)
	if flatVel.Magnitude > 60 then
		local y = rootPart.Velocity.Y
		rootPart.Velocity = flatVel.Unit * 60 + Vector3.new(0, y, 0)
	end
end)

i am still sliding with these scripts..
New scripts anyway:

-- SourceMovement ModuleScript
-- Implements Source-engine-like movement physics for Roblox characters, with bhop, surfing, and crouch sliding

local SourceMovement = {}

-- Tweakable parameters
local CROUCH_SPEED = 8
local JUMP_POWER = 50
local CROUCH_JUMP_BOOST = 1.2
local AIR_ACCEL = 34 -- Increased for more bhop speed gain
local GROUND_ACCEL = 10
local FRICTION = 12
local AIR_FRICTION = 0.5
local SLOPE_SLIDE_ANGLE = 40
local SLOPE_SLOW_ANGLE = 20
local LANDING_PENALTY_VEL = 60
local LANDING_PENALTY_TIME = 0.4
local WATER_SPEED = 8
local LADDER_SPEED = 10
local MAX_AIR_SPEED = 32 -- Increased for more bhop speed gain
local CROUCH_SLIDE_SPEED = 30
local CROUCH_SLIDE_TIME = 0.5
local CROUCH_SLIDE_FRICTION = 1

local COUNTERSTRAFE_TAP_THRESHOLD = 0.15 -- seconds, must tap opposite key within this time to trigger instant stop

-- Bunnyhop grace period settings
local BHOP_GRACE_PERIOD = 0.12 -- seconds to jump after landing to keep velocity
local BHOP_MAX_MULTIPLIER = 1.25 -- max velocity multiplier allowed from bunnyhop

local function getSlopeAngle(normal)
	if normal.Y == 0 then return 90 end
	return math.deg(math.acos(normal.Y))
end

function SourceMovement:ApplyFriction(vel, dt, onGround, frictionOverride)
	local friction = frictionOverride or (onGround and FRICTION or AIR_FRICTION)
	local speed = vel.Magnitude
	-- Source Engine style: snap to zero when speed is very low
	if speed < 0.5 then
		return Vector3.zero
	end
	if speed ~= 0 then
		local drop = speed * friction * dt
		local newSpeed = math.max(speed - drop, 0)
		return vel.Unit * newSpeed
	end
	return vel
end

function SourceMovement:Accelerate(vel, wishDir, wishSpeed, accel, dt, maxSpeed)
	local currentSpeed = vel:Dot(wishDir)
	local addSpeed = wishSpeed - currentSpeed
	if addSpeed <= 0 then return vel end
	local accelSpeed = accel * dt * wishSpeed
	if accelSpeed > addSpeed then accelSpeed = addSpeed end
	local newVel = vel + wishDir * accelSpeed
	if maxSpeed and newVel.Magnitude > maxSpeed then
		newVel = newVel.Unit * maxSpeed
	end
	return newVel
end

function SourceMovement:HandleSlope(vel, groundNormal)
	local angle = getSlopeAngle(groundNormal)
	if angle > SLOPE_SLIDE_ANGLE then
		-- Slide down steep slopes
		local slideDir = Vector3.new(groundNormal.X, 0, groundNormal.Z)
		if slideDir.Magnitude > 0 then
			slideDir = slideDir.Unit
			return vel + slideDir * (angle - SLOPE_SLIDE_ANGLE)
		end
	elseif angle > SLOPE_SLOW_ANGLE then
		-- Slow uphill
		return vel * 0.7
	end
	return vel
end

function SourceMovement:HandleLandingPenalty(vel, fallVel, landed, lastLandTime, now)
	if landed and fallVel < -LANDING_PENALTY_VEL and (now - lastLandTime) > 0.1 then
		return vel * 0.5, now
	end
	return vel, lastLandTime
end

-- Counter-strafe helper
local function counterStrafeVelocity(vel, moveDir, prevMoveDir, axisTapTime, canCounterStrafe)
	local newVel = vel

	if not canCounterStrafe then
		return newVel
	end

	if prevMoveDir and moveDir then
		-- Counter-strafe left/right (A/D)
		if prevMoveDir.X > 0.5 and moveDir.X < -0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
			newVel = Vector3.new(0, newVel.Y, newVel.Z)
		elseif prevMoveDir.X < -0.5 and moveDir.X > 0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
			newVel = Vector3.new(0, newVel.Y, newVel.Z)
		end

		-- Counter-strafe forward/back (W/S)
		if prevMoveDir.Z > 0.5 and moveDir.Z < -0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
			newVel = Vector3.new(newVel.X, newVel.Y, 0)
		elseif prevMoveDir.Z < -0.5 and moveDir.Z > 0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
			newVel = Vector3.new(newVel.X, newVel.Y, 0)
		end

		-- NEW: If you release the opposite key after counter-strafe (i.e. go from input to zero), stop instantly
		-- Only if the opposite key was tapped (held for less than threshold)
		-- X axis
		if math.abs(prevMoveDir.X) > 0.5 and math.abs(moveDir.X) <= 0.01 then
			if axisTapTime and axisTapTime.X and axisTapTime.X > 0 and axisTapTime.X <= COUNTERSTRAFE_TAP_THRESHOLD then
				newVel = Vector3.new(0, newVel.Y, newVel.Z)
			end
		end

		-- Z axis
		if math.abs(prevMoveDir.Z) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
			if axisTapTime and axisTapTime.Z and axisTapTime.Z > 0 and axisTapTime.Z <= COUNTERSTRAFE_TAP_THRESHOLD then
				newVel = Vector3.new(newVel.X, newVel.Y, 0)
			end
		end
	end

	return newVel
end

function SourceMovement:Move(state, input, dt)
	-- state: {vel, onGround, groundNormal, inWater, onLadder, crouching, jumpQueued, jumpHeld, canJump, surf, crouchSliding, crouchSlideTimer, fallVel, landed, lastLandTime, prevMoveDir, axisTapTime, wasOnGround, bhopGraceTimer, bhopShouldPreserve}
	-- input: {moveDir, jump, crouch}
	local vel = state.vel
	local onGround = state.onGround
	local groundNormal = state.groundNormal
	local inWater = state.inWater
	local onLadder = state.onLadder
	local crouching = input.crouch
	local jump = input.jump
	local moveDir = input.moveDir
	local dt = dt
	local now = tick()

	-- Counter-strafe logic (only on ground and not bunnyhopping)
	local canCounterStrafe = false
	if onGround and state.wasOnGround then
		canCounterStrafe = true
	end

	if canCounterStrafe then
		vel = counterStrafeVelocity(vel, moveDir, state.prevMoveDir, state.axisTapTime, canCounterStrafe)
		-- Reset axisTapTime after using it, so it only applies once per tap
		if state.axisTapTime then
			if math.abs(state.prevMoveDir and state.prevMoveDir.X or 0) > 0.5 and math.abs(moveDir.X) <= 0.01 then
				state.axisTapTime.X = 0
			end
			if math.abs(state.prevMoveDir and state.prevMoveDir.Z or 0) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
				state.axisTapTime.Z = 0
			end
		end
	end

	-- Water/Ladder
	if inWater then
		vel = moveDir * WATER_SPEED
		if jump then vel = vel + Vector3.new(0, JUMP_POWER * 0.5, 0) end
		return vel
	elseif onLadder then
		vel = moveDir * LADDER_SPEED
		if jump then vel = vel + Vector3.new(0, LADDER_SPEED, 0) end
		return vel
	end

	-- Crouch sliding
	if state.crouchSliding then
		vel = self:ApplyFriction(vel, dt, true, CROUCH_SLIDE_FRICTION)
		vel = self:Accelerate(vel, moveDir, CROUCH_SLIDE_SPEED, GROUND_ACCEL, dt, CROUCH_SLIDE_SPEED)
		state.crouchSlideTimer = state.crouchSlideTimer - dt
		if state.crouchSlideTimer <= 0 or not crouching then
			state.crouchSliding = false
		end
		return vel
	end

	-- Bunnyhop grace period logic
	state.bhopGraceTimer = state.bhopGraceTimer or 0
	state.bhopShouldPreserve = state.bhopShouldPreserve or false

	-- If just landed, start grace period
	if state.landed then
		state.bhopGraceTimer = now + BHOP_GRACE_PERIOD
		state.bhopShouldPreserve = false
	end

	-- Friction
	vel = self:ApplyFriction(vel, dt, onGround)

	-- Slope
	if onGround then
		vel = self:HandleSlope(vel, groundNormal)
	end

	-- Acceleration and velocity clamping
	local wishSpeed = crouching and CROUCH_SPEED or 16
	local accel = onGround and GROUND_ACCEL or AIR_ACCEL
	if not onGround then
		wishSpeed = math.min(wishSpeed, MAX_AIR_SPEED)
	end

	if onGround then
		-- --- STRICT GROUND VELOCITY CLAMP ---
		-- Always clamp horizontal velocity to ground speed (16) unless in crouch slide or bunnyhop grace
		local flatVel = Vector3.new(vel.X, 0, vel.Z)
		local maxBhopSpeed = 16 * BHOP_MAX_MULTIPLIER

		-- Bunnyhop velocity cap logic
		if state.bhopShouldPreserve then
			-- If bunnyhopped within grace period, preserve velocity but cap it
			if flatVel.Magnitude > maxBhopSpeed then
				flatVel = flatVel.Unit * maxBhopSpeed
			end
			vel = Vector3.new(flatVel.X, vel.Y, flatVel.Z)
			-- Reset flag so it only applies once per jump
			state.bhopShouldPreserve = false
		else
			-- Not bunnyhopping: always clamp to ground speed
			if flatVel.Magnitude > 16 then
				flatVel = flatVel.Unit * 16
				vel = Vector3.new(flatVel.X, vel.Y, flatVel.Z)
			end
		end

		-- --- NO GROUND ACCELERATION IF AT OR ABOVE GROUND SPEED ---
		flatVel = Vector3.new(vel.X, 0, vel.Z)
		if moveDir.Magnitude > 0 then
			if flatVel.Magnitude < 16 then
				vel = self:Accelerate(vel, moveDir.Unit, wishSpeed, GROUND_ACCEL, dt, 16)
				-- Clamp again after acceleration
				flatVel = Vector3.new(vel.X, 0, vel.Z)
				if flatVel.Magnitude > 16 then
					flatVel = flatVel.Unit * 16
					vel = Vector3.new(flatVel.X, vel.Y, flatVel.Z)
				end
			end
			-- If already at or above ground speed, do not accelerate further
		end
	else
		-- --- AIR ACCELERATION ---
		if moveDir.Magnitude > 0 then
			vel = self:Accelerate(vel, moveDir.Unit, wishSpeed, AIR_ACCEL, dt, MAX_AIR_SPEED)
		end
		-- --- AIR CONTROL ---
		local airControl = 0.4
		if moveDir.Magnitude > 0 then
			local proj = vel:Dot(moveDir.Unit)
			if proj < wishSpeed then
				vel = vel + moveDir.Unit * AIR_ACCEL * dt * airControl
			end
		end
	end

	-- Jump (no auto-hop: must release and repress jump)
	if jump and onGround and state.canJump then
		-- Bunnyhop grace: If jumping within grace period, preserve velocity (up to cap)
		if now <= (state.bhopGraceTimer or 0) then
			state.bhopShouldPreserve = true
		else
			state.bhopShouldPreserve = false
		end

		-- Apply jump
		vel = Vector3.new(vel.X, JUMP_POWER, vel.Z)
		if crouching then
			vel = vel * CROUCH_JUMP_BOOST
		end
		state.canJump = false
	elseif not jump and onGround then
		state.canJump = true
	end

	-- Inertia (momentum preserved)
	-- Already handled by not resetting velocity

	-- Landing penalty
	vel, state.lastLandTime = self:HandleLandingPenalty(vel, state.fallVel, state.landed, state.lastLandTime, now)

	-- --- INSTANT STOP ON GROUND WHEN NO INPUT ---
	if onGround and (not moveDir or moveDir.Magnitude < 0.01) then
		vel = Vector3.new(0, vel.Y, 0)
	end

	return vel
end

function SourceMovement:TryStartCrouchSlide(state, input)
	-- Start crouch slide if moving fast, on ground, and just pressed crouch
	if state.onGround and not state.crouchSliding and input.crouch and not state.prevCrouch and state.vel.Magnitude > 16 * 1.2 then
		state.crouchSliding = true
		state.crouchSlideTimer = CROUCH_SLIDE_TIME
	end
end

return SourceMovement
-- SourceMovementController LocalScript (Humanoid disabled version)
-- Handles player input and applies SourceMovement physics, with bhop fix, surfing, and crouch sliding
-- Humanoid is present but fully disabled; all velocity is applied directly to the root part

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local SourceMovement = require(game.ReplicatedStorage:WaitForChild("SourceMovement"))

local player = Players.LocalPlayer

-- Helper to get character and root part (Humanoid may exist, but is disabled)
local function getCharacterRoot()
	local character = player.Character or player.CharacterAdded:Wait()
	local rootPart = character:FindFirstChild("HumanoidRootPart") or character:FindFirstChildWhichIsA("BasePart")
	return character, rootPart
end

-- Disable Humanoid movement (if present)
local function disableHumanoid(character)
	local humanoid = character:FindFirstChildOfClass("Humanoid")
	if humanoid then
		humanoid.WalkSpeed = 0
	end
end

local character, rootPart = getCharacterRoot()
if character then
	disableHumanoid(character)
end

-- Reacquire rootPart and disable Humanoid on respawn
player.CharacterAdded:Connect(function(char)
	character = char
	rootPart = character:FindFirstChild("HumanoidRootPart") or character:FindFirstChildWhichIsA("BasePart")
	disableHumanoid(character)
end)

local state = {
	vel = Vector3.new(),
	onGround = true,
	groundNormal = Vector3.new(0,1,0),
	inWater = false,
	onLadder = false,
	crouching = false,
	jumpQueued = false,
	jumpHeld = false,
	canJump = true,
	crouchSliding = false,
	crouchSlideTimer = 0,
	fallVel = 0,
	landed = false,
	lastLandTime = 0,
	prevCrouch = false,
	prevMoveDir = Vector3.new(), -- Added for counter-strafe
	-- For counter-strafe tap logic:
	axisHoldStart = {X = 0, Z = 0}, -- When did we start holding a direction on each axis
	axisLastDir = {X = 0, Z = 0},   -- Last nonzero direction on each axis
	axisTapTime = {X = 0, Z = 0},   -- How long the opposite was held before release
	wasOnGround = true, -- Track previous onGround state for bunnyhop/counter-strafe logic
	-- Bunnyhop grace period
	bhopGraceTimer = 0,
	bhopShouldPreserve = false,
}

local input = {
	moveDir = Vector3.new(),
	jump = false,
	crouch = false,
}

-- Input handling
local moveVector = Vector3.new()
local jumpPressed = false
local jumpPressedLast = false -- Track previous jump key state
local jumpConsumed = false    -- Prevents holding space for auto-hop
local crouchPressed = false

UserInputService.InputBegan:Connect(function(inputObj, processed)
	if processed then return end
	if inputObj.KeyCode == Enum.KeyCode.Space then
		jumpPressed = true
	elseif inputObj.KeyCode == Enum.KeyCode.LeftControl or inputObj.KeyCode == Enum.KeyCode.C then
		crouchPressed = true
	end
end)

UserInputService.InputEnded:Connect(function(inputObj, processed)
	if inputObj.KeyCode == Enum.KeyCode.Space then
		jumpPressed = false
		jumpConsumed = false -- Allow jump again after key is released
	elseif inputObj.KeyCode == Enum.KeyCode.LeftControl or inputObj.KeyCode == Enum.KeyCode.C then
		crouchPressed = false
	end
end)

local function getMoveDirection()
	local dir = Vector3.new()
	if UserInputService:IsKeyDown(Enum.KeyCode.W) then dir = dir + Vector3.new(0,0,-1) end
	if UserInputService:IsKeyDown(Enum.KeyCode.S) then dir = dir + Vector3.new(0,0,1) end
	if UserInputService:IsKeyDown(Enum.KeyCode.A) then dir = dir + Vector3.new(-1,0,0) end
	if UserInputService:IsKeyDown(Enum.KeyCode.D) then dir = dir + Vector3.new(1,0,0) end
	-- Camera relative
	local cam = workspace.CurrentCamera
	if cam then
		local cf = cam.CFrame
		local move = (cf.LookVector * -dir.Z + cf.RightVector * dir.X)
		move = Vector3.new(move.X, 0, move.Z)
		if move.Magnitude > 0 then
			return move.Unit
		end
	end
	return Vector3.new()
end

-- Main movement loop
local lastGrounded = true
local lastY = rootPart and rootPart.Position.Y or 0

-- For velocity smoothing
local prevAppliedVelocity = Vector3.new()
local velocityLerpAlpha = 1 -- Smoothing factor (1 = instant, 0.5 = fast, 0.25 = slow)

-- Counter-strafe settings
local COUNTERSTRAFE_TAP_WINDOW = 0.18 -- seconds, max time for a tap to count as counter-strafe

RunService.RenderStepped:Connect(function(dt)
	-- If character or rootPart is missing, reacquire
	if not character or not rootPart or not rootPart.Parent then
		character, rootPart = getCharacterRoot()
		if character then
			disableHumanoid(character)
		end
		if not character or not rootPart then return end
	end

	-- Update input
	input.moveDir = getMoveDirection()
	input.crouch = crouchPressed

	-- Jump logic: Only allow jump on the frame the key is pressed (no hold-to-bhop)
	if jumpPressed and not jumpPressedLast and not jumpConsumed then
		input.jump = true
		jumpConsumed = true -- Block further jumps until key is released
	else
		input.jump = false
	end
	jumpPressedLast = jumpPressed

	-- Store previous move direction for counter-strafe
	state.prevMoveDir = state.moveDir or Vector3.new()
	state.moveDir = input.moveDir

	-- Counter-strafe tap logic: Track axis hold times and tap durations
	local now = tick()
	for _, axis in {"X", "Z"} do
		local prev = state.axisLastDir[axis] or 0
		local curr = input.moveDir[axis]
		-- If we just started holding a direction (from 0 to nonzero)
		if math.abs(prev) < 0.01 and math.abs(curr) > 0.5 then
			state.axisHoldStart[axis] = now
		end
		-- If we just released a direction (from nonzero to 0)
		if math.abs(prev) > 0.5 and math.abs(curr) < 0.01 then
			state.axisTapTime[axis] = now - (state.axisHoldStart[axis] or now)
		end
		state.axisLastDir[axis] = curr
	end

	-- Check ground
	local ray = Ray.new(rootPart.Position, Vector3.new(0, -3, 0))
	local hit, pos, norm = workspace:FindPartOnRay(ray, character)
	state.onGround = hit ~= nil and norm and norm.Y > 0.7
	state.groundNormal = norm or Vector3.new(0,1,0)

	-- Water/Ladder detection (simple placeholder)
	state.inWater = false
	state.onLadder = false

	-- Fall velocity
	state.fallVel = rootPart.Velocity.Y

	-- Landing detection
	state.landed = (not lastGrounded) and state.onGround
	lastGrounded = state.onGround

	-- Track previous onGround state for bunnyhop/counter-strafe logic
	state.wasOnGround = state.wasOnGround or true
	state.wasOnGround = state.onGround

	-- Crouch slide logic
	state.prevCrouch = state.crouching
	state.crouching = input.crouch
	SourceMovement:TryStartCrouchSlide(state, input)

	-- Apply movement
	state.vel = SourceMovement:Move(state, input, dt)

	-- --- COUNTER-STRAFE INSTANT STOP LOGIC ---
	-- If the player quickly switches direction (counter-strafe) on X or Z axis, and is on ground,
	-- forcibly zero velocity on that axis for one frame to prevent sliding.
	if state.onGround then
		for _, axis in {"X", "Z"} do
			local prev = state.prevMoveDir[axis]
			local curr = input.moveDir[axis]
			-- If the player just switched from moving in one direction to the opposite (counter-strafe)
			if math.abs(prev) > 0.5 and math.abs(curr) > 0.5 and (prev * curr) < 0 then
				-- Only trigger if the tap was quick enough (within COUNTERSTRAFE_TAP_WINDOW)
				if state.axisTapTime[axis] > 0 and state.axisTapTime[axis] <= COUNTERSTRAFE_TAP_WINDOW then
					-- Zero velocity on this axis
					state.vel = Vector3.new(
						axis == "X" and 0 or state.vel.X,
						state.vel.Y,
						axis == "Z" and 0 or state.vel.Z
					)
					-- Also forcibly zero rootPart velocity on this axis for this frame
					rootPart.Velocity = Vector3.new(
						axis == "X" and 0 or rootPart.Velocity.X,
						rootPart.Velocity.Y,
						axis == "Z" and 0 or rootPart.Velocity.Z
					)
					-- Reset tap time so it doesn't trigger again until next tap
					state.axisTapTime[axis] = 0
				end
			end
		end
	end

	-- --- INSTANT STOP OVERRIDE ---
	-- If there is no input and the player is on the ground, forcibly zero velocity to prevent sliding.
	if input.moveDir.Magnitude < 0.01 and state.onGround then
		state.vel = Vector3.new(0, 0, 0)
		rootPart.Velocity = Vector3.new(0, rootPart.Velocity.Y, 0)
		prevAppliedVelocity = rootPart.Velocity
	else
		-- Instantly set velocity for snappy control (no smoothing)
		local targetVel
		targetVel = Vector3.new(state.vel.X, rootPart.Velocity.Y, state.vel.Z)

		if input.jump and state.onGround and state.canJump then
			targetVel = Vector3.new(state.vel.X, state.vel.Y, state.vel.Z)
		end
		-- Lerp previous velocity to target velocity for smoothness (set alpha=1 for instant)
		prevAppliedVelocity = prevAppliedVelocity:Lerp(targetVel, velocityLerpAlpha)
		rootPart.Velocity = prevAppliedVelocity
	end

	-- Crouch (simulate camera offset by moving the camera down, since Humanoid is disabled)
	if input.crouch then
		-- Lower the camera by offsetting the Camera's CFrame
		local cam = workspace.CurrentCamera
		if cam then
			cam.CFrame = cam.CFrame * CFrame.new(0, -2, 0)
		end
	else
		-- No offset, camera stays at normal height
		-- (Optional: could reset camera CFrame here, but Roblox will update it next frame)
	end

	-- Clamp speed for sanity
	local flatVel = Vector3.new(rootPart.Velocity.X, 0, rootPart.Velocity.Z)
	if flatVel.Magnitude > 60 then
		local y = rootPart.Velocity.Y
		rootPart.Velocity = flatVel.Unit * 60 + Vector3.new(0, y, 0)
	end
end)

I just made insta stop for now

1 Like