Hello ![]()
so after long scripting i amde a Npc system but it runs into serious problems and there is a lot of them so first before i stsart here are some things to know:
I Used Colletion service for better managment and to try to manage as much npc as possible and the attck state value are booalen values inside the player charachter and npc so they can reacte acordingly and other factions priroritise demons because there a larger threat:
local Debrie = game:GetService("Debris")
local COL = game:GetService("CollectionService")
local blood = game.ReplicatedStorage.GoreStuff.Blood.BloodonHit
local NpcTag = COL:GetTagged("Npc")
-- IdleState: standing doing nothing
-- PatrolState: moves around the map by Waypoints
-- AttackState: moves to the player or enemy npc and attacks them
--Check waht they are
local Pathfinding = game:GetService("PathfindingService")
local MoveAni = {
Walk = "rbxassetid://116892861184871",
Run = "rbxassetid://140361513612310",
}
local WalkAnim = Instance.new("Animation")
WalkAnim.AnimationId = MoveAni.Walk
local RunAnim = Instance.new("Animation")
RunAnim.AnimationId = MoveAni.Run
local AttackAnimations = {
Light = Instance.new("Animation"),
Heavy = Instance.new("Animation"),
Parry = Instance.new("Animation"),
SuccesParry = Instance.new("Animation"),
Shove = Instance.new("Animation")
}
local OtherAnimations = {
Idle = Instance.new("Animation"),
}
local baseANRT = 1.3
local detected = false
local DetectedNpcFaction = nil
local EnemyNpc = nil
local function NPCLogic(Npc)
if not Npc or Npc.Name ~= "Human" and Npc.Name ~= "Demon" then
return
end
local level = Npc:GetAttribute("Level")
local npcANRT = baseANRT / level
local State = "Idle"
local Attack = 0
local Humanoid = Npc:WaitForChild("Humanoid")
local HumanoidRootPart = Npc:WaitForChild("HumanoidRootPart")
local Charmodul
if Charmodul == nil then
for i, ModuleSript in pairs(Npc:GetChildren()) do
if ModuleSript:IsA("ModuleScript") then
Charmodul = ModuleSript
print("Found it " .. ModuleSript.Name)
break
end
end
end
local isStunned = Npc:FindFirstChild("Stuned")
local isAttacking = Npc:FindFirstChild("Attack")
local isParrying = Npc:FindFirstChild("Parry")
local isShoving = Npc:FindFirstChild("Shove")
local CharStats = require(Charmodul)
local NpcFaction = Npc:GetAttribute("Faction")
local hum = Npc:FindFirstChild("Humanoid")
local root =Npc:FindFirstChild("HumanoidRootPart")
local holdingAnimTrack = nil
local holdingAnimation = nil
local isHolding = false
local Module
local WeaponStats
if hum and hum.Health == 0 then
return
else
local Weapon = Npc:FindFirstChild("Weapon")
if Weapon then
Module = Weapon and Weapon:WaitForChild("WeponStats")
WeaponStats = Module and require(Module)
holdingAnimation = Instance.new("Animation")
holdingAnimation.AnimationId = WeaponStats.Idle
end
end
local SwingSound = Module and Module:FindFirstChild("SwingSound")
local HitSoundLight = Module and Module:FindFirstChild("HitSoundLight")
local HitSoundHeavy = Module and Module:FindFirstChild("HitSoundHeavy")
local BackStabSound = Module and Module:FindFirstChild("Backstab")
local ParrySucces = Module and Module:FindFirstChild("ParrySucces")
local walkTrack = hum:LoadAnimation(WalkAnim)
local runTrack = hum:LoadAnimation(RunAnim)
walkTrack.Priority = Enum.AnimationPriority.Movement
runTrack.Priority = Enum.AnimationPriority.Movement
local function FaceTarget(target)
local lookAt = CFrame.lookAt(root.Position, target.Position)
if target and target:IsA("BasePart") then
root.CFrame = CFrame.lookAt(root.Position, target.Position)
end
end
local function getRaycastHit() -- For melle Attack
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
raycastParams.FilterDescendantsInstances = {Npc}
raycastParams.IgnoreWater = true
local origin = root.Position
local direction
if WeaponStats and WeaponStats.Range then
-- Ray direction based on weapon range
direction = root.CFrame.LookVector * WeaponStats.Range
else
-- Fall back if fails
direction = root.CFrame.LookVector * 6.5
end
local result = workspace:Raycast(origin, direction, raycastParams)
return result
end
local function Detect(angle, castAmount) -- that he can see
local current = 0 - math.abs(angle)
for i=1, castAmount do
local filter = {Npc}
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = filter
local LookDistance = 60
local head = Npc:FindFirstChild("Head")
if not head then return end
local rayCFrame = head.CFrame
local rayAngle = rayCFrame * CFrame.Angles(0, math.rad(current), 0)
local ray = workspace:Raycast(Npc.Head.Position, rayAngle.LookVector * LookDistance, params)
if ray and ray.Instance then
local hitModel = ray.Instance:FindFirstAncestorOfClass("Model")
local closestEnemy = nil
local closestDistance = math.huge
if hitModel:FindFirstChild("Humanoid") then
print("Is here")
else
return
end
if hitModel and hitModel:GetAttribute("Faction") ~= NpcFaction then
local distance = (Npc.HumanoidRootPart.Position - hitModel.HumanoidRootPart.Position).Magnitude
if distance < closestDistance then
closestDistance = distance
closestEnemy = hitModel
end
end
if closestEnemy then
detected = true
DetectedNpcFaction = closestEnemy:GetAttribute("Faction")
EnemyNpc = closestEnemy
print(Npc.Name .. " detected enemy: " .. EnemyNpc.Name)
State = "Combat"
end
end
current += math.abs(angle)*2 / castAmount
end
end
Detect(55, 10)
local movementTrack = nil
local function PatrolMovement(targetPosition,Running)
if Running then
hum.WalkSpeed = CharStats.SprintSpeed
if not runTrack.IsPlaying then
walkTrack:Stop()
runTrack:Play()
end
else
hum.WalkSpeed = CharStats.BaseWalkSpeed
if not walkTrack.IsPlaying then
runTrack:Stop()
walkTrack:Play()
end
end
local Path = Pathfinding:CreatePath({
AgentRadius = 2,
AgentHeight = 5,
AgentCanJump = true,
AgentCanClimb = true
})
Path:ComputeAsync(Npc.HumanoidRootPart.Position, targetPosition)
local waypoints = Path:GetWaypoints()
if Path.Status == Enum.PathStatus.Success then
local waypoints = Path:GetWaypoints()
for i = 2, #waypoints do
if isStunned.Value or hum.Health <= 0 then break end
hum:MoveTo(waypoints[i].Position)
hum.MoveToFinished:Wait(0)
end
else
warn(Npc.Name .. " failed to pathfind.")
end
end
local function CombatMovement(enemyRoot, Running)
if Running then
hum.WalkSpeed = CharStats.SprintSpeed
if not runTrack.IsPlaying then
walkTrack:Stop()
runTrack:Play()
end
else
hum.WalkSpeed = CharStats.BaseWalkSpeed
if not walkTrack.IsPlaying then
runTrack:Stop()
walkTrack:Play()
end
end
hum:MoveTo(enemyRoot.Position)
end
local function LightAttack()
local Damage = WeaponStats.LightAttack.Damage
local SwingSpeed = WeaponStats.LightAttack.SwingSpeed -- delay before ray shoots
local AttackSpeed = WeaponStats.LightAttack.AttackSpeed -- delay before another attack
isAttacking.Value = true
if Attack == 0 then
Attack += 1
AttackAnimations.Light.AnimationId = WeaponStats.LightAttack.Animation1
elseif Attack == 1 then
Attack -= 1
AttackAnimations.Light.AnimationId = WeaponStats.LightAttack.Animation2
end
local AttackTrack = hum:LoadAnimation(AttackAnimations.Light)
AttackTrack:Play()
SwingSound.PlaybackSpeed = 1
SwingSound:Play()
task.wait(SwingSpeed)
getRaycastHit()
local RayResult = getRaycastHit()
if RayResult then
local hitPart = RayResult.Instance
local hitCharacter = hitPart:FindFirstAncestorOfClass("Model")
local hitHumanoid = hitCharacter:FindFirstChild("Humanoid")
local hitRoot = hitCharacter:FindFirstChild("HumanoidRootPart")
local hitmodul
-- First: Check if it's a player (CharacterStats inside "Modules")
local modulesFolder = hitCharacter:FindFirstChild("Modules")
if modulesFolder then
local playerStats = modulesFolder:FindFirstChild("CharacterStats")
if playerStats and playerStats:IsA("ModuleScript") then
hitmodul = playerStats
end
end
-- Second: If not found, fallback to NPC module
if not hitmodul then
for _, mod in ipairs(hitCharacter:GetChildren()) do
if mod:IsA("ModuleScript") then
hitmodul = mod
break -- Assume NPCs only have one module
end
end
end
local hitCharStats = require(hitmodul)
local DmgReduction = hitCharStats.DamageReduction
if hitCharacter:GetAttribute("Faction") == NpcFaction then
print("Friendly")
else
if hitHumanoid and hitHumanoid.Health > 0 then
local EnemyisParrying = hitCharacter:FindFirstChild("Parry")
if EnemyisParrying.Value == true then
EnemyisParrying:SetAttribute("Succes","Yes")
local StunDuration = 2
isStunned.Value = true
task.delay(StunDuration,function()
isStunned.Value = false
EnemyisParrying:SetAttribute("Succes","No")
isAttacking.Value = false
end)
local StunedAnim2 = Instance.new("Animation")
StunedAnim2.AnimationId = "rbxassetid://71741690708726"
local animTrack = hum:LoadAnimation(StunedAnim2)
animTrack:Play()
animTrack:AdjustSpeed(2)
isAttacking.Value = false
return
else
print("He isnt Parrying")
end
local directionToEnemy = (hitRoot.Position - root.Position).Unit
local enemyLook = hitRoot.CFrame.LookVector
local dot = directionToEnemy:Dot(enemyLook)
local isBackstab = dot > 0.5
if isBackstab then
Damage = Damage * 2 / DmgReduction
hitHumanoid:TakeDamage(Damage)
hitCharacter:SetAttribute("KFB",true)
HitSoundLight:Play()
BackStabSound:Play()
else
hitHumanoid:TakeDamage(Damage / DmgReduction)
hitCharacter:SetAttribute("KFB",false)
HitSoundLight:Play()
end
hitCharacter:SetAttribute("DeathCause",WeaponStats.WeponClass)
local hitPosition = RayResult.Position
local hitNormal = RayResult.Normal
local bloodClone = blood:Clone()
bloodClone.Parent = hitCharacter.Torso
bloodClone.Anchored = true
-- Offset slightly in the direction of the surface normal
local offset = 0.05
bloodClone.CFrame = CFrame.new(hitPosition + hitNormal * offset, hitPosition + hitNormal * 2)
for _, emitter in pairs(bloodClone:GetDescendants()) do
if emitter:IsA("ParticleEmitter") then
emitter.Enabled = true
task.delay(0.15,function()
emitter.Enabled = false
end)
end
end
-- Auto-cleanup
Debrie:AddItem(bloodClone, 1)
isAttacking.Value = false
end
end
end
end
local function HeavyAttack()
local Damage = WeaponStats.HeavyAttack.Damage
local SwingSpeed = WeaponStats.HeavyAttack.SwingSpeed -- delay before ray shoots
local AttackSpeed = WeaponStats.HeavyAttack.AttackSpeed -- delay before another attack
isAttacking.Value = true
AttackAnimations.Heavy.AnimationId = WeaponStats.HeavyAttack.Animation
local AttackTrack = hum:LoadAnimation(AttackAnimations.Heavy)
AttackTrack:Play()
SwingSound.PlaybackSpeed = 0.9
SwingSound:Play()
task.wait(SwingSpeed)
getRaycastHit()
local RayResult = getRaycastHit()
if RayResult then
local hitPart = RayResult.Instance
local hitCharacter = hitPart:FindFirstAncestorOfClass("Model")
local hitHumanoid = hitCharacter:FindFirstChild("Humanoid")
local hitRoot = hitCharacter:FindFirstChild("HumanoidRootPart")
local hitmodul
-- First: Check if it's a player (CharacterStats inside "Modules")
local modulesFolder = hitCharacter:FindFirstChild("Modules")
if modulesFolder then
local playerStats = modulesFolder:FindFirstChild("CharacterStats")
if playerStats and playerStats:IsA("ModuleScript") then
hitmodul = playerStats
end
end
-- Second: If not found, fallback to NPC module
if not hitmodul then
for _, mod in ipairs(hitCharacter:GetChildren()) do
if mod:IsA("ModuleScript") then
hitmodul = mod
break -- Assume NPCs only have one module
end
end
end
local hitCharStats = require(hitmodul)
local DmgReduction = hitCharStats.DamageReduction
if hitCharacter:GetAttribute("Faction") == NpcFaction then
print("friendly")
else
if hitHumanoid and hitHumanoid.Health > 0 then
local EnemyisParrying = hitCharacter:FindFirstChild("Parry")
if EnemyisParrying.Value == true then
EnemyisParrying:SetAttribute("Succes","Yes")
local StunDuration = 2
isStunned.Value = true
task.delay(StunDuration,function()
isStunned.Value = false
EnemyisParrying:SetAttribute("Succes","No")
isAttacking.Value = false
end)
local StunedAnim2 = Instance.new("Animation")
StunedAnim2.AnimationId = "rbxassetid://71741690708726"
local animTrack = hum:LoadAnimation(StunedAnim2)
animTrack:Play()
animTrack:AdjustSpeed(2)
isAttacking.Value = false
return
else
print("He isnt Parrying")
end
local directionToEnemy = (hitRoot.Position - root.Position).Unit
local enemyLook = hitRoot.CFrame.LookVector
local dot = directionToEnemy:Dot(enemyLook)
local isBackstab = dot > 0.5
if isBackstab then
Damage = Damage * 2 / DmgReduction
hitHumanoid:TakeDamage(Damage)
hitCharacter:SetAttribute("KFB",true)
HitSoundHeavy:Play()
BackStabSound:Play()
else
Damage = Damage / DmgReduction
hitHumanoid:TakeDamage(Damage)
HitSoundHeavy:Play()
hitCharacter:SetAttribute("KFB",false)
end
hitCharacter:SetAttribute("DeathCause",WeaponStats.WeponClass)
local hitPosition = RayResult.Position
local hitNormal = RayResult.Normal
local bloodClone = blood:Clone()
bloodClone.Parent = hitCharacter.Torso
bloodClone.Anchored = true
-- Offset slightly in the direction of the surface normal
local offset = 0.05
bloodClone.CFrame = CFrame.new(hitPosition + hitNormal * offset, hitPosition + hitNormal * 2)
for _, emitter in pairs(bloodClone:GetDescendants()) do
if emitter:IsA("ParticleEmitter") then
emitter.Enabled = true
task.delay(0.15,function()
emitter.Enabled = false
end)
end
end
-- Auto-cleanup
Debrie:AddItem(bloodClone, 1)
isAttacking.Value = false
end
end
end
end
local function Shove()
isShoving.Value = true
AttackAnimations.Shove.AnimationId = "rbxassetid://71009689426196"
local AttackTrack = hum:LoadAnimation(AttackAnimations.Shove)
AttackTrack:Play()
getRaycastHit()
local RayResult = getRaycastHit()
if RayResult then
local hitPart = RayResult.Instance
local hitCharacter = hitPart:FindFirstAncestorOfClass("Model")
local hitHumanoid = hitCharacter:FindFirstChild("Humanoid")
local hitRoot = hitCharacter:FindFirstChild("HumanoidRootPart")
local hitmodul
-- First: Check if it's a player (CharacterStats inside "Modules")
local modulesFolder = hitCharacter:FindFirstChild("Modules")
if modulesFolder then
local playerStats = modulesFolder:FindFirstChild("CharacterStats")
if playerStats and playerStats:IsA("ModuleScript") then
hitmodul = playerStats
end
end
-- Second: If not found, fallback to NPC module
if not hitmodul then
for _, mod in ipairs(hitCharacter:GetChildren()) do
if mod:IsA("ModuleScript") then
hitmodul = mod
break -- Assume NPCs only have one module
end
end
end
local hitCharStats = require(hitmodul)
local EnemyIsStuned = hitCharacter:FindFirstChild("Stuned")
local EnemyisParrying = hitCharacter:FindFirstChild("Parry")
if hitCharacter:GetAttribute("Faction") == NpcFaction then
print("friendly")
else
if hitHumanoid and hitHumanoid.Health > 0 then
if EnemyisParrying.Value == true then
EnemyisParrying.Value = false
local StunDuration = 1.5
EnemyIsStuned.Value = true
task.delay(StunDuration,function()
EnemyIsStuned.Value = false
end)
local StunedAnim2 = Instance.new("Animation")
StunedAnim2.AnimationId = "rbxassetid://95656243016408"
local animTrack = hum:LoadAnimation(StunedAnim2)
animTrack:Play()
animTrack:AdjustSpeed(2)
else
print("He isnt Parrying")
end
isShoving.Value = false
end
end
end
end
local function Parry()
local ParryDuration = 1
isParrying.Value = true
AttackAnimations.Parry.AnimationId = WeaponStats.Parry.Animation
local AttackTrack = hum:LoadAnimation(AttackAnimations.Parry)
AttackTrack:Play()
isParrying:GetAttributeChangedSignal("Succes"):Connect(function()
if isParrying:GetAttribute("Succes") == "Yes" then
AttackAnimations.SuccesParry.AnimationId = WeaponStats.SuccesParry.Animation
local animTrack1 = hum:LoadAnimation(AttackAnimations.SuccesParry)
local sound = Instance.new("Sound")
sound.Parent = Npc
sound.SoundId = WeaponStats.ParrySucces.Sound
sound:Play()
animTrack1:Play()
print("animation has played")
Debrie:AddItem(sound,3)
isParrying.Value = false
end
end)
task.delay(ParryDuration, function()
isParrying.Value = false
end)
end
local function IdleState()
print("Nothing now")
isParrying.Value = false
isAttacking.Value = false
isShoving.Value = false
isStunned.Value = false
end
local function PatrolState()
print("Nothing now")
isParrying.Value = false
isAttacking.Value = false
isShoving.Value = false
isStunned.Value = false
end
local Cooldown = 0
local LastAttack = 0
local function CombatState()
local EnemyisStunned = EnemyNpc:FindFirstChild("Stuned")
local EnemyisAttacking = EnemyNpc:FindFirstChild("Attack")
local EnemyisParrying = EnemyNpc:FindFirstChild("Parry")
local EnemyisShoving = EnemyNpc:FindFirstChild("Shove")
if isStunned.Value then
return -- do not attack
end
if EnemyNpc and EnemyNpc:GetAttribute("Faction") == "Demon" then
local distance = (root.Position - EnemyNpc.HumanoidRootPart.Position).Magnitude
if EnemyNpc.Humanoid.Health <= 0 and not detected then
State = "Idle"
EnemyNpc = nil
end
if distance >= 20 then
CombatMovement(EnemyNpc.HumanoidRootPart, true) -- run
return
end
if distance >= WeaponStats.Range then
CombatMovement(EnemyNpc.HumanoidRootPart, false) -- walk
return
end
if walkTrack.IsPlaying then walkTrack:Stop() end
if runTrack.IsPlaying then runTrack:Stop() end
hum.WalkSpeed = 0
hum:Move(Vector3.zero)
task.wait(npcANRT)
FaceTarget(EnemyNpc.HumanoidRootPart)
if tick() - LastAttack >= Cooldown then
if EnemyisAttacking.Value == true then
Parry()
Cooldown = 1
elseif EnemyisParrying.Value == true then
Shove()
Cooldown = 1 * CharStats.CombatBuff
else
local random = math.random(1,2)
if random == 1 then
LightAttack()
Cooldown = WeaponStats.LightAttack.AttackSpeed
elseif random == 2 then
HeavyAttack()
Cooldown = WeaponStats.HeavyAttack.AttackSpeed
end
LastAttack = tick()
end
end
elseif EnemyNpc and EnemyNpc:GetAttribute("Faction") ~= NpcFaction then
local distance = (root.Position - EnemyNpc.HumanoidRootPart.Position).Magnitude
if EnemyNpc.Humanoid.Health <= 0 and not detected then
State = "Idle"
end
if distance >= 20 then
CombatMovement(EnemyNpc.HumanoidRootPart, true) -- run
return
end
if distance >= WeaponStats.Range then
CombatMovement(EnemyNpc.HumanoidRootPart, false) -- run
return
end
if distance > 60 then -- completely lost target
State = "Idle"
EnemyNpc = nil
detected = false
return
end
-- Close enough to fight → stop moving
if walkTrack.IsPlaying then walkTrack:Stop() end
if runTrack.IsPlaying then runTrack:Stop() end
hum.WalkSpeed = 0
hum:Move(Vector3.zero)
task.wait(npcANRT)
if tick() - LastAttack >= Cooldown then
if EnemyisAttacking.Value == true then
Parry()
Cooldown = 1
elseif EnemyisParrying.Value == true then
Shove()
Cooldown = 1 * CharStats.CombatBuff
else
local random = math.random(1,2)
if random == 1 then
LightAttack()
Cooldown = WeaponStats.LightAttack.AttackSpeed
elseif random == 2 then
HeavyAttack()
Cooldown = WeaponStats.HeavyAttack.AttackSpeed
end
LastAttack = tick()
end
end
end
end
if State == "Idle" then
--Make the npc stand around logic for future maybe talking with each other or animation
IdleState()
if detected and EnemyNpc then
State = "Combat"
end
elseif State == "Patrol" then
--Move around the map by waypoints
PatrolState()
if detected and EnemyNpc then
State = "Combat"
else
PatrolState()
end
elseif State == "Combat" then
if EnemyNpc and EnemyNpc:FindFirstChild("Humanoid") and EnemyNpc.Humanoid.Health > 0 then
CombatState() -- <--- actually run the combat logic
else
State = "Idle"
EnemyNpc = nil
detected = false
end
end
print(State)
end
local function SetupNPC(Npc)
task.defer(function()
local hum = Npc:WaitForChild("Humanoid")
local root = Npc:WaitForChild("HumanoidRootPart")
if not hum or not root then
repeat
task.wait(0.1)
hum = Npc:FindFirstChild("Humanoid")
root = Npc:FindFirstChild("HumanoidRootPart")
until not Npc.Parent or (hum and root)
end
while Npc.Parent and hum and hum.Health > 0 do
NPCLogic(Npc)
task.wait(0.5)
end
end)
end
-- Run for existing NPCs
for _, Npc in pairs(COL:GetTagged("Npc")) do
SetupNPC(Npc)
end
-- Run for future NPCs
COL:GetInstanceAddedSignal("Npc"):Connect(SetupNPC)
But the problems are noumerous the walking or run animation dosent stop for some reason they do absolutley nothing after a parry or literely for any reason idk waht causes this excatly, some just stand around and its weird in gernerall or in short the longer it runs the chance of breaking increases rapadly some ideas to improve because i really need some advice its my first time making a npc script that large
note: i dont know if it because the patrol and idle state arent complete but i dont really think so because they simply dont interfere with combat state waht im focusing right now
thank you ![]()