A part of the AI System doesn't work for whatever reason

Hi there!
I’ve been trying to make some sort of an enemy AI that dodges at random intervals, making it harder to fight against.

However, I’ve recently ran into a problem where the enemy doesn’t move, but no error occurs.

Code:

local dist = 20

local refreshRate = 0.25

local tracks = {}

local debounce = false
local dodgeDebounce = false
local particleDebounce = false
local smokeDebounce = false -- for specific function
local soundDebounce = false

local currentBehaviour = "Idle"
local oldBehaviour = ""

local BaseDamage = 20
local DamageAmplifier = 1
local debounceTimerAmplifier = 1
local ForcefieldResistance = 2
local cooldownDuration = 0.2315
local dodgePower = 50000
local dodgePower2 = dodgePower / 7

local dodgeChance = 3

local SlashNumber = 1

local lastActionTime = 0
local resetTimeThreshold = 5

local introThread = nil
local idleThread = nil
local chaseThread = nil
local slashThread = nil

local isSlashing = false
local isDodging = false
local canSwitchAnims = false
local canDodge = true

local Chat = game:GetService("Chat")
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
local RunService = game:GetService("RunService")

local Enemy = script.Parent.Parent.Parent
local Humanoid = Enemy.Humanoid
local Torso = Enemy.Torso

local Animations = Enemy.Animations
local Sounds = Enemy.Sounds
local Particles = Enemy.Particles

local function playAnimation(animationName, humanoid, speed, loop)
	local animation = Animations:FindFirstChild(animationName)
	if animation then
		if not humanoid or not animation then
			return
		end

		task.synchronize()
		local animationTrack = humanoid:LoadAnimation(animation)
		if animationTrack then
			animationTrack.Priority = Enum.AnimationPriority.Action
			animationTrack:AdjustSpeed(speed)
			animationTrack.Looped = loop
			animationTrack:Play()
			task.desynchronize()

			table.insert(tracks, animationTrack)
		end
	end
end

local function stopAllAnimations()
	coroutine.wrap(function()
		for i = #tracks, 1, -1 do
			local track = tracks[i]
			if track.IsPlaying then
				task.synchronize()
				track:Stop()
				task.desynchronize()
			end
			table.remove(tracks, i)
		end
	end)()
end

local function playParticle(particleName, handle, rate, lifetime)
	if particleDebounce then return end
	particleDebounce = true

	local particle = Particles:FindFirstChild(particleName)
	if particle then
		task.synchronize()

		local particleClone = particle:Clone()
		particleClone.Parent = handle
		particleClone:Emit(rate)
		Debris:AddItem(particleClone, lifetime)

		task.desynchronize()
	end

	task.delay(lifetime / 2, function()
		particleDebounce = false
	end)
end

local function playSound(soundName, handle)
	if soundDebounce then return end

	local sound = Sounds:FindFirstChild(soundName)
	if sound and not soundDebounce then
		task.synchronize()
		soundDebounce = not soundDebounce

		local soundClone = sound:Clone()
		soundClone.Parent = handle
		soundClone:Play()
		Debris:AddItem(soundClone, soundClone.TimeLength)
		task.wait(soundClone.TimeLength / 2)
		soundDebounce = not soundDebounce
		task.desynchronize()
	end
end

local function createHitbox(enemy, waiter, handle, size, dmg)
	task.delay(waiter, function()
		task.synchronize()
		local HitboxRegion = Region3.new(handle.Position - (size / 2), handle.Position + (size / 2))
		local partsInHitbox = workspace:FindPartsInRegion3(HitboxRegion, nil, math.huge)
		task.desynchronize()

		local npcs = {}

		for _, part in pairs(partsInHitbox) do
			local hum = part.Parent:FindFirstChildOfClass("Humanoid")
			task.synchronize()
			if hum and part.Parent.Name ~= Enemy.Name and part.Parent.Name == Players:GetPlayerFromCharacter(part.Parent).Name and hum.Health > 0 and Humanoid.Health > 0 and part.Parent ~= nil then
				table.insert(npcs, hum.Parent)
				local hasForceField = hum.Parent:FindFirstChild("ForceField") ~= nil

				local constantDamage = math.floor(dmg)

				if hasForceField then
					constantDamage = constantDamage / ForcefieldResistance
				end

				hum:TakeDamage(constantDamage)
				playParticle("BloodHit", hum.Parent:FindFirstChild("Torso") or hum.Parent:FindFirstChild("UpperTorso"), 12.5, 1)

				local hitSound = "Hit" .. SlashNumber
				playSound(hitSound, Enemy.Sword.Handle)

				task.synchronize()
				if hasForceField then
					hum.WalkSpeed = hum.WalkSpeed / (3 - (1.5 * (ForcefieldResistance - 0.25)))
				else
					hum.WalkSpeed = hum.WalkSpeed / (3 - 1.5)
				end
				hum.JumpPower = 0

				task.wait(0.5)

				if hasForceField then
					hum.WalkSpeed = hum.WalkSpeed * (3 - (1.5 * (ForcefieldResistance - 0.25)))
				else
					hum.WalkSpeed = hum.WalkSpeed * (3 - 1.5)
				end
				hum.JumpPower = 50
				break
			end
		end
		task.desynchronize()

		for i = #npcs, 1, -1 do
			table.remove(npcs, i)
		end
	end)
end

local function findClosestPlayer()
	local closestPlayer = nil
	local closestDistance = math.huge

	for _, player in pairs(Players:GetPlayers()) do
		local character = player.Character
		if character and character:FindFirstChild("HumanoidRootPart") and character:FindFirstChild("Humanoid") then
			local distance = (character.HumanoidRootPart.Position - Enemy.HumanoidRootPart.Position).Magnitude
			if distance < closestDistance then
				closestDistance = distance
				closestPlayer = player
			end
		end
	end

	return closestPlayer, closestDistance
end

local function ManipulateJump(Hum: Humanoid)
	coroutine.wrap(function()
		task.synchronize()
		Hum.JumpPower = Hum.JumpPower / 4
		Hum.Jump = true
		task.wait(0.1)
		Hum.JumpPower = Hum.JumpPower * 4
		task.desynchronize()
	end)()
end

local function CreateSmoke(part: BasePart)
	if smokeDebounce then return end

	coroutine.wrap(function()
		task.synchronize()
		smokeDebounce = not smokeDebounce

		local Smoke = Instance.new("Smoke")
		Smoke.Parent = part
		Smoke.Enabled = true
		task.wait(5/10)
		Smoke.Enabled = false
		Debris:AddItem(Smoke, 5)
		task.wait(2.5)
		smokeDebounce = not smokeDebounce
		task.desynchronize()
	end)()
end

local function Dodge()
	if dodgeDebounce == true or isSlashing == true or isDodging == true or Humanoid.Health == 0 or canDodge == false then return end

	if Humanoid and Torso and not dodgeDebounce then
		coroutine.wrap(function()
			task.synchronize()

			isDodging = true
			dodgeDebounce = true

			local moveDirection = Humanoid.MoveDirection
			local targetPosition

			if moveDirection.Magnitude > 0 then
				targetPosition = Torso.Position + (moveDirection * dodgePower) + Vector3.new(0, 35 * (dodgePower / 5000), 0)
			else
				local lookVector = Torso.CFrame.LookVector
				targetPosition = Torso.Position + (lookVector * dodgePower) + Vector3.new(0, 35 * (dodgePower / 5000), 0)
			end

			ManipulateJump(Humanoid)

			local BodyPosition = Instance.new("BodyPosition")
			BodyPosition.Parent = Torso
			BodyPosition.Position = targetPosition
			BodyPosition.MaxForce = Vector3.new(4000 * (dodgePower2 / 5000), 4000 * (dodgePower2 / 5000), 4000 * (dodgePower2 / 5000))
			BodyPosition.P = 100000 * (dodgePower / 5000)

			coroutine.wrap(function()
				playSound("Dodge", Torso)
				playParticle("DodgeParticle", Torso, 100, 1.5)
				CreateSmoke(Torso)
			end)()

			Debris:AddItem(BodyPosition, 0.1)

			task.wait(2.5)

			dodgeDebounce = false
			isDodging = false

			task.desynchronize()
		end)()
	end
end

local function Backflip()
	if dodgeDebounce == true or isSlashing == true or isDodging == true or Humanoid.Health == 0 or canDodge == false then return end

	if Humanoid and Torso and not dodgeDebounce then
		coroutine.wrap(function()
			task.synchronize()

			isDodging = false
			dodgeDebounce = false

			local moveDirection = -Humanoid.MoveDirection
			local targetPosition

			if moveDirection.Magnitude > 0 then
				targetPosition = Torso.Position + (moveDirection * dodgePower) + Vector3.new(0, 35 * (dodgePower / 5000), 0)
			else
				local lookVector = -Torso.CFrame.LookVector
				targetPosition = Torso.Position + (lookVector * dodgePower) + Vector3.new(0, 35 * (dodgePower / 5000), 0)
			end

			ManipulateJump(Humanoid)

			local BodyPosition = Instance.new("BodyPosition")
			BodyPosition.Parent = Torso
			BodyPosition.Position = targetPosition
			BodyPosition.MaxForce = Vector3.new(4000 * (dodgePower2 / 5000), 4000 * (dodgePower2 / 5000), 4000 * (dodgePower2 / 5000))
			BodyPosition.P = 100000 * (dodgePower / 5000)

			coroutine.wrap(function()
				playSound("Dodge", Torso)
				playParticle("DodgeParticle", Torso, 100, 1.5)
				CreateSmoke(Torso)
			end)()

			Debris:AddItem(BodyPosition, 0.1)

			task.wait(2.5)

			dodgeDebounce = false
			isDodging = false

			task.desynchronize()
		end)()
	end
end

local function AnimationHandler(behaviour: string)
	if behaviour == "Idle" or behaviour == "Intro" then
		stopAllAnimations()
		playAnimation("Idle", Humanoid, 0.5, true)
	elseif behaviour == "Chase" then
		stopAllAnimations()
		playAnimation("Walk", Humanoid, 1, true)
	elseif behaviour:match("^Slash%d$") then
		stopAllAnimations()
		playAnimation(behaviour, Humanoid, 1, false)
	elseif behaviour == "Freefall" then
		stopAllAnimations()
		playAnimation("Freefall", Humanoid, 1, true)
	else
		stopAllAnimations()
	end
end

local function lookAtGyro(part1: BasePart, part2: BasePart)
	coroutine.wrap(function()
		local bodyGyro = Instance.new("BodyGyro")
		bodyGyro.Parent = part1

		bodyGyro.D = 450
		bodyGyro.P = 120000
		bodyGyro.MaxTorque = Vector3.one * 400000

		local targetPosition = Vector3.new(part2.Position.X, part1.Position.Y, part2.Position.Z)

		local thread = coroutine.create(function()
			RunService.Heartbeat:ConnectParallel(function()
				if Enemy.Humanoid.Health > 0 then
					bodyGyro.CFrame = CFrame.new(part1.Position, targetPosition)
				end
			end)
		end)
		if Enemy.Humanoid.Health > 0 then
			task.wait(0.25)
			coroutine.close(thread)
			bodyGyro:Destroy()
		else
			coroutine.close(thread)
			bodyGyro:Destroy()
		end
	end)()
end

local function resetSlashNumber()
	SlashNumber = 1
	lastActionTime = tick()
end

local function AIHandler()
	idleThread = coroutine.create(function()
		Humanoid:MoveTo(Enemy.Torso.Position)

		while true do
			if currentBehaviour == "Idle" then
				local closestPlayer, closestDistance = findClosestPlayer()
				if closestPlayer and closestDistance <= dist and closestPlayer.Character.Humanoid.Health > 0 then
					currentBehaviour = "Intro"
					canDodge = true
				end
			end
			task.wait(refreshRate)
		end
	end)

	introThread = coroutine.create(function()
		while true do
			if currentBehaviour == "Intro" then
				Chat:Chat(Enemy.Head, "die.")
				currentBehaviour = "Chase"
			end
			task.wait(refreshRate)
		end
	end)

	chaseThread = coroutine.create(function()
		while true do
			if currentBehaviour == "Chase" then
				local closestPlayer, closestDistance = findClosestPlayer()
				if closestPlayer then
					local character = closestPlayer.Character
					if character and character:FindFirstChild("Humanoid") and character.Humanoid.Health > 0 then
						Humanoid:MoveTo(character.HumanoidRootPart.Position)
						canDodge = true
					else
						currentBehaviour = "Idle"
						canDodge = true
					end

					if closestDistance > dist * 1.5 then
						Dodge()
						canDodge = true
					elseif closestDistance > dist * 4 then
						currentBehaviour = "Idle"
						canDodge = false
					elseif closestDistance < dist / 3.45 then
						canDodge = false
						local Chance = math.random(1, dodgeChance*2)

						if Chance == dodgeChance then
							coroutine.wrap(function()
								canDodge = true
								Backflip()
								task.wait(0.5)
								canDodge = false
							end)()
						else
							currentBehaviour = "Slash" .. SlashNumber
						end
					end
				else
					currentBehaviour = "Idle"
					canDodge = true
				end
			end
			task.wait(0.25)
		end
	end)

	slashThread = coroutine.create(function()
		if isDodging or Humanoid.Health == 0 then return end

		while true do
			if currentBehaviour:match("^Slash%d$") and Enemy.Sword.Parent ~= nil and Humanoid.Health > 0 then
				local closestPlayer, closestDistance = findClosestPlayer()
				if closestPlayer then
					local character = closestPlayer.Character
					if character and character:FindFirstChild("Humanoid") and character.Humanoid.Health > 0 then
						local Mag = (character.HumanoidRootPart.Position - Enemy.HumanoidRootPart.Position).Magnitude
						local targetPosition = Vector3.new(closestPlayer.Character.Torso.Position.X, Enemy.HumanoidRootPart.Position.Y, closestPlayer.Character.Torso.Position.Z)

						if debounce then return end
						task.synchronize()
						Humanoid:MoveTo(character.HumanoidRootPart.Position)
						Enemy.HumanoidRootPart.CFrame = CFrame.new(Enemy.HumanoidRootPart.Position, targetPosition)
						task.desynchronize()
						local currentTime = tick()
						if currentTime - lastActionTime < cooldownDuration then
							return
						end

						debounce = true
						lastActionTime = currentTime
						isSlashing = true

						stopAllAnimations()

						if SlashNumber == 1 then
							DamageAmplifier = 0.75
							debounceTimerAmplifier = 1
						elseif SlashNumber == 2 then
							DamageAmplifier = 1
							debounceTimerAmplifier = 1
						elseif SlashNumber == 3 then
							DamageAmplifier = 1.25
							debounceTimerAmplifier = 1.5
						end

						playAnimation(tostring("Slash" .. SlashNumber), Humanoid, 0.5, false)
						playSound(tostring("Swing" .. SlashNumber), Enemy.Sword.Handle)
						createHitbox(Humanoid, 0.3, Enemy.Sword.Handle, Enemy.Sword.Handle.Size * 2.5, BaseDamage * DamageAmplifier)
						task.wait(cooldownDuration * debounceTimerAmplifier)
						debounce = false
						isSlashing = false

						SlashNumber = SlashNumber % 3 + 1

						if Mag > dist * 4 or character.Humanoid.Health <= 0 then
							currentBehaviour = "Idle"
						else
							currentBehaviour = "Chase"
						end
					else
						currentBehaviour = "Idle"
					end
				else
					currentBehaviour = "Idle"
				end
			end
			task.wait(refreshRate)
		end
	end)

	coroutine.resume(introThread)
	coroutine.resume(idleThread)
	coroutine.resume(chaseThread)
	coroutine.resume(slashThread)
end

local function AnimationHandlerSystem()
	coroutine.wrap(function()
		while not canSwitchAnims do
			if oldBehaviour ~= currentBehaviour then
				canSwitchAnims = not canSwitchAnims
				oldBehaviour = currentBehaviour
			end
			if canSwitchAnims then
				AnimationHandler(currentBehaviour)
				canSwitchAnims = not canSwitchAnims
			end
			task.wait(refreshRate)
		end
	end)()
end

local function EnemySystem()
	while Humanoid.Health > 0 do
		AIHandler()
		AnimationHandlerSystem()
		task.wait(refreshRate)
		if Humanoid.Health <= 0 then
			currentBehaviour = "" -- Dead state
			AnimationHandlerSystem()
		end
	end

	if currentBehaviour == "" then
		task.synchronize()
		Enemy.Sword:Destroy()
		task.desynchronize()

		coroutine.close(idleThread)
		coroutine.close(chaseThread)
		coroutine.close(slashThread)
	end
end

task.desynchronize()
EnemySystem()

Of course, it uses parallel lua for performance reasons.
The SignalBehaviour is also deferred (tho will be default because of the 3d skybox).

I’ve tried tracking it down, but to no avail.
I don’t know if it has something to do with the way I manage states or if it’s just how it is, but I don’t want a hacky way of solving this (if it isn’t absolutely necessary).

Feel free to improve the code also, as this is the first time I’ve used parallel lua for this.

Thank you!

2 Likes