Need help with boss AI

Hello, I have been trying to make a boss AI for a game I’m making. But I ran into some issues as of lately regarding my code.

I need him to be able to actually walk and have awareness of what the players doing. It’s written there but it doesnt work for some reason the AI just keeps bugging out and pauses or does something incredibly stupid.

Here is an example of what I’m trying to explain:

I’ve been going at this for a week trying to tackle it with machine learning, trying to get support from many sources but to no avail they just keep failing in diffrent ways.

If there are any sources or tutorials I could follow spesifically for this it would be great. I’ve left the script below if you want to check it out (I doubt it it’s like 300 lines) but feel free to use it for yourself if it works for you but it’s really broken and I couldn’t fix it thats why I’m asking for support.
Also I don’t mind starting from scratch so I’m open to any method.

local boss = script.Parent
local humanoid = boss:WaitForChild("Humanoid", 5)
local rootPart = boss:WaitForChild("HumanoidRootPart", 5)
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Debris = game:GetService("Debris")

if not humanoid or not rootPart then
	warn("Boss missing Humanoid or HumanoidRootPart")
	return
end

-- Reference the Linked Sword
local sword = boss:WaitForChild("LinkedSword", 5)
if not sword then
	warn("LinkedSword not found in boss model")
	return
end

-- Skills module
local success, Skills = pcall(function()
	return require(game.ServerStorage.SkillsModule)
end)
if not success then
	warn("Failed to load SkillsModule: " .. tostring(Skills))
	return
end

-- Initialize variables
local originalWalkSpeed = 20 -- Faster than player (16 studs/s)
humanoid.WalkSpeed = originalWalkSpeed
local turnSpeed = 0.8
local predictionTime = 0.6
local currentDirection = rootPart.CFrame.LookVector
local lastSwingTime = 0
local swingCooldown = 0.4
local lastDashTime = 0
local dashCooldown = 1
local isDashing = false
local minDistance = 6
local optimalDistance = 8
local lastActionTime = 0
local actionCooldown = 0.5
local playerDamageTracker = {}
local lastDamageTime = 0
local lastAttack = 0
local toolEquipped = false

-- Sword gear variables
local DamageValues = {
	BaseDamage = 5,
	SlashDamage = 10,
	LungeDamage = 30,
}
local Damage = DamageValues.BaseDamage

-- Sword gear functions
local function SwordUp()
	sword.Grip = CFrame.new(0, 0, -1.5, 0, 0, 1, 1, 0, 0, 0, 1, 0)
end

local function SwordOut()
	sword.Grip = CFrame.new(0, 0, -1.5, 0, -1, 0, -1, 0, 0, 0, 0, -1)
end

local function TagHumanoid(humanoid, player)
	local Creator_Tag = Instance.new("ObjectValue")
	Creator_Tag.Name = "creator"
	Creator_Tag.Value = player
	Debris:AddItem(Creator_Tag, 2)
	Creator_Tag.Parent = humanoid
end

local function UntagHumanoid(humanoid)
	for _, v in pairs(humanoid:GetChildren()) do
		if v:IsA("ObjectValue") and v.Name == "creator" then
			v:Destroy()
		end
	end
end

local function Attack()
	Damage = DamageValues.SlashDamage
	local slashSound = sword:WaitForChild("Handle"):FindFirstChild("Slash")
	if slashSound then slashSound:Play() end
	local anim = Instance.new("StringValue")
	anim.Name = "toolanim"
	anim.Value = "Slash"
	anim.Parent = sword
	print("Boss used Slash (10 damage)")
end

local function Lunge()
	Damage = DamageValues.LungeDamage
	local lungeSound = sword:WaitForChild("Handle"):FindFirstChild("Lunge")
	if lungeSound then lungeSound:Play() end
	local anim = Instance.new("StringValue")
	anim.Name = "toolanim"
	anim.Value = "Lunge"
	anim.Parent = sword
	local force = Instance.new("BodyVelocity")
	force.Velocity = (target.Character.HumanoidRootPart.Position - rootPart.Position).Unit * 40 + Vector3.new(0, 10, 0)
	force.MaxForce = Vector3.new(1e5, 1e5, 1e5)
	force.P = 1e4
	force.Parent = rootPart
	Debris:AddItem(force, 0.3)
	createWhiteSilhouettes(boss)
	task.spawn(function()
		task.wait(0.25)
		SwordOut()
		task.wait(0.75)
		SwordUp()
	end)
	print("Boss used Lunge (30 damage)")
end

local function Blow(Hit)
	if not Hit or not Hit.Parent or not humanoid or humanoid.Health <= 0 then
		return
	end
	local character = Hit.Parent
	local victimHumanoid = character:FindFirstChildOfClass("Humanoid")
	if not victimHumanoid then
		return
	end
	local player = Players:GetPlayerFromCharacter(character)
	if player then
		UntagHumanoid(victimHumanoid)
		TagHumanoid(victimHumanoid, nil)
		victimHumanoid:TakeDamage(Damage)
	end
end

local function Activated()
	if not toolEquipped or humanoid.Health <= 0 or tick() - lastSwingTime < swingCooldown then
		return
	end
	lastSwingTime = tick()
	lastActionTime = tick()
	local tickTime = tick()
	if (tickTime - lastAttack) < 0.4 then
		Lunge()
	else
		Attack()
	end
	Damage = DamageValues.BaseDamage
	lastAttack = tickTime
end

local function Equipped()
	if humanoid.Health <= 0 then
		return
	end
	toolEquipped = true
	local unsheathSound = sword:WaitForChild("Handle"):FindFirstChild("Unsheath")
	if unsheathSound then unsheathSound:Play() end
end

-- White silhouettes for lunge and dash
local function createWhiteSilhouettes(character)
	for _, part in pairs(character:GetChildren()) do
		if part:IsA("BasePart") and part.Name ~= "HumanoidRootPart" then
			local silhouette = part:Clone()
			silhouette.Color = Color3.fromRGB(255, 255, 255)
			silhouette.Material = Enum.Material.SmoothPlastic
			silhouette.CanCollide = false
			silhouette.Transparency = 0.9
			silhouette.Anchored = true
			silhouette.Parent = workspace
			Debris:AddItem(silhouette, 0.125)
		end
	end
end

-- Dash function
local function dash(direction)
	if isDashing or tick() - lastDashTime < dashCooldown then return end
	isDashing = true
	lastDashTime = tick()
	local bv = Instance.new("BodyVelocity")
	bv.Velocity = direction * 40
	bv.MaxForce = Vector3.new(1e5, 0, 1e5)
	bv.P = 1e4
	bv.Parent = rootPart
	Debris:AddItem(bv, 0.2)
	createWhiteSilhouettes(boss)
	task.wait(0.2)
	isDashing = false
	print("Boss dashed")
end

-- Initialize sword
SwordUp()
sword:WaitForChild("Handle").Touched:Connect(Blow)
Equipped()

-- Track player aggression
local function updatePlayerAggression(player, damage)
	if not playerDamageTracker[player] then
		playerDamageTracker[player] = { damage = 0, lastHit = tick() }
	end
	playerDamageTracker[player].damage = playerDamageTracker[player].damage + damage
	playerDamageTracker[player].lastHit = tick()
end

-- Find nearest player
local function getNearestPlayer()
	local closestPlayer = nil
	local minDistance = math.huge
	local maxAggression = 0
	local lowestHealth = math.huge
	for _, player in ipairs(Players:GetPlayers()) do
		if player.Character and player.Character:FindFirstChild("HumanoidRootPart") and player.Character:FindFirstChildOfClass("Humanoid") and player.Character.Humanoid.Health > 0 then
			local distance = (rootPart.Position - player.Character.HumanoidRootPart.Position).Magnitude
			local aggression = playerDamageTracker[player] and (playerDamageTracker[player].damage / (tick() - playerDamageTracker[player].lastHit + 1)) or 0
			local health = player.Character.Humanoid.Health
			if health < lowestHealth or (health == lowestHealth and aggression > maxAggression) or (health == lowestHealth and aggression == maxAggression and distance < minDistance) then
				lowestHealth = health
				maxAggression = aggression
				minDistance = distance
				closestPlayer = player
			end
		end
	end
	return closestPlayer
end

-- Get player distance
local function getPlayerDistance(target)
	local origin = rootPart.Position
	local targetPos = target.Character.HumanoidRootPart.Position
	local direction = (targetPos - origin)
	local ray = Ray.new(origin, direction.Unit * 50)
	local hit, position = workspace:FindPartOnRayWithIgnoreList(ray, {boss, target.Character})
	if hit and hit:IsDescendantOf(target.Character) then
		return (position - origin).Magnitude
	end
	return direction.Magnitude
end

-- Smooth movement
local function smoothSeek(targetPosition)
	local direction = (targetPosition - rootPart.Position)
	local distance = direction.Magnitude
	if distance < minDistance then
		direction = -direction.Unit * (minDistance - distance)
	end
	local desiredDirection = direction.Unit
	currentDirection = currentDirection:Lerp(desiredDirection, turnSpeed)
	humanoid:Move(currentDirection)
end

-- Choose best skill
local function getBestSkill(distance, healthPercent, aggression)
	local isAggressive = aggression > 1
	local isDefensive = distance > 15 or (tick() - lastDamageTime > 5)
	if healthPercent < 0.3 then
		if distance < 5 and not Skills.isOnCooldown(boss, "IceBurst") then
			print("Chose IceBurst (low health, close)")
			return "IceBurst"
		end
		return nil
	end
	if isAggressive and distance < 5 and not Skills.isOnCooldown(boss, "IceBurst") then
		print("Chose IceBurst (aggressive player, close)")
		return "IceBurst"
	elseif isDefensive and distance > 10 and not Skills.isOnCooldown(boss, "IlluminaLunge") then
		print("Chose IlluminaLunge (defensive player, far)")
		return "IlluminaLunge"
	elseif distance < 5 and not Skills.isOnCooldown(boss, "FirebrandSlash") then
		print("Chose FirebrandSlash (close range)")
		return "FirebrandSlash"
	elseif distance < 15 and not Skills.isOnCooldown(boss, "WitherSlash") then
		print("Chose WitherSlash (mid range)")
		return "WitherSlash"
	elseif not Skills.isOnCooldown(boss, "IlluminaLunge") then
		print("Chose IlluminaLunge (default)")
		return "IlluminaLunge"
	elseif not Skills.isOnCooldown(boss, "IceBurst") then
		print("Chose IceBurst (default)")
		return "IceBurst"
	elseif not Skills.isOnCooldown(boss, "FirebrandSlash") then
		print("Chose FirebrandSlash (default)")
		return "FirebrandSlash"
	elseif not Skills.isOnCooldown(boss, "WitherSlash") then
		print("Chose WitherSlash (default)")
		return "WitherSlash"
	end
	return nil
end

-- Track damage
humanoid.HealthChanged:Connect(function(health)
	if health < humanoid.Health then
		local damage = humanoid.Health - health
		local closestPlayer = getNearestPlayer()
		if closestPlayer then
			updatePlayerAggression(closestPlayer, damage)
			lastDamageTime = tick()
		end
	end
end)

-- React to player sword swings
game.ReplicatedStorage.SwordSwingEvent.OnServerEvent:Connect(function(player)
	print("Player swung sword:", player.Name)
	if player == target then
		local directionAway = (rootPart.Position - target.Character.HumanoidRootPart.Position).Unit
		dash(directionAway)
	end
end)

-- Main loop
local target = nil
while true do
	if not humanoid or humanoid.Health <= 0 then
		print("Boss is dead or missing humanoid")
		break
	end
	target = getNearestPlayer()
	if not target or not target.Character or not target.Character:FindFirstChild("HumanoidRootPart") then
		humanoid.WalkSpeed = originalWalkSpeed
		print("No valid target found")
		RunService.Heartbeat:Wait()
		continue
	end
	local distance = getPlayerDistance(target)
	local healthPercent = humanoid.Health / humanoid.MaxHealth
	local aggression = playerDamageTracker[target] and (playerDamageTracker[target].damage / (tick() - playerDamageTracker[target].lastHit + 1)) or 0
	print("Target:", target.Name, "Distance:", distance, "Aggression:", aggression, "ActionTime:", tick() - lastActionTime, "MoveActive:", boss:GetAttribute("MoveActive"), "Stunned:", boss:GetAttribute("Stunned"), "IsDashing:", isDashing)
	local toPlayer = (target.Character.HumanoidRootPart.Position - rootPart.Position)
	local circleDirection = Vector3.new(-toPlayer.Z, 0, toPlayer.X).Unit * (math.random() < 0.5 and 1 or -1)
	local targetPosition = rootPart.Position + circleDirection * optimalDistance
	local idealDistance = (distance < 5 and minDistance) or (distance < 15 and optimalDistance) or 10
	if distance < 5 then
		targetPosition = rootPart.Position - toPlayer.Unit * (minDistance - distance)
		if math.random() < 0.3 then
			dash(-toPlayer.Unit)
		end
	elseif distance > idealDistance then
		targetPosition = target.Character.HumanoidRootPart.Position + target.Character.HumanoidRootPart.Velocity * predictionTime
	elseif distance < 15 then
		local playerLookVector = target.Character.HumanoidRootPart.CFrame.LookVector
		targetPosition = target.Character.HumanoidRootPart.Position - playerLookVector * 5
	end
	if not boss:GetAttribute("MoveActive") and not boss:GetAttribute("Stunned") and not isDashing then
		humanoid.WalkSpeed = originalWalkSpeed * (healthPercent < 0.3 and 1.2 or 1)
		smoothSeek(targetPosition)
		if math.random() < 0.1 then
			dash(circleDirection)
		end
	else
		humanoid.WalkSpeed = 0
	end
	if tick() - lastActionTime >= actionCooldown and not boss:GetAttribute("MoveActive") and not boss:GetAttribute("Stunned") and not isDashing then
		if distance < 6 and math.random() < 0.6 then
			Activated()
		elseif distance > 10 and distance < 15 and math.random() < 0.2 then
			Lunge()
		else
			local bestSkill = getBestSkill(distance, healthPercent, aggression)
			if bestSkill then
				Skills[bestSkill](boss)
				lastActionTime = tick()
			end
		end
	end
	RunService.Heartbeat:Wait()
end

I also made this script with the help of AI since scripting is not my forte.

Thanks for checking out.

-- Boss AI Script

local boss = script.Parent
local humanoid = boss:WaitForChild("Humanoid", 5)
local rootPart = boss:WaitForChild("HumanoidRootPart", 5)
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Debris = game:GetService("Debris")

if not humanoid or not rootPart then
	warn("Boss missing Humanoid or HumanoidRootPart")
	return
end

-- Ensure attributes exist
if boss:GetAttribute("MoveActive") == nil then
	boss:SetAttribute("MoveActive", false)
end
if boss:GetAttribute("Stunned") == nil then
	boss:SetAttribute("Stunned", false)
end

-- Reference Linked Sword
local sword = boss:WaitForChild("LinkedSword", 5)
if not sword then
	warn("LinkedSword not found in boss model")
	return
end

-- Load Skills module
local success, Skills = pcall(function()
	return require(game.ServerStorage.SkillsModule)
end)
if not success then
	warn("Failed to load SkillsModule: " .. tostring(Skills))
	return
end

-- Variables
local originalWalkSpeed = 20
humanoid.WalkSpeed = originalWalkSpeed
local predictionTime = 0.6
local lastSwingTime = 0
local swingCooldown = 0.4
local lastDashTime = 0
local dashCooldown = 1
local isDashing = false
local minDistance = 6
local optimalDistance = 8
local lastActionTime = 0
local actionCooldown = 0.5
local playerDamageTracker = {}
local lastDamageTime = 0
local lastAttack = 0
local toolEquipped = false
local DamageValues = { BaseDamage = 5, SlashDamage = 10, LungeDamage = 30 }
local Damage = DamageValues.BaseDamage

-- Sword Functions
local function SwordUp()
	sword.Grip = CFrame.new(0, 0, -1.5, 0, 0, 1, 1, 0, 0, 0, 1, 0)
end

local function SwordOut()
	sword.Grip = CFrame.new(0, 0, -1.5, 0, -1, 0, -1, 0, 0, 0, 0, -1)
end

local function TagHumanoid(humanoid, player)
	local tag = Instance.new("ObjectValue")
	tag.Name = "creator"
	tag.Value = player
	Debris:AddItem(tag, 2)
	tag.Parent = humanoid
end

local function UntagHumanoid(humanoid)
	for _, v in ipairs(humanoid:GetChildren()) do
		if v:IsA("ObjectValue") and v.Name == "creator" then
			v:Destroy()
		end
	end
end

local function Attack()
	Damage = DamageValues.SlashDamage
	local sound = sword:WaitForChild("Handle"):FindFirstChild("Slash")
	if sound then sound:Play() end
	local anim = Instance.new("StringValue")
	anim.Name = "toolanim"
	anim.Value = "Slash"
	anim.Parent = sword
	print("Boss used Slash")
end

local function Lunge()
	Damage = DamageValues.LungeDamage
	local sound = sword:WaitForChild("Handle"):FindFirstChild("Lunge")
	if sound then sound:Play() end
	local anim = Instance.new("StringValue")
	anim.Name = "toolanim"
	anim.Value = "Lunge"
	anim.Parent = sword
	local force = Instance.new("BodyVelocity")
	force.Velocity = (target.Character.HumanoidRootPart.Position - rootPart.Position).Unit * 40 + Vector3.new(0,10,0)
	force.MaxForce = Vector3.new(1e5,1e5,1e5)
	force.P = 1e4
	force.Parent = rootPart
	Debris:AddItem(force, 0.3)
	createWhiteSilhouettes(boss)
	task.spawn(function()
		task.wait(0.25)
		SwordOut()
		task.wait(0.75)
		SwordUp()
	end)
	print("Boss used Lunge")
end

local function Blow(hit)
	if not hit or not hit.Parent or not humanoid or humanoid.Health <= 0 then return end
	local character = hit.Parent
	local victim = character:FindFirstChildOfClass("Humanoid")
	if victim then
		local player = Players:GetPlayerFromCharacter(character)
		if player then
			UntagHumanoid(victim)
			TagHumanoid(victim, nil)
			victim:TakeDamage(Damage)
		end
	end
end

local function Activated()
	if not toolEquipped or humanoid.Health <= 0 or tick() - lastSwingTime < swingCooldown then return end
	lastSwingTime = tick()
	lastActionTime = tick()
	if tick() - lastAttack < 0.4 then
		Lunge()
	else
		Attack()
	end
	Damage = DamageValues.BaseDamage
	lastAttack = tick()
end

local function Equipped()
	if humanoid.Health <= 0 then return end
	toolEquipped = true
	local sound = sword:WaitForChild("Handle"):FindFirstChild("Unsheath")
	if sound then sound:Play() end
end

local function createWhiteSilhouettes(character)
	for _, part in ipairs(character:GetChildren()) do
		if part:IsA("BasePart") and part.Name ~= "HumanoidRootPart" then
			local clone = part:Clone()
			clone.Color = Color3.new(1,1,1)
			clone.Material = Enum.Material.SmoothPlastic
			clone.Transparency = 0.9
			clone.Anchored = true
			clone.CanCollide = false
			clone.Parent = workspace
			Debris:AddItem(clone, 0.125)
		end
	end
end

local function dash(direction)
	if isDashing or tick() - lastDashTime < dashCooldown then return end
	isDashing = true
	lastDashTime = tick()
	local bv = Instance.new("BodyVelocity")
	bv.Velocity = direction * 40
	bv.MaxForce = Vector3.new(1e5,0,1e5)
	bv.P = 1e4
	bv.Parent = rootPart
	Debris:AddItem(bv, 0.2)
	createWhiteSilhouettes(boss)
	task.wait(0.2)
	isDashing = false
	print("Boss dashed")
end

SwordUp()
sword:WaitForChild("Handle").Touched:Connect(Blow)
Equipped()

local function updatePlayerAggression(player, damage)
	if not playerDamageTracker[player] then
		playerDamageTracker[player] = { damage = 0, lastHit = tick() }
	end
	playerDamageTracker[player].damage += damage
	playerDamageTracker[player].lastHit = tick()
end

local function getNearestPlayer()
	local closest, minDist = nil, math.huge
	for _, player in ipairs(Players:GetPlayers()) do
		if player.Character and player.Character:FindFirstChild("HumanoidRootPart") and player.Character:FindFirstChildOfClass("Humanoid") and player.Character.Humanoid.Health > 0 then
			local dist = (rootPart.Position - player.Character.HumanoidRootPart.Position).Magnitude
			if dist < minDist then
				closest, minDist = player, dist
			end
		end
	end
	return closest
end

local function smoothSeek(targetPos)
	local dir = (targetPos - rootPart.Position).Unit
	humanoid:Move(dir, false)
end

local function getBestSkill(distance, healthPercent, aggression)
	if healthPercent < 0.3 and distance < 5 and not Skills.isOnCooldown(boss, "IceBurst") then
		return "IceBurst"
	end
	if aggression > 1 and distance < 5 and not Skills.isOnCooldown(boss, "IceBurst") then
		return "IceBurst"
	elseif distance > 10 and not Skills.isOnCooldown(boss, "IlluminaLunge") then
		return "IlluminaLunge"
	elseif distance < 5 and not Skills.isOnCooldown(boss, "FirebrandSlash") then
		return "FirebrandSlash"
	elseif distance < 15 and not Skills.isOnCooldown(boss, "WitherSlash") then
		return "WitherSlash"
	else
		return "IlluminaLunge"
	end
end

humanoid.HealthChanged:Connect(function(newHealth)
	if humanoid.Health > newHealth then
		local damage = humanoid.Health - newHealth
		local nearest = getNearestPlayer()
		if nearest then
			updatePlayerAggression(nearest, damage)
			lastDamageTime = tick()
		end
	end
end)

-- Main Loop
local target = nil
local nextMoveTime = 0

while humanoid and humanoid.Health > 0 do
	target = getNearestPlayer()
	if target and target.Character and target.Character:FindFirstChild("HumanoidRootPart") then
		local now = tick()
		local distance = (rootPart.Position - target.Character.HumanoidRootPart.Position).Magnitude
		local healthPercent = humanoid.Health / humanoid.MaxHealth
		local aggression = playerDamageTracker[target] and (playerDamageTracker[target].damage / (now - playerDamageTracker[target].lastHit + 1)) or 0

		if now >= nextMoveTime and not boss:GetAttribute("MoveActive") and not boss:GetAttribute("Stunned") and not isDashing then
			humanoid.WalkSpeed = originalWalkSpeed * (healthPercent < 0.3 and 1.2 or 1)
			local toPlayer = target.Character.HumanoidRootPart.Position - rootPart.Position
			local idealDist = (distance < 5 and minDistance) or (distance < 15 and optimalDistance) or 10
			local targetPos
			if distance < 5 then
				targetPos = rootPart.Position - toPlayer.Unit * (minDistance - distance)
			elseif distance > idealDist then
				targetPos = target.Character.HumanoidRootPart.Position + target.Character.HumanoidRootPart.Velocity * predictionTime
			else
				targetPos = target.Character.HumanoidRootPart.Position
			end
			smoothSeek(targetPos)
			nextMoveTime = now + 0.25
			if math.random() < 0.1 then
				dash(Vector3.new(-toPlayer.Z, 0, toPlayer.X).Unit)
			end
		end
		
		if now - lastActionTime >= actionCooldown and not boss:GetAttribute("MoveActive") and not boss:GetAttribute("Stunned") and not isDashing then
			if distance < 6 and math.random() < 0.6 then
				Activated()
			elseif distance > 10 and distance < 15 and math.random() < 0.2 then
				Lunge()
			else
				local skill = getBestSkill(distance, healthPercent, aggression)
				if skill then
					Skills[skill](boss)
					lastActionTime = now
				end
			end
		end
	end
	RunService.Heartbeat:Wait()
end

print("Boss has died or missing humanoid.")