Help with RAYCASTS and Projectiles -- SCRIPTING SUPPORT!

I want to try to make a projectile system that involves cloning and launching a part in the replicated storage that is meant to bounce off walls and deal damage. The client is supposed to trigger the event by holding down the mouse/screen while showing a visible path using raycasting then when you let go the part is supposed to travel the raycast line that was shown including the bouncing.

The issue with the script is that the client path is different from what the server shows when the part is released and is offset by a little but the difference can be critical at times. Also sometimes the part does not even bounce off some surfaces and completely ignores them.

Can anyone help with this problem the client and server scripts are provided below. Thanks in advance!

Server:


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ThrowRazorwingEvent = ReplicatedStorage:WaitForChild("ThrowRazorwing")
local Workspace = game:GetService("Workspace")
local Debris = game:GetService("Debris")

local damagePerBounce = 25
local MAX_BOUNCES = 3 

ThrowRazorwingEvent.OnServerEvent:Connect(function(player, startPosition, direction)

	local RazorwingClone = ReplicatedStorage:WaitForChild("Razorwing"):Clone()
	RazorwingClone.Parent = Workspace

	
	local playerCharacter = player.Character
	local playerRoot = playerCharacter and playerCharacter:FindFirstChild("HumanoidRootPart")
	if playerRoot then
		startPosition = playerRoot.Position + direction.Unit * 2
	end

	RazorwingClone.Position = startPosition
	RazorwingClone.CanCollide = true
	RazorwingClone.Anchored = false


	local BodyVelocity = Instance.new("BodyVelocity")
	BodyVelocity.Velocity = direction.Unit * 50  
	BodyVelocity.P = 1250
	BodyVelocity.MaxForce = Vector3.new(100000, 100000, 100000)
	BodyVelocity.Parent = RazorwingClone

	
	local BodyGyro = Instance.new("BodyGyro")
	BodyGyro.CFrame = CFrame.new(RazorwingClone.Position, RazorwingClone.Position + direction.Unit)
	BodyGyro.P = 3000
	BodyGyro.MaxTorque = Vector3.new(4000, 4000, 4000)
	BodyGyro.Parent = RazorwingClone

	
	local hitTable = {}
	local bounces = 0


	local runService = game:GetService("RunService")
	local connection

	connection = runService.Heartbeat:Connect(function(dt)
		
		local moveStep = BodyVelocity.Velocity * dt
		RazorwingClone.CFrame = RazorwingClone.CFrame + moveStep

		
		BodyGyro.CFrame = CFrame.new(RazorwingClone.Position, RazorwingClone.Position + BodyVelocity.Velocity.Unit)

	
		local rayLength = moveStep.Magnitude
		local ray = Ray.new(RazorwingClone.Position, moveStep.Unit * rayLength)
		local raycastParams = RaycastParams.new()
		raycastParams.IgnoreWater = true
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		raycastParams.FilterDescendantsInstances = {player.Character, RazorwingClone}

		local result = Workspace:Raycast(ray.Origin, ray.Direction, raycastParams)
		if result then
			local hit = result.Instance
			local position = result.Position
			local normal = result.Normal

			
			local incomingVelocity = BodyVelocity.Velocity
			local dotProduct = incomingVelocity:Dot(normal)
			local bounceVelocity = incomingVelocity - 2 * dotProduct * normal
			BodyVelocity.Velocity = bounceVelocity

		
			RazorwingClone.Position = position

		
			bounces = bounces + 1

			
			if bounces >= MAX_BOUNCES then
				connection:Disconnect()
			end
		end
	end)

	
	RazorwingClone.Touched:Connect(function(hit)
		
		local humanoid = hit.Parent:FindFirstChild("Humanoid")
		if humanoid and not hitTable[humanoid] and hit.Parent ~= player.Character then
			
			humanoid:TakeDamage(damagePerBounce)
			hitTable[humanoid] = true

			
			local highlight = Instance.new("Highlight")
			highlight.Parent = hit.Parent
			highlight.Adornee = hit.Parent
			highlight.FillColor = Color3.fromRGB(255, 0, 0) 
			highlight.FillTransparency = 0.5

			game.ReplicatedStorage.HitMarkerEvent:FireClient(player, hit.Parent.Head)
			Debris:AddItem(highlight, 0.5)
		end
	end)


	Debris:AddItem(RazorwingClone, 10)
end)

Client:


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local player = Players.LocalPlayer
local mouse = player:GetMouse()

local ThrowRazorwingEvent = ReplicatedStorage:WaitForChild("ThrowRazorwing")
local charging = false
local indicatorParts = {}
local reflectionEffects = {}

local MAX_BOUNCES = 3 
local PATH_LENGTH = 100


local function createIndicatorPart()
	local part = Instance.new("Part")
	part.Anchored = true
	part.CanCollide = false
	part.Size = Vector3.new(0.5, 0.5, 5)  
	part.Color = Color3.fromRGB(0, 255, 0)
	part.Material = Enum.Material.ForceField
	part.Parent = workspace
	table.insert(indicatorParts, part)
	return part
end


local function createReflectionEffect(position)
	local effect = Instance.new("Part")
	effect.Size = Vector3.new(1, 1, 1)
	effect.Shape = Enum.PartType.Ball
	effect.Material = Enum.Material.Neon
	effect.Color = Color3.fromRGB(255, 0, 0)
	effect.Anchored = true
	effect.CanCollide = false
	effect.CFrame = CFrame.new(position)
	effect.Parent = workspace
	table.insert(reflectionEffects, effect) 

	game:GetService("Debris"):AddItem(effect, 0.5)
end


local function clearIndicators()
	for _, part in ipairs(indicatorParts) do
		part:Destroy()
	end
	indicatorParts = {}

	for _, effect in ipairs(reflectionEffects) do
		effect:Destroy()
	end
	reflectionEffects = {}
end


local function showPathWithReflections()
	clearIndicators() 

	local character = player.Character
	local rootPart = character and character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	
	local currentPosition = rootPart.Position + rootPart.CFrame.LookVector * 2 
	local currentDirection = (mouse.Hit.Position - rootPart.Position).Unit

	local remainingDistance = PATH_LENGTH
	local bounces = 0

	while bounces <= MAX_BOUNCES and remainingDistance > 0 do
		
		local raycastParams = RaycastParams.new()
		raycastParams.IgnoreWater = true
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		raycastParams.FilterDescendantsInstances = {character}

		for _, part in ipairs(indicatorParts) do
			table.insert(raycastParams.FilterDescendantsInstances, part)
		end
		for _, effect in ipairs(reflectionEffects) do
			table.insert(raycastParams.FilterDescendantsInstances, effect)
		end

		local rayResult = workspace:Raycast(currentPosition, currentDirection * remainingDistance, raycastParams)

		if rayResult then
			local hitPosition = rayResult.Position
			local distanceCovered = (hitPosition - currentPosition).Magnitude
			local indicatorPart = createIndicatorPart()
			indicatorPart.CFrame = CFrame.new((currentPosition + hitPosition) / 2, hitPosition)
			indicatorPart.Size = Vector3.new(0.5, 0.5, distanceCovered)

			createReflectionEffect(hitPosition)

			local normal = rayResult.Normal
			local incomingDirection = currentDirection
			currentDirection = incomingDirection - 2 * incomingDirection:Dot(normal) * normal

			currentPosition = hitPosition
			remainingDistance = remainingDistance - distanceCovered
			bounces = bounces + 1
		else
			local endPosition = currentPosition + currentDirection * remainingDistance
			local indicatorPart = createIndicatorPart()
			indicatorPart.CFrame = CFrame.new((currentPosition + endPosition) / 2, endPosition)
			indicatorPart.Size = Vector3.new(0.5, 0.5, remainingDistance)

			break
		end
	end
end


local function throwRazorwing()
	local character = player.Character
	local rootPart = character and character:FindFirstChild("HumanoidRootPart")

	if rootPart then
		local direction = (mouse.Hit.Position - rootPart.Position).Unit
		local startPosition = rootPart.Position + rootPart.CFrame.LookVector * 2  

		
		ThrowRazorwingEvent:FireServer(startPosition, direction)
	end
end


UserInputService.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		charging = true
		showPathWithReflections()
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		charging = false
		clearIndicators()
		throwRazorwing()
	end
end)


RunService.RenderStepped:Connect(function()
	if charging then
		showPathWithReflections()
	end
end)

you’ll have to treat startPosition like an input and sanity check it (probably using magnitude / sqrmagnitude)

just always assume the position replication is a bit ahead/behind, roblox’s syncronization with that stuff is wonky (with good reason)

this bit v

if playerRoot then
	startPosition = playerRoot.Position + direction.Unit * 2
end

would look something like v

local SPAWN_DISAGREEMENT_DISTANCE:number = 10 -- you could name this better than me
if not playerRoot then return; end
if (StartPosition - playerRoot.Position).Magnitude > SPAWN_DISAGREEMENT_DISTANCE then -- you can optimize this with sqrMagnitude since the exact distance isn't needed
	--[[
 	 	if given position is too far away from the character position on the server, then...
	 	...re-calculate startPosition with server authority
 	]]
	startPosition = playerRoot.Position + direction.Unit * 2
end

you could also get more clever with your sanity check methods