Inconsistent projectile ricochet?

Thanks for reading in advance.

In my little class-based fighter I’m making, one of the classes has an ability that throws a bouncing knife that damages and bleeds targets on hit. Aside from the richochet I’ve just implemented, the projectile’s behavior is working as intended. The knife is allotted ten wall-bounces before it’s destroyed on the next impact with a solid object - however, occasionally when throwing the knife it will either fail to register a raycast and destroy itself, or reflect in the same spot nigh instantaneously and use up the remaining bounces without actually moving.

local function projectile(cf, owner, ignore)
		local p = game.Players:GetPlayerFromCharacter(owner)
		local id = generateKey(7)
		
		local hitdb = {}
		local con = nil; local hit = nil
		
		local knife = newPart(workspace, "Knife", size.X, size.Y, size.Z, 1, 0, false, false)
		knife.CFrame = cf
			
		if showBoxes then knife.Material = Enum.Material.Neon
			knife.BrickColor = bcn("Really red"); knife.Transparency = .5 
		end
			
		local o = knife.CFrame.p
		local v = knife.CFrame.lookVector * speed
			
		local bv = i_n("BodyVelocity")
		bv.MaxForce = v3(math.huge, math.huge, math.huge)
		bv.Velocity = v; bv.Parent = knife
		
		pjadd:FireServer(id, o, v.unit, speed)
		owner = owner or char
			
		if owner ~= char then hitfx = nil end
			
		knife.Touched:connect(function(hit)
			local targ = hit.Parent
			local hume = targ:FindFirstChild("Humanoid")
			local ac = (p and isAlly(targ, p.TeamColor))
				
			local f = vol + (pms/20)
			f = math.min(f, vol * 1.5)
				
			if not (hit:IsDescendantOf(owner) or hit:IsDescendantOf(cam)) and hit ~= ignore then
				if hit.Name:lower() == "reflector" then local ref = hit:FindFirstChildWhichIsA("ObjectValue")
					if ref then ref = ref.Value; else ref = hit end
						
					local new = cfn(knife.Position, knife.Position + knife.CFrame.lookVector * -1)
					knife:Destroy(); effect("clear", key); projectile(new, ref, hit)
					
				elseif hit.CanCollide and not hume then print(id)
					local normal = knife.CFrame.lookVector
					local pos = knife.CFrame.p
					
					local wr = ray(pos, normal * 100)
					local hit, pos, sNorm = workspace:FindPartOnRayWithIgnoreList(wr, {char, cam, ignore})
					
					if hit and rico < 10 then rico = rico + 1
						local ref = normal - (2 * normal:Dot(sNorm) * sNorm)
						local new = cfn(knife.Position, knife.Position + ref)
						knife:Destroy(); effect("clear", key)
						projectile(new, owner, hit)
						
					else knife:Destroy(); effect("clear", key)
					end
						
				elseif hume and not hitdb[targ] and isAlive(targ) and not ac then hitdb[targ] = true
					if checkStat(targ, "Reflect") then 
						local new = cfn(knife.Position, knife.Position + knife.CFrame.lookVector * -1)
						knife:Destroy(); effect("clear", key); projectile(new, targ, hit)
							
					else local loc = hit.Parent:FindFirstChild("Torso") or hrp
						hitfx.bleed = {targ = targ, amt = 100, dur = 2}
						bjunc(targ, knife.CFrame.p, id, f, dmg, hitfx, 3)
						common("sound", {id = 160432334, v = 1, pos = loc})
					end
				end
			end
		end)
			
		con = reflect.OnClientEvent:connect(function(k, ref)
			if k == key then con:disconnect()
				local new = cfn(knife.Position, knife.Position + knife.CFrame.lookVector * -1)
				knife:Destroy(); effect("clear", key); projectile(new, ref, hit)
			end
		end)
			
		knife.AncestryChanged:connect(function()
			con:disconnect()
		end)
			
		effect("knife", {cf = knife.CFrame, col = tCol, d = v.unit, s = speed, lt = lt, key = key})
		game:GetService("Debris"):AddItem(knife, lt)
	end

For clarification, ‘cf’ is the origin CFrame where the projectile is created, ‘owner’ is the character (or reflector, sometimes) that shot the projectile, and ‘ignore’ is a situational argument that is used whenever the projectile is reflected that prevents the newly created reflection from triggering a hit event against the part that reflected it.

When projectiles in my game are reflected, due to the nature of visual-only replication I’m using, I don’t alter the trajectory of an existing part; I delete the old projectile and use recursion to create a new one from the position the previous projectile was reflected from, as you’ll see by examining this function.

As previously mentioned, the projectile’s behavior is completely functional aside from the ricochet, which is contained within this snippet:

elseif hit.CanCollide and not hume then print(id)
	local normal = knife.CFrame.lookVector
	local pos = knife.CFrame.p
					
	local wr = ray(pos, normal * 100)
	local hit, pos, sNorm = workspace:FindPartOnRayWithIgnoreList(wr, {char, cam, ignore})
					
	if hit and rico < 10 then rico = rico + 1
		local ref = normal - (2 * normal:Dot(sNorm) * sNorm)
		local new = cfn(knife.Position, knife.Position + ref)
		knife:Destroy(); effect("clear", key)
		projectile(new, owner, hit)
						
	else knife:Destroy(); effect("clear", key)
	end

The ‘projectile’ function is localized to the ability function itself, thus explaining the absence of the ‘rico’ variable in the chunk I provided. I can post the entire ability function if needed. If anyone has any explanation for the odd behavior, I’d appreciate your time.

5 Likes