NPCs pausing up randomly

I made a game were some NPCs fight each other. They fight normally at first but after a while they start to act weird and pause out of nowhere. In this video, you can see them acting fine until the 40 second mark where they start acting weird.

Here’s my code:

--services--
local PathfindingService = game:GetService("PathfindingService")
local ServerScriptService = game:GetService("ServerScriptService")
local TweenService = game:GetService('TweenService')

--npc--
local fighter = script.Parent
local humanoid = fighter.Humanoid
local torso = fighter.Torso
local head = fighter.Head
local HRP = fighter.HumanoidRootPart
local bloodAttachment = torso.Blood
local team = fighter.Team

--modules--
local modulesFolder = fighter.ModuleScripts
local modules = {
	raycastHitbox = require(ServerScriptService:FindFirstChild("RaycastHitboxV4")),
	traitsManager = require(modulesFolder.TraitsModule),
	setupManager  = require(modulesFolder.SetupModule)
}

local traits = modules.traitsManager.traits
local chosenTrait = traits[math.random(#traits)]

--weapon--
local weapon = fighter.Weapon
local weaponStats = weapon.Stats
local animationsFolder = weapon.Animations
local blockHealth = fighter.BlockHealth
local hitbox = weapon.Hitbox

--events
local gloryKillEvent = fighter.GloryKill

--status--
local status = fighter.Status
local statuses = {
	Neutral = "Neutral",
	Attacking = "Attacking",
	Blocking = "Blocking",
	Parrying = "Parrying",
	Fleeing = "Fleeing",
	Dodging = "Dodging",
	Bracing = "Bracing",
	Stunned  = "Stunned"
}

--stats--
local statDeviation = 5
local statBonus = modules.setupManager.grade()
local startingStats = {
	stamina = chosenTrait.stamina + math.random(-statDeviation, statDeviation) + statBonus,
	resistance = chosenTrait.resistance + math.random(-statDeviation, statDeviation) + statBonus,
	defense = chosenTrait.defense + math.random(-statDeviation, statDeviation) + statBonus,
	agility = chosenTrait.agility + math.random(-statDeviation, statDeviation) + statBonus,
	strength = chosenTrait.strength + math.random(-statDeviation, statDeviation) + statBonus,
	technique = chosenTrait.technique + math.random(-statDeviation, statDeviation) + statBonus,
	rage = chosenTrait.rage + math.random(-statDeviation, statDeviation) + statBonus
}

--playstyles--
local playstyles = {
	neutral = {},
	defensive = {},
	agressive = {}
}
local playstyle = playstyles.neutral

--animations--
local animations = {
	neutral = humanoid:LoadAnimation(animationsFolder.Neutral),
	block = humanoid:LoadAnimation(animationsFolder.Block),
	blocked = humanoid:LoadAnimation(animationsFolder.Blocked),
	stunned = humanoid:LoadAnimation(animationsFolder.Stunned),
	attack = {
		poke = humanoid:LoadAnimation(animationsFolder.Poke),
		chop = humanoid:LoadAnimation(animationsFolder.Chop),
		slice = humanoid:LoadAnimation(animationsFolder.Slice)
	},
	glorykill = humanoid:LoadAnimation(animationsFolder.GloryKill),
	glorydie = humanoid:LoadAnimation(animationsFolder.GloryDie)
}

--sounds--
local sounds = {
	block1 = hitbox.Block1,
	block2 = hitbox.Block2,
	break1 = hitbox.Break1,
	break2 = hitbox.Break2,
	hit1 = hitbox.Hit1,
	hit2 = hitbox.Hit2,
	swing1 = hitbox.Swing1,
	swing2 = hitbox.Swing2,
	parry = hitbox.Parry
}

--settings--
local fighterSettings = {
	engageDistance = 8, --start fighting
	attackDistance = 6,
	backupDistance = 5 --distance creation
}

--misc
local debugging = true
local cutscene = false
local maxStamina = startingStats.stamina
local processSpeed = 0.1 --faster = more efficiency, slower = less lag
local stamina = maxStamina

--setup--
modules.setupManager.setup(startingStats, chosenTrait)

--functions--
local function debugMessage(msg)
	if debugging then
		print(humanoid.DisplayName, msg)
	end
end

local function waitEvent(event: RBXScriptSignal, timeout: number?, dt: number?)
	timeout = timeout or 5
	local start = os.clock()
	local connection, data, done
	connection = event:Connect(function(...)
		data, done = {...}, true
		connection:Disconnect() 
	end)
	repeat task.wait(dt) until done or (os.clock()-start > timeout)
	if not done then connection:Disconnect() return nil end
	return table.unpack(data)
end

local function isPlayer(character)
	for _, player in pairs(game.Players:GetChildren()) do
		if player.Character == character then
			return true
		end
	end
	return false
end

local function findNearestEnemy(pos)
	local dist = math.huge
	local e
	local t
	for _, instance in pairs(workspace.Fighters:GetChildren()) do
		if instance:IsA("Model") and not isPlayer(instance) then
			if instance:FindFirstChildWhichIsA("Humanoid") and instance:FindFirstChild("Torso") and instance ~= fighter then
				if (instance.Torso.Position - pos).magnitude < dist and instance.Humanoid.Health > 0 then
					if instance:FindFirstChild("Team") then
						if (instance.Team.Value ~= team.Value or string.lower(instance.Team.Value) == "neutral") and instance.Team.Value ~= "Friend" then							
							t = instance:FindFirstChild("Torso")
							dist = (t.Position - pos).magnitude
						end
					else	
						t = instance:FindFirstChild("Torso")
						dist = (t.Position - pos).magnitude
					end
				end
			end
		end
	end
	if t then e = t.Parent end
	return e, dist
end

local function findNearbyTeammates(pos)
	local dist = 25
	local teammates = {}
	local e
	local t
	for _, instance in pairs(workspace.Fighters:GetChildren()) do
		if instance:IsA("Model")then
			if instance:FindFirstChildWhichIsA("Humanoid") and instance:FindFirstChild("Torso") and instance ~= fighter then
				if (instance.Torso.Position - pos).magnitude < dist and instance.Humanoid.Health > 0 then
					if instance:FindFirstChild("Team") then
						if instance.Team.Value == team.Value and string.lower(instance.Team.Value) ~= "neutral" then
							t = instance:FindFirstChild("Torso")
							if t.Parent then
								e = t.Parent
							end
							table.insert(teammates, t.Parent)
							dist = (t.Position - pos).magnitude
						end
					else
						return nil
					end
				end
			end
		end
	end
	return teammates, e, dist
end

local function checkForInterception(from, to)
	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = {fighter}
	rayParams.FilterType = Enum.RaycastFilterType.Exclude
	local ray = workspace:Raycast(from, to-from, rayParams)
	
	if ray then
		return ray
	else 
		return nil 
	end
end

local function pathTo(position, instance)
	local path = PathfindingService:CreatePath()
	path:ComputeAsync(torso.Position, position)
	local waypoints = path:GetWaypoints()
	
	local teammates, closestTeammate = findNearbyTeammates(torso.Position)
	local targetPos

	local function verifyPosition(position)
		if teammates == nil then return true end
		for _, teammate in pairs(teammates) do
			if (teammate.Torso.Position - position).magnitude < 5 then
				return false
			end
		end
		return true
	end
	
	local originalInstancepos
	if instance ~= nil then
		originalInstancepos = instance.Position
	end
	for _, waypoint in ipairs(waypoints) do
		if instance ~= nil then
			if checkForInterception(torso.Position,instance.Position).Instance:IsDescendantOf(instance.Parent) or checkForInterception(torso.Position,instance.Position) == nil or (originalInstancepos - instance.Position).magnitude > 10 or verifyPosition(position) == false then
				break
			end
		end
		humanoid:MoveTo(waypoint.Position)
		waitEvent(humanoid.MoveToFinished, 1.5)
	end
end

local function changeFace(face)
	local currentFace
	local newFace = head:FindFirstChild(face)
	for _, v in pairs(head:GetChildren()) do
		if v:IsA("Decal") then
			if v.Transparency == 0 then
				currentFace = v
			end
		end
	end
	currentFace.Transparency = 1
	newFace.Transparency = 0
end

local function move(direction, studs, pathfind)
	local teammates, closestTeammate = findNearbyTeammates(torso.Position)
	local targetPos
	
	local function verifyPosition(position)
		if teammates == nil then return true end
		for _, teammate in pairs(teammates) do
			if (teammate.Torso.Position - position).magnitude < 5 or checkForInterception(torso.Position, targetPos) then
				return false
			end
		end
		return true
	end
	
	if direction == "right" or direction == 2 then
		targetPos = HRP.Position + HRP.CFrame.RightVector * studs
	elseif direction == "left" or direction == 1 then
		targetPos = HRP.Position + HRP.CFrame.RightVector * -studs
	elseif direction == "forward" or direction == 3 then
		targetPos = HRP.Position + HRP.CFrame.LookVector * studs
	elseif direction == "back" or direction == 4 then
		targetPos = HRP.Position + HRP.CFrame.LookVector * -studs
	end
	if verifyPosition(targetPos) == true then
		if pathfind then
			--debugMessage('pathfinding to '..tostring(Vector3.new(math.round(targetPos.X),math.round(targetPos.Y),math.round(targetPos.Z))))
			pathTo(targetPos)
		elseif not checkForInterception(torso.Position, targetPos) then
			--debugMessage('moving straight to '..tostring(Vector3.new(math.round(targetPos.X),math.round(targetPos.Y),math.round(targetPos.Z))))
			humanoid:MoveTo(targetPos)
			humanoid.MoveToFinished:Wait()
		end
	else
		return false
	end
end

local function adjustHead(pos)
	local dist = (pos - HRP.Position).Magnitude
	local dir = (pos - HRP.Position).Unit
	local vecA = Vector2.new(dir.X, dir.Z)
	local vecB = Vector2.new(HRP.CFrame.LookVector.X, HRP.CFrame.LookVector.Z)
	local dotProd = vecA:Dot(vecB)
	local crossProd = vecA:Cross(vecB)
	local angle = math.atan2(crossProd, dotProd)
	
	local ht = pos.Y - HRP.Position.Y
	local UDA = math.atan(ht/dist)
		
	torso.Neck.C0 = CFrame.new(torso.Neck.C0.Position) * CFrame.Angles(UDA, angle, 0) * CFrame.Angles(math.rad(-90), 0, math.rad(-180))
end

local function adjustArms(pos)
	local rightShoulder = torso:FindFirstChild("Right Shoulder")

	local distArm = (pos - HRP.Position).Magnitude
	local dirArm = (pos - HRP.Position).Unit
	local vecAArm = Vector2.new(dirArm.X, dirArm.Z)
	local vecBArm = Vector2.new(HRP.CFrame.LookVector.X, HRP.CFrame.LookVector.Z)
	local dotProdArm = vecAArm:Dot(vecBArm)
	local crossProdArm = vecAArm:Cross(vecBArm)
	local angleArm = math.atan2(crossProdArm, dotProdArm)

	local htArm = pos.Y - HRP.Position.Y
	local UDAArm = math.atan(htArm/distArm)
	
	local tween = TweenService:Create(rightShoulder, TweenInfo.new(0.15, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut), {C0 = CFrame.new(rightShoulder.C0.Position) * CFrame.Angles(UDAArm, math.pi/2, UDAArm) * CFrame.Angles(UDAArm, angleArm, 0)})
	tween:Play()
	--rightShoulder.C0 = CFrame.new(rightShoulder.C0.Position) * CFrame.Angles(UDAArm, math.pi/2, UDAArm) * CFrame.Angles(UDAArm, angleArm, 0)
end

local function facePosition(pos)
	local bodyGyro = Instance.new("BodyGyro")
	bodyGyro.MaxTorque = Vector3.new(math.huge, math.huge, math.huge)
	bodyGyro.P = 3000
	bodyGyro.D = 100

	bodyGyro.CFrame = CFrame.new(fighter.Torso.Position, Vector3.new(pos.X, HRP.Position.Y, pos.Z))
	bodyGyro.Parent = fighter.HumanoidRootPart

	game.Debris:AddItem(bodyGyro, 0.1)
	adjustHead(pos)
	adjustArms(pos)
end	

local function changeStatus(newStatus)
	status.Value = newStatus
end

local function addParticle(attachment, part, duration)
	local attachmentAdd = torso:FindFirstChild(attachment):Clone()
	attachmentAdd.Parent = part
	for _, effect in pairs(attachmentAdd:GetChildren()) do
		effect.Enabled = true
	end
	game.Debris:AddItem(attachmentAdd, duration + 1)
	spawn(function()
		task.wait(duration)
		for _, effect in pairs(attachmentAdd:GetChildren()) do
			effect.Enabled = false
		end
	end)
end

local raycastHitbox = modules.raycastHitbox.new(fighter)
local function attack(attackAnim)
	if status.Value ~= "Attacking" and status.Value ~= "Blocking" and stamina > 5 then
		stamina -= 10
		local bluntDamage = weaponStats.BluntDamage.Value * ((startingStats.strength/100)+1)
		local damage  = weaponStats.SlashDamage.Value * ((startingStats.strength/100)+1)
		changeStatus(statuses.Attacking)
	
		attackAnim:Play(0.1, 1, 0.4+(startingStats.agility/80))
	
		attackAnim:GetMarkerReachedSignal("DamageActive"):Connect(function()
			raycastHitbox:HitStart()
			sounds["swing"..tostring(math.random(1,2))]:Play()
			
			raycastHitbox.OnHit:Connect(function(hit, hHum)
				raycastHitbox:HitStop()
				local e
				if hit.Parent.Name == "Weapon" then
					sounds["block"..tostring(math.random(1,2))]:Play()
					addParticle("Sparks", hit, 0.15)
				elseif hit.Parent:FindFirstChildWhichIsA("Humanoid") then
					e = hit.Parent
				elseif hit.Parent.Parent:FindFirstChildWhichIsA("Humanoid") and hit.Name ~= "Hitbox" then
					e = hit.Parent.Parent
				elseif hit.Parent.Parent.Parent:FindFirstChildWhichIsA("Humanoid") then
					e = hit.Parent.Parent.Parent
				end
				if e then
					if e:FindFirstChild("BlockHealth") then
						if e.BlockHealth.Value > 0 and e.Status.Value == "Blocking" then
							sounds["block"..tostring(math.random(1,2))]:Play()
								e.BlockHealth.Value -= startingStats.strength
								addParticle("Sparks", e.Weapon.Hitbox, 0.15)
							if e.BlockHealth.Value <= 0 then
								sounds["break"..tostring(math.random(1,2))]:Play()
								e.Humanoid:TakeDamage(startingStats.strength/2)
							end
						else
							local armor
							if hit.Name == "Armor" then
								armor = hit
							elseif hit.Parent.Name == "Armor" then
								armor = hit.Parent
							end
							if armor then
								armor:SetAttribute("HP", armor:GetAttribute("HP")-bluntDamage)
								if armor:GetAttribute("HP") > 0 then
									sounds["block"..tostring(math.random(1,2))]:Play()
								else
									sounds["break"..tostring(math.random(1,2))]:Play()
								end
							elseif hit.Name == "Head" then
								sounds.hit1:Play()
								sounds.hit2:Play()
								e.Humanoid:TakeDamage(damage * 2)
								e.Torso.TorsoToHead0:Destroy()
								addParticle("Blood", hit, 20)
							elseif hit.Name == "Torso" then
								sounds["hit"..tostring(math.random(1,2))]:Play()
								e.Humanoid:TakeDamage(damage)
							else
								sounds["hit"..tostring(math.random(1,2))]:Play()
								e.Humanoid:TakeDamage(damage *  0.75)
							end
							if not armor then
								addParticle("Blood", hit, 0.15)
							else
								addParticle("Sparks", hit, 0.15)
							end
						end
					else
						if hit.Name == "Head" then
							sounds.hit1:Play()
							sounds.hit2:Play()
							e.Humanoid:TakeDamage(damage * 2)
							e.Torso.TorsoToHead0:Destroy()
							addParticle("Blood", hit, 20)
						elseif hit.Name == "Torso" then
							sounds["hit"..tostring(math.random(1,2))]:Play()
							e.Humanoid:TakeDamage(damage)
						else
							sounds["hit"..tostring(math.random(1,2))]:Play()
							e.Humanoid:TakeDamage(damage *  0.75)
						end
						addParticle("Blood", hit, 0.15)
					end
				end
			end)
		end)
		attackAnim:GetMarkerReachedSignal("DamageInactive"):Connect(function()
			raycastHitbox:HitStop()
			task.wait(0.1)
			changeStatus(statuses.Neutral)
		end)
	end
end

local function block()
	if blockHealth.Value > 40 then
		local prevBlock = blockHealth.Value
		local canUnblock = true
		local prevSpeed = humanoid.WalkSpeed
		humanoid.WalkSpeed = 4
		animations.block:Play()
		changeStatus(statuses.Blocking)
		
		local changed
		changed = blockHealth.Changed:Connect(function(newBlock)
			if prevBlock > newBlock then
				canUnblock = false
				animations.block:Stop()
				animations.blocked:Play()
				changeStatus(statuses.Neutral)
				humanoid.WalkSpeed = prevSpeed
				changed:Disconnect()
			end
		end)
		
		task.wait(0.4)
		changed:Disconnect()
		if canUnblock then
			animations.block:Stop()
			changeStatus(statuses.Neutral)
			humanoid.WalkSpeed = prevSpeed
		end
	end
end

local function getStatus(char)
	if char:FindFirstChild("Status") then
		return char.Status.Value
	else return nil end
end

local function gloryKill(victim, killer)
	if not cutscene then
		local ff = Instance.new('ForceField', fighter)
		ff.Visible = false
		cutscene = true
		local originalTeam = team.Value
		team.Value = "Friend"
		HRP.Anchored = true
		
		if victim == fighter then
			fighter:MoveTo(killer.HumanoidRootPart.Position + killer.HumanoidRootPart.CFrame.LookVector * 5)
			HRP.CFrame = CFrame.new(HRP.Position, killer.HumanoidRootPart.Position)
			animations.glorydie:Play()
			animations.glorydie:GetMarkerReachedSignal("Hit"):Connect(function(part)
				sounds["hit"..tostring(math.random(1,2))]:Play()
				addParticle("Blood", fighter:FindFirstChild(part), 0.15)
			end)
			animations.glorydie.Ended:Wait()
			local bv = Instance.new('BodyVelocity', torso)
			bv.MaxForce = Vector3.new(1000, 1000, 100)
			HRP.Anchored = false
			bv.Velocity = torso.Position + torso.CFrame.LookVector *-5
			game.Debris:AddItem(bv, 0.2)
		else
			victim.GloryKill:Fire(victim, killer)
			animations.glorykill:Play()
			animations.glorykill.Ended:Wait()
		end
		victim.Humanoid.Health = 0
		HRP.Anchored = false
		ff:Destroy()
		cutscene = false
		team.Value = originalTeam
	end
end

--events--
gloryKillEvent.Event:Connect(function(victim, killer)
	gloryKill(victim, killer)
end)

humanoid.Died:Connect(function()
	changeFace("Dead")
	weapon.Grip:Destroy()
	weapon.Hitbox.CanCollide = true
end)

local prevH = humanoid.Health
humanoid.HealthChanged:Connect(function(newH)
	if newH - prevH < 0 and humanoid.Health > 0 then
		changeFace('Hit')
		task.wait(0.5)
		if humanoid.Health > 0 then
			changeFace('Normal')
		end
	end
end)

local prevHealth = humanoid.Health
humanoid.HealthChanged:Connect(function(newHealth)
	if newHealth < prevHealth then
		startingStats.agility += startingStats.rage/7
		startingStats.strength += startingStats.rage/8
	end
end)

--ai--
animations.neutral:Play()

coroutine.resume(coroutine.create(function()
	while task.wait(processSpeed) and humanoid.Health > 0 and not cutscene and findNearestEnemy(torso.Position) do
		local enemy, dist = findNearestEnemy(torso.Position)
		if enemy ~= nil then
			if getStatus(enemy) == "Attacking" and status.Value ~= "Attacking" and blockHealth.Value > 0 and dist < 8 then
				if math.random(1, 100) < startingStats.technique then
					move("back", math.random(2, 4))
					block()
					task.wait(0.2)
				else
					task.wait(0.8)
				end
			end
		end
	end
end))

coroutine.resume(coroutine.create(function()
	task.wait(2)
	while task.wait(0.05) and humanoid.Health > 0 and findNearestEnemy(torso.Position) do
		local enemy = findNearestEnemy(torso.Position)
		facePosition(enemy.Torso.Position)
	end
end))

coroutine.resume(coroutine.create(function()
	while task.wait(0.4) and humanoid.Health > 0 and findNearestEnemy(torso.Position) do
		if stamina < 0 then 
			stamina = 0 
		elseif stamina > maxStamina then
			stamina = maxStamina
		end
		stamina += maxStamina/100
		humanoid.WalkSpeed = 6 + (startingStats.agility/20) + (stamina/10)
	end
end))
	
while task.wait(processSpeed) and humanoid.Health > 0 and not cutscene do
	local enemy, dist = findNearestEnemy(torso.Position)
	
	debugMessage('processing')
	if enemy then
		local enemyWeakSpots = {}
		
		for _, v in pairs(enemy:GetChildren()) do
			if not v:FindFirstChild('Armor') and v:IsA('Part') then
				table.insert(enemyWeakSpots, v)
			end
		end
		
		local teammates, closestTeammate, tmDist = findNearbyTeammates(torso.Position)
	
		local eTorso = enemy.Torso
		local eHum = enemy.Humanoid
	
		local interception = checkForInterception(torso.Position,eTorso.Position)
		
		if humanoid.Health + 60 < eHum.Health then
			playstyle = playstyles.defensive
		elseif humanoid.Health - 80 > eHum.Health then
			playstyle = playstyles.agressive
		else
			playstyle = playstyles.neutral
		end
	
		if closestTeammate ~= nil then
			while (closestTeammate.Torso.Position - torso.Position).magnitude < 5 do
				move(math.random(1,4), math.random(3,6))
				task.wait(processSpeed)
			end
		end
		
		if (interception == nil or interception.Instance:IsDescendantOf(enemy)) and enemy then
			if playstyle == playstyles.neutral then
				fighterSettings.backupDistance = 4
			elseif playstyle == playstyles.defensive then
				fighterSettings.backupDistance = 5
			elseif playstyle == playstyles.agressive then
				fighterSettings.backupDistance = 3.5
			end
			
			--debugMessage('enemy is in LOS')
			if dist > fighterSettings.engageDistance then
				humanoid:MoveTo(eTorso.Position)
			elseif dist < fighterSettings.backupDistance then
				move("back", math.random(4, 7))
				if math.random(1, 2) == 1 then
					attack(animations.attack.poke)
				end
			else
				move(math.random(1, 4), math.random(3, 6))
				if dist < fighterSettings.attackDistance then
					move("forward", math.random(2, 4))
					if math.random(1, 2) == 1 then
						attack(animations.attack.chop)
					else
						attack(animations.attack.poke)
					end
				end
			end
		else
			pathTo(eTorso.Position, eTorso)
		end
	end
end
3 Likes

nvm chatgpt helped me #thirtycharacters

2 Likes

Roblox Assistant be like: am I a joke to you?

2 Likes

bluddy i dont think anybody else would :skull: thats gotta be like 4000 lines of code.

I aint reading allat :skull:

1 Like

howd you get chatgpt to help fix it?

Give chatbot an insanely high amount of detail and they work better, helped me to approach a fix to a glitch I was struggling with for a while. Don’t use the chatbot all the time.

hm, I never knew that existed haha. i should probably use that for further problems

the problem clearly has to do with the “while” loop which helps narrow the problem down to under 50 lines and trouble shoot the function that was blocking the loop through the help of chatgpt of course, however i will admit that chatgpt sucks at being specific

1 Like

ChatGPT advised me to add print statements to a bunch of lines in my while loop to see what the blockage was, which is where I figured out that my “move” function was causing the problem.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.