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