Hello, I am currently making an NPC fighting and movement system using Forbidden AI, I am having a slight issue where sometimes my NPC behaves very jittery, it sometimes also flies around occasionally after gripping a player as well. Below is a video of the problem I’m experiencing and my actual code for NPC functionality. Thank you for anyone’s help.
-- Services
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")
local Server = game:GetService("ServerScriptService"):WaitForChild("Server")
-- Libraries
local ModuleLibrary = ReplicatedStorage:WaitForChild("Library")
local MiscLibrary = ModuleLibrary.Misc
local States = ModuleLibrary:WaitForChild("States")
local BridgeFolder = ModuleLibrary.Bridge
local BridgeNet2 = require(BridgeFolder.BridgeNet2)
local StateModule = require(States:WaitForChild("StateManager"))
local ActionCheck = require(States:WaitForChild("StateChecker"))
local ForbiddenAI = require(MiscLibrary.Forbidden.AI)
local Janitor = require(MiscLibrary:WaitForChild("Janitor"))
-- Combat modules
local LightAttack = require(Server.CharacterUnit.LightAttack)
local Grip = require(Server.CharacterUnit.Grip)
local Parry = require(Server.CharacterUnit.Parry)
local Util = require(MiscLibrary.Utility)
local BlockStart = require(Server.CharacterUnit.BlockStart)
local BlockStop = require(Server.CharacterUnit.BlockStop)
local Feint = require(Server.CharacterUnit.Feint)
local RunningM1 = require(Server.CharacterUnit.RunningM1)
local Aerial = require(Server.CharacterUnit.AerialAttack)
-- Weapon data
local Models = ReplicatedStorage.Assets.Models
local Welds = ReplicatedStorage.Assets.Welds
local Weapons = Models.Weapons
local WeaponWelds = Welds.Weapons
-- NPC setup
local NPC = script.Parent
local janitor = Janitor.new()
local root = NPC:WaitForChild("HumanoidRootPart")
local hum = NPC:WaitForChild("Humanoid")
-- Animation setup
local Style = NPC:GetAttribute("Style")
local Weapon = NPC:GetAttribute("Weapon")
local AnimationFolder = ReplicatedStorage.Assets.Animations
local StyleAnimations = AnimationFolder.Styles[Style][Weapon]
local MovementAnimations = StyleAnimations.Dashes.Equipped
local Crit = require(Server.CharacterUnit.Critical[Weapon])
local RunAnim = hum:LoadAnimation(StyleAnimations.Main:WaitForChild("RunEquipped"))
local WalkAnim = hum:LoadAnimation(StyleAnimations.Main:WaitForChild("Walk"))
local IdleAnim = nil
janitor:Add(RunAnim, "Stop")
-- DISABLE JUMPING
NPC.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
-- Enhanced Bandit Config
local DETECTION_RADIUS = 50
local UPDATE_INTERVAL = 0.1
local TARGET_POSITION_THRESHOLD = 2
local ATTACK_DISTANCE = 5
local FOLLOW_DISTANCE = 15
local STOP_DISTANCE = 4
local WALK_DISTANCE = 14
local WALK_STOP_DISTANCE = 10
local SPRINT_DISTANCE = 30
local MOVEMENT_STATE_DELAY = 0.3
local VISION_ANGLE = 140
local VERTICAL_VISION_ANGLE = 80
local VISION_DISTANCE = 85
local SHOW_RAYS = false
local RAY_FADE = 0.08
-- Enhanced tracking variables
local CHASE_DURATION = 10
local DIRECT_LOS_DISTANCE = 200
local MEMORY_UPDATE_DISTANCE = 35
local MAX_TRACKING_DISTANCE = 1000
local SIGHT_LOSS_BUFFER = 1.5
local TRACKING_GRACE_PERIOD = 3
-- Tracking state
local currentTarget = nil
local lastKnownPosition = nil
local lastSightTime = 0
local trackingStartTime = 0
local isActivelyTracking = false
local sightLossTime = 0
local hasEquippedWeapon = false
-- AI variables
local lastTarget, lastTargetPos = nil, nil
local lastUpdate = 0
local isPathfinding = false
-- Combat variables
local lastAttackTime = 0
local ATTACK_COOLDOWN = 0.6
local lastFeintTime = 0
local FEINT_COOLDOWN = 5.5
local lastRunningM1Time = 0
local RUNNINGM1_COOLDOWN = 3
local lastAerialTime = 0
local AERIAL_COOLDOWN = 5
local comboCount = 0
local lastComboTime = 0
local COMBO_RESET_TIME = 2
local lastMovementStateChange = 0
local DODGE_INTERVAL = 5
local lastDodgeTime = 0
local currentMovementState = "Idle"
-- HP tracking
local maxHealth = hum.MaxHealth
local currentHPPercent = 1.0
-- Combat args
local CombatArguements = { NPC = true }
-- Forbidden AI setup
local aiConfig = ForbiddenAI.GetConfig(NPC)
aiConfig.Tracking.Enabled = true
aiConfig.Visualization.Enabled = false
aiConfig.StopType = "Instant"
if aiConfig.Movement then
aiConfig.Movement.Jump = false
aiConfig.Movement.MaxJumpHeight = 0
end
-- Helpers
local function IsAir() return hum:GetState() == Enum.HumanoidStateType.Freefall end
local function isCharacterDisabled(char)
return StateModule.findAttribute(char, "BeingGripped")
or StateModule.findAttribute(char, "BeingCarried")
or StateModule.findAttribute(char, "Carrying")
or StateModule.findAttribute(char, "IsDodging")
or StateModule.findAttribute(char, "Dashing")
or StateModule.findAttribute(char, "Gripping")
or StateModule.findAttribute(char, "CantMove")
end
local function updateHPPercent()
currentHPPercent = math.clamp(hum.Health / maxHealth, 0, 1)
end
local function hasFeature(threshold)
return currentHPPercent <= threshold
end
local function getParryChance()
if currentHPPercent <= 0.25 then
return 80
elseif currentHPPercent <= 0.5 then
return 70
else
return 60
end
end
-- Enhanced 3D vision cone function
local function isInVisionCone(npcPos, npcDir, targetPos)
local toTarget = (targetPos - npcPos)
local distance = toTarget.Magnitude
if distance == 0 then return true end
local toTargetUnit = toTarget.Unit
-- Calculate horizontal angle (yaw)
local npcDirHorizontal = Vector3.new(npcDir.X, 0, npcDir.Z).Unit
local toTargetHorizontal = Vector3.new(toTarget.X, 0, toTarget.Z).Unit
local horizontalAngle = math.deg(math.acos(math.clamp(npcDirHorizontal:Dot(toTargetHorizontal), -1, 1)))
-- Calculate vertical angle (pitch)
local verticalAngle = math.deg(math.asin(math.clamp(toTargetUnit.Y, -1, 1)))
local npcPitch = math.deg(math.asin(math.clamp(npcDir.Y, -1, 1)))
local relativePitch = math.abs(verticalAngle - npcPitch)
-- Check if target is within both horizontal and vertical FOV
local withinHorizontalFOV = horizontalAngle <= (VISION_ANGLE / 2)
local withinVerticalFOV = relativePitch <= (VERTICAL_VISION_ANGLE / 2)
return withinHorizontalFOV and withinVerticalFOV
end
-- Enhanced debug ray
local function drawDebugRay(fromPos, toPos, blocked)
if not SHOW_RAYS then return end
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.Material = Enum.Material.Neon
part.Transparency = 0.9
part.Color = blocked and Color3.fromRGB(255,0,0) or Color3.fromRGB(0,255,0)
local length = (toPos - fromPos).Magnitude
part.Size = Vector3.new(0.1,0.1,length)
part.CFrame = CFrame.lookAt(fromPos, toPos) * CFrame.new(0,0,-length/2)
part.Name = "DebugVisionRay"
part.Parent = workspace
game.Debris:AddItem(part, RAY_FADE)
end
local blacklistParams = RaycastParams.new()
blacklistParams.FilterType = Enum.RaycastFilterType.Exclude
blacklistParams.IgnoreWater = true
local function getLOSOrigin(character)
local head = character:FindFirstChild("Head")
if head then return head.Position end
local hrp = character:FindFirstChild("HumanoidRootPart")
return hrp and (hrp.Position + Vector3.new(0,1.5,0)) or root.Position
end
local function hasLineOfSight(fromModel, toModel)
local fromPos = getLOSOrigin(fromModel)
local toPos = getLOSOrigin(toModel)
blacklistParams.FilterDescendantsInstances = {fromModel, toModel}
local dir = (toPos - fromPos)
local result = workspace:Raycast(fromPos, dir, blacklistParams)
drawDebugRay(fromPos, result and result.Position or toPos, result ~= nil)
return not result
end
-- Completely rewritten target detection with robust tracking
local function getNearestCharacter(position)
local nearest, nearestDist = nil, math.huge
local npcLook = root.CFrame.LookVector
local currentTime = os.clock()
local function evaluateTarget(char)
if not char or char == NPC then return end
local hrp = char:FindFirstChild("HumanoidRootPart")
if not hrp then return end
local targetPos = hrp.Position
local distance = (position - targetPos).Magnitude
if distance > MAX_TRACKING_DISTANCE then
return
end
local hasLOS = hasLineOfSight(NPC, char)
local inVisionCone = isInVisionCone(position, npcLook, targetPos)
local isCurrentTarget = (currentTarget == char)
local timeSinceLastSight = isCurrentTarget and (currentTime - lastSightTime) or math.huge
local trackingDuration = isCurrentTarget and (currentTime - trackingStartTime) or 0
local canDetect = false
local detectionReason = ""
-- Tier 1: Perfect visibility
if hasLOS and inVisionCone and distance <= VISION_DISTANCE then
canDetect = true
detectionReason = "direct_sight"
end
-- Tier 2: Close range detection
if not canDetect and hasLOS and distance <= 30 then
canDetect = true
detectionReason = "close_range"
end
-- Tier 3: Active tracking continuation
if not canDetect and isCurrentTarget and isActivelyTracking then
if timeSinceLastSight <= CHASE_DURATION then
if hasLOS then
canDetect = true
detectionReason = "tracking_los"
elseif distance <= MEMORY_UPDATE_DISTANCE then
canDetect = true
detectionReason = "tracking_close"
elseif timeSinceLastSight <= SIGHT_LOSS_BUFFER then
canDetect = true
detectionReason = "sight_buffer"
end
end
end
-- Tier 4: Sound detection
if not canDetect and distance <= 12 then
canDetect = true
detectionReason = "sound_detection"
end
if canDetect and distance < nearestDist then
nearest = char
nearestDist = distance
if detectionReason == "direct_sight" or detectionReason == "close_range" then
if currentTarget ~= char then
currentTarget = char
trackingStartTime = currentTime
isActivelyTracking = true
end
lastKnownPosition = targetPos
lastSightTime = currentTime
sightLossTime = 0
elseif detectionReason == "tracking_los" or detectionReason == "tracking_close" then
lastKnownPosition = targetPos
if hasLOS then
lastSightTime = currentTime
sightLossTime = 0
elseif sightLossTime == 0 then
sightLossTime = currentTime
end
elseif detectionReason == "sight_buffer" then
if sightLossTime == 0 then
sightLossTime = currentTime
end
else
if currentTarget ~= char then
currentTarget = char
trackingStartTime = currentTime
isActivelyTracking = true
lastKnownPosition = targetPos
lastSightTime = currentTime
sightLossTime = 0
end
end
end
end
for _, p in pairs(game.Players:GetPlayers()) do
if p.Character then evaluateTarget(p.Character) end
end
for _, char in pairs(workspace.Alive:GetChildren()) do
if char:IsA("Model") and char ~= NPC and char:FindFirstChild("HumanoidRootPart") and not char:FindFirstChild("NPCValues") then
evaluateTarget(char)
end
end
if not nearest and currentTarget and isActivelyTracking then
local targetHRP = currentTarget:FindFirstChild("HumanoidRootPart")
if targetHRP then
local distanceToCurrentTarget = (position - targetHRP.Position).Magnitude
local timeSinceLastSight = currentTime - lastSightTime
local timeSinceSightLoss = sightLossTime > 0 and (currentTime - sightLossTime) or 0
local shouldContinueTracking = false
if distanceToCurrentTarget <= MAX_TRACKING_DISTANCE then
if timeSinceLastSight <= CHASE_DURATION then
shouldContinueTracking = true
elseif lastKnownPosition then
local distanceToLastKnown = (position - lastKnownPosition).Magnitude
if distanceToLastKnown <= FOLLOW_DISTANCE * 2 and timeSinceLastSight <= CHASE_DURATION + TRACKING_GRACE_PERIOD then
shouldContinueTracking = true
end
end
end
if shouldContinueTracking then
nearest = currentTarget
nearestDist = distanceToCurrentTarget
else
currentTarget = nil
lastKnownPosition = nil
isActivelyTracking = false
trackingStartTime = 0
lastSightTime = 0
sightLossTime = 0
end
else
currentTarget = nil
lastKnownPosition = nil
isActivelyTracking = false
trackingStartTime = 0
lastSightTime = 0
sightLossTime = 0
end
elseif not nearest then
currentTarget = nil
lastKnownPosition = nil
isActivelyTracking = false
trackingStartTime = 0
lastSightTime = 0
sightLossTime = 0
end
return nearest, nearestDist
end
-- Enhanced target position calculation
local function getTargetPosition(target)
if not target then return nil end
local hrp = target:FindFirstChild("HumanoidRootPart")
local currentTime = os.clock()
if hrp then
local distance = (root.Position - hrp.Position).Magnitude
local hasRecentSight = (currentTime - lastSightTime) <= SIGHT_LOSS_BUFFER
if hasLineOfSight(NPC, target) or distance <= MEMORY_UPDATE_DISTANCE or hasRecentSight then
return hrp.Position
end
end
if lastKnownPosition and currentTarget == target then
local timeSinceUpdate = currentTime - lastSightTime
local distanceToLastKnown = (root.Position - lastKnownPosition).Magnitude
if timeSinceUpdate <= CHASE_DURATION and distanceToLastKnown <= FOLLOW_DISTANCE * 3 then
return lastKnownPosition
end
end
if hrp then
return hrp.Position
end
return nil
end
local function isAggroedByOtherNPCs(character)
for _, npc in pairs(workspace.Alive:GetChildren()) do
if npc ~= NPC and npc:FindFirstChild("NPCValues") then
local aggro = npc.NPCValues:FindFirstChild("Aggro")
if aggro and aggro.Value == character.Name then return true end
end
end
return false
end
-- Adjust weapon visuals
local function adjustWeapon(isEquipped)
local WeaponFolder = NPC:FindFirstChild("WeaponFolder")
if not WeaponFolder then
WeaponFolder = Instance.new("Folder")
WeaponFolder.Name = "WeaponFolder"
WeaponFolder.Parent = NPC
else
WeaponFolder:ClearAllChildren()
end
local weaponName = NPC:GetAttribute("Weapon") or "Fists"
local styleName = NPC:GetAttribute("Style") or "None"
local WeaponToFind = Weapons:FindFirstChild(weaponName)
if WeaponToFind and WeaponToFind:IsA("Folder") then
for _, weaponModel in pairs(WeaponToFind:GetChildren()) do
if weaponModel:IsA("Model") and weaponModel.PrimaryPart then
local NewModel = weaponModel:Clone()
NewModel.Parent = WeaponFolder
local WeldPart = NewModel.PrimaryPart
local WeldTargetAttr = isEquipped and "WeldTarget_Equipped" or "WeldTarget_Unequipped"
local WeldTargetName = WeldPart:GetAttribute(WeldTargetAttr)
local BasePart = NPC:FindFirstChild(WeldTargetName)
if BasePart and WeldPart then
local weaponWeldFolder = WeaponWelds[weaponName]
local modelWeldData = weaponWeldFolder and weaponWeldFolder[weaponModel.Name]
if modelWeldData then
local Motor6D = (isEquipped and modelWeldData.EquippedWeld or modelWeldData.UnequippedWeld):Clone()
Motor6D.Name = "Handle"
Motor6D.Part0 = BasePart
Motor6D.Part1 = WeldPart
Motor6D.Parent = WeldPart
end
end
for _, child in pairs(NewModel:GetDescendants()) do
local style = child:GetAttribute("IsStyle")
if style and style ~= styleName then
child:Destroy()
end
end
end
end
end
end
-- Equip weapon
local function equipWeapon()
if hasEquippedWeapon then return end
if StateModule.findAttribute(NPC, "Equipped") then return end
local weaponName = NPC:GetAttribute("Weapon") or "Fists"
local equipSound = SoundService.Weapons:FindFirstChild(weaponName)
if equipSound and equipSound:FindFirstChild("Equip") then
local sound = equipSound.Equip:Clone()
sound.Parent = root
sound:Play()
game.Debris:AddItem(sound, sound.TimeLength + 0.1)
end
adjustWeapon(true)
if not IdleAnim then
IdleAnim = hum:LoadAnimation(StyleAnimations.Main.Idle)
IdleAnim.Priority = Enum.AnimationPriority.Idle
end
if IdleAnim and not IdleAnim.IsPlaying then
IdleAnim:Play()
end
StateModule.createAttribute(NPC, "Equipped", true)
hasEquippedWeapon = true
end
-- Unequip weapon
local function unequipWeapon()
if not hasEquippedWeapon then return end
if not StateModule.findAttribute(NPC, "Equipped") then return end
local weaponName = NPC:GetAttribute("Weapon") or "Fists"
local equipSound = SoundService.Weapons:FindFirstChild(weaponName)
if equipSound and equipSound:FindFirstChild("Equip") then
local sound = equipSound.Equip:Clone()
sound.Parent = root
sound:Play()
game.Debris:AddItem(sound, sound.TimeLength + 0.1)
end
if IdleAnim and IdleAnim.IsPlaying then
IdleAnim:Stop()
end
adjustWeapon(false)
StateModule.removeAttribute(NPC, "Equipped")
hasEquippedWeapon = false
end
-- Animations
local function playWalk()
if not WalkAnim.IsPlaying and not isCharacterDisabled(NPC) then
WalkAnim:Play()
end
end
local function stopWalk()
if WalkAnim.IsPlaying then
WalkAnim:Stop()
end
end
local function playRun()
if not IsAir() and not isCharacterDisabled(NPC) then
if not RunAnim.IsPlaying then
RunAnim:Play()
RunAnim:AdjustSpeed(1.2)
RunAnim.Priority = Enum.AnimationPriority.Action2
end
end
end
local function stopRun()
if RunAnim.IsPlaying then
RunAnim:Stop()
end
end
-- Feinting system
local function attemptFeint()
if not hasFeature(0.75) then return false end
local currentTime = os.clock()
if (currentTime - lastFeintTime) < FEINT_COOLDOWN then return false end
if not NPC:GetAttribute("CanFeint") then return false end
local comboValue = NPC:FindFirstChild("CombatStats") and NPC.CombatStats:FindFirstChild("ComboValue")
if not comboValue or comboValue.Value <= 0 then return false end
if math.random(1, 100) <= 100 then
lastFeintTime = currentTime
task.delay(.2, function()
Feint.fire(NPC, CombatArguements)
end)
return true
end
return false
end
-- Dodge cancel system
local function attemptDodgeCancel()
if not hasFeature(0.5) then return false end
if not StateModule.findAttribute(NPC, "Dashing") then return false end
if math.random(1, 100) <= 35 then
StateModule.removeAttribute(NPC, "Dashing")
local cancelAnimName = math.random(1, 2) == 1 and "CancelLeft" or "CancelRight"
local cancelAnim = MovementAnimations:FindFirstChild(cancelAnimName)
if cancelAnim then
local CancelAnimation = hum:LoadAnimation(cancelAnim)
CancelAnimation:Play()
CancelAnimation.Priority = Enum.AnimationPriority.Action3
end
local highlight = Instance.new("Highlight")
highlight.FillColor = Color3.fromRGB(255, 255, 255)
highlight.OutlineColor = Color3.fromRGB(255, 255, 255)
highlight.FillTransparency = 0
highlight.OutlineTransparency = 0
highlight.Parent = NPC
task.spawn(function()
local fadeTime = 1
local steps = 20
local stepTime = fadeTime / steps
for i = 1, steps do
if highlight and highlight.Parent then
highlight.FillTransparency = i / steps
highlight.OutlineTransparency = i / steps
end
task.wait(stepTime)
end
if highlight and highlight.Parent then
highlight:Destroy()
end
end)
return true
end
return false
end
local function attemptRunningM1()
local currentTime = os.clock()
if (currentTime - lastRunningM1Time) < RUNNINGM1_COOLDOWN then return false end
if isCharacterDisabled(NPC) then return false end
if StateModule.findAttribute(NPC, "Aerial") then return false end
RunningM1.fire(NPC)
lastRunningM1Time = currentTime
return true
end
local function attemptAerial()
local currentTime = os.clock()
if (currentTime - lastAerialTime) < AERIAL_COOLDOWN then return false end
if isCharacterDisabled(NPC) then return false end
if StateModule.findAttribute(NPC, "Aerial") then return false end
-- Mark as performing aerial to prevent other attacks
StateModule.createAttribute(NPC, "Aerial", true)
-- Temporarily enable jumping
hum:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
hum.JumpHeight = 7.2
hum.Jump = true
-- Disable after jump and fire aerial
task.delay(0.2, function()
hum:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
hum.JumpHeight = 0
Aerial.fire(NPC)
-- Remove aerial flag after attack completes
task.delay(1, function()
StateModule.removeAttribute(NPC, "Aerial")
end)
end)
lastAerialTime = currentTime
return true
end
-- Enhanced dash system
local function dash(rootPart, humanoid, char)
if isCharacterDisabled(char) or StateModule.findAttribute(char, "DashCooldown") then
return
end
local dirs, strings
if currentTarget then
local targetHRP = currentTarget:FindFirstChild("HumanoidRootPart")
if targetHRP then
local toTarget = (targetHRP.Position - rootPart.Position).Unit
local dotForward = rootPart.CFrame.LookVector:Dot(toTarget)
local dotRight = rootPart.CFrame.RightVector:Dot(toTarget)
if math.abs(dotForward) > 0.7 then
dirs = {-rootPart.CFrame.RightVector, rootPart.CFrame.RightVector}
strings = {"Left", "Right"}
else
dirs = {rootPart.CFrame.LookVector, -rootPart.CFrame.LookVector, -rootPart.CFrame.RightVector, rootPart.CFrame.RightVector}
strings = {"Forward","Back","Left","Right"}
end
else
dirs = {rootPart.CFrame.LookVector, -rootPart.CFrame.LookVector, -rootPart.CFrame.RightVector, rootPart.CFrame.RightVector}
strings = {"Forward","Back","Left","Right"}
end
else
dirs = {rootPart.CFrame.LookVector, -rootPart.CFrame.LookVector, -rootPart.CFrame.RightVector, rootPart.CFrame.RightVector}
strings = {"Forward","Back","Left","Right"}
end
local idx = math.random(1,#dirs)
local dir, str = dirs[idx], strings[idx]
if ActionCheck.CanDash(char) then
StateModule.CreateAttributeTemp(char, "IsDodging", true, 0.3)
StateModule.CreateAttributeTemp(char, "DashCooldown", true, 1.8)
StateModule.CreateAttributeTemp(char, "Dashing", true, 0.35)
local anim = humanoid:LoadAnimation(MovementAnimations:FindFirstChild(str))
anim:Play()
anim.Priority = Enum.AnimationPriority.Action3
anim:AdjustSpeed(1.25)
local bv = Instance.new("BodyVelocity")
bv.MaxForce = Vector3.new(math.huge, 0, math.huge)
bv.Velocity = Vector3.new(dir.X * 65, 0, dir.Z * 65)
bv.Parent = rootPart
game.Debris:AddItem(bv, 0.25)
task.defer(function()
while bv.Parent do
local flatVelocity = Vector3.new(0, 0, 0)
if str == "Forward" then
flatVelocity = Vector3.new(rootPart.CFrame.LookVector.X * 70, 0, rootPart.CFrame.LookVector.Z * 70)
elseif str == "Back" then
flatVelocity = Vector3.new(-rootPart.CFrame.LookVector.X * 70, 0, -rootPart.CFrame.LookVector.Z * 70)
elseif str == "Left" then
flatVelocity = Vector3.new(-rootPart.CFrame.RightVector.X * 70, 0, -rootPart.CFrame.RightVector.Z * 70)
elseif str == "Right" then
flatVelocity = Vector3.new(rootPart.CFrame.RightVector.X * 70, 0, rootPart.CFrame.RightVector.Z * 70)
end
bv.Velocity = flatVelocity
task.wait(0.01)
end
end)
end
end
-- Enhanced pathfinding
local function startPathfinding(target)
if not isPathfinding then
isPathfinding = true
ForbiddenAI.SmartPathfind(NPC, target, false, "DefaultStart")
end
end
local function stopPathfinding()
if isPathfinding then
isPathfinding = false
ForbiddenAI.Stop(NPC, false, "DefaultStop")
end
end
local function updateCombo()
local currentTime = os.clock()
if (currentTime - lastComboTime) > COMBO_RESET_TIME then
comboCount = 0
end
comboCount = comboCount + 1
lastComboTime = currentTime
end
local function dodgeOrBlock()
if hasFeature(0.25) then
if math.random(1, 100) <= 30 then
return true
end
elseif hasFeature(0.5) then
if math.random(1, 100) <= 20 then
return true
end
else
if math.random(1, 100) <= 10 then
return true
end
end
return false
end
-- SEPARATE DEFENSIVE REACTION SYSTEM (handles all ranges)
local function handleDefense(target, distance, canSeeTarget)
local currentTime = os.clock()
updateHPPercent()
-- Check target state
local targetIsAttacking = StateModule.findAttribute(target, "LightAttack")
or StateModule.findAttribute(target, "HeavyAttack")
or StateModule.findAttribute(target, "UsingMove")
local targetIsBlocking = StateModule.findAttribute(target, "IsBlocking")
-- Stop blocking if already blocking and target isn't attacking
if StateModule.findAttribute(NPC, "IsBlocking") and not targetIsAttacking then
BlockStop.fire(NPC, CombatArguements)
end
-- Attempt dodge cancel during dash
if attemptDodgeCancel() then return true end
-- React to target attacking at ANY range (including far-range moves)
if targetIsAttacking then
-- Try to feint if we're in the middle of an attack
attemptFeint()
-- Defensive responses based on distance and HP
if ActionCheck.CanBlock(NPC) and not StateModule.findAttribute(NPC, "ParryCooldown") then
local parryChance = getParryChance()
-- Close range - prefer parry
if distance <= ATTACK_DISTANCE then
if math.random(1, 100) <= parryChance then
local anim = hum:LoadAnimation(AnimationFolder.Styles[Style][Weapon].Blocking.Blocking)
anim:Play()
Parry.fire(NPC, CombatArguements)
StateModule.CreateAttributeTemp(NPC, "IsBlocking", true, 0.35)
task.wait(0.35)
anim:Stop()
return true
end
end
-- Any range - dodge or block based on HP and RNG
if not StateModule.findAttribute(NPC, "IsBlocking") and dodgeOrBlock() then
if ActionCheck.CanDash(NPC) and distance >= 5 then
-- Prefer dodge at medium-far range
dash(root, hum, NPC)
return true
else
-- Block if can't dash or too close
BlockStart.fire(NPC, {Target = target})
local blockDuration = currentHPPercent <= 0.5 and 0.8 or 0.5
StateModule.CreateAttributeTemp(NPC, "BlockCooldown", true, blockDuration)
task.delay(math.random(1, 2), function()
if StateModule.findAttribute(NPC, "IsBlocking") then
BlockStop.fire(NPC, CombatArguements)
end
end)
return true
end
-- Fallback dodge if no other option
elseif ActionCheck.CanDash(NPC) and distance >= 5 and math.random(1, 100) <= 40 then
dash(root, hum, NPC)
return true
end
end
end
return false
end
-- SEPARATE OFFENSIVE SYSTEM (handles attacks)
local function handleOffense(target, distance, canSeeTarget)
local currentTime = os.clock()
-- Handle knocked targets
if target:GetAttribute("Knocked") then
if distance <= ATTACK_DISTANCE * 1.5 then
if not StateModule.findAttribute(NPC, "Gripping") and not StateModule.findAttribute(NPC, "BeingGripped") then
Grip.fire(NPC, {Target = target, NPC = true})
return true
end
end
return false
end
-- Check if we're in attack cooldown
if (currentTime - lastAttackTime) < ATTACK_COOLDOWN then
return false
end
local targetIsBlocking = StateModule.findAttribute(target, "IsBlocking")
-- Offensive actions - close range attacks
if distance <= ATTACK_DISTANCE and canSeeTarget then
if not StateModule.findAttribute(NPC, "LightAttack")
and not StateModule.findAttribute(NPC, "HeavyAttack")
and not StateModule.findAttribute(NPC, "IsBlocking")
and not StateModule.findAttribute(NPC, "IsDodging") then
lastAttackTime = currentTime
updateCombo()
-- Intelligent attack choice based on situation
local attackChoice = math.random(1, 100)
if targetIsBlocking then
if attackChoice <= 30 then
Crit.fire(NPC, CombatArguements)
else
LightAttack.fire(NPC, CombatArguements)
end
else
LightAttack.fire(NPC, CombatArguements)
end
if NPC:GetAttribute("CanFeint") then
if attemptFeint() then
return true
end
end
return true
end
end
return false
end
-- Main AI initialization
local function InitiateBandit()
local cleanup = Janitor.new()
ForbiddenAI.InsertAntiLag(NPC)
-- HP monitoring
local hpConn = hum.HealthChanged:Connect(function(health)
updateHPPercent()
end)
cleanup:Add(hpConn)
-- Enhanced AI loop with better state management
local targetFPS = 60
local targetDelta = 1 / targetFPS
local accumulator = 0
local aiConn = RunService.Heartbeat:Connect(function(dt)
accumulator += dt
while accumulator >= targetDelta do
accumulator -= targetDelta
-- Dynamic movement speed based on state and distance
local WalkSpeed = 0
local now = os.clock()
if currentTarget and isActivelyTracking and not isCharacterDisabled(NPC) then
local targetHRP = currentTarget:FindFirstChild("HumanoidRootPart")
if targetHRP then
local distance = (root.Position - targetHRP.Position).Magnitude
local desiredState = currentMovementState
-- Periodic defensive dodge
if now - lastDodgeTime >= DODGE_INTERVAL then
lastDodgeTime = now
if not isCharacterDisabled(NPC) and ActionCheck.CanDash(NPC) then
dash(root, hum, NPC)
end
end
-- Determine desired movement state with hysteresis
if distance <= STOP_DISTANCE then
desiredState = "Stop"
elseif distance <= WALK_STOP_DISTANCE then
desiredState = "Walk"
elseif distance <= WALK_DISTANCE then
desiredState = "Walk"
elseif distance <= SPRINT_DISTANCE then
desiredState = "Run"
else
desiredState = "Sprint"
end
-- Only change state if enough time has passed OR if it's a major change
local canChangeState = (now - lastMovementStateChange) >= MOVEMENT_STATE_DELAY
local isMajorChange = (currentMovementState == "Stop" and desiredState ~= "Stop") or
(currentMovementState ~= "Stop" and desiredState == "Stop")
if desiredState ~= currentMovementState and (canChangeState or isMajorChange) then
currentMovementState = desiredState
lastMovementStateChange = now
end
-- Apply movement state
if currentMovementState == "Stop" then
WalkSpeed = 0
stopRun()
stopWalk()
StateModule.removeAttribute(NPC, "Sprint")
elseif currentMovementState == "Walk" then
WalkSpeed = 16
stopRun()
playWalk()
StateModule.removeAttribute(NPC, "Sprint")
elseif currentMovementState == "Run" then
StateModule.createAttribute(NPC, "Sprint", true)
WalkSpeed = 28
playRun()
stopWalk()
elseif currentMovementState == "Sprint" then
StateModule.createAttribute(NPC, "Sprint", true)
WalkSpeed = 32
playRun()
stopWalk()
end
-- Apply status effects
if StateModule.findAttribute(NPC, "TrueSlow") then
WalkSpeed = 4
elseif StateModule.findAttribute(NPC, "Slowed") then
WalkSpeed = 10
end
else
-- No target HRP - sprint to last known
if currentMovementState ~= "Sprint" then
currentMovementState = "Sprint"
lastMovementStateChange = now
end
StateModule.createAttribute(NPC, "Sprint", true)
WalkSpeed = 32
playRun()
stopWalk()
end
else
-- Idle state
if currentMovementState ~= "Idle" then
currentMovementState = "Idle"
lastMovementStateChange = now
end
if StateModule.findAttribute(NPC, "Sprint") then
StateModule.removeAttribute(NPC, "Sprint")
end
stopRun()
stopWalk()
end
-- Apply movement settings
hum.WalkSpeed = WalkSpeed
hum.JumpHeight = 7.2
end
-- Main AI logic - runs at UPDATE_INTERVAL
local now = os.clock()
if now - lastUpdate < UPDATE_INTERVAL then return end
lastUpdate = now
-- Handle disabled states
if NPC:GetAttribute("Ragdoll") or isCharacterDisabled(NPC) then
stopPathfinding()
NPC.NPCValues.State.Value = "Idle"
NPC.NPCValues.Aggro.Value = ""
lastTarget, lastTargetPos = nil, nil
currentTarget = nil
lastKnownPosition = nil
isActivelyTracking = false
return
end
-- Enhanced target detection
local nearest, distance = getNearestCharacter(root.Position)
if nearest and distance <= MAX_TRACKING_DISTANCE and not StateModule.findAttribute(nearest, "Dead") then
-- Equip weapon when target is detected
if not hasEquippedWeapon then
equipWeapon()
end
-- Check for NPC conflicts with improved logic
if isAggroedByOtherNPCs(nearest) and NPC.NPCValues.Aggro.Value ~= nearest.Name then
local trackingDuration = isActivelyTracking and (now - trackingStartTime) or 0
if trackingDuration < 3 then
NPC.NPCValues.State.Value = "Idle"
NPC.NPCValues.Aggro.Value = ""
stopPathfinding()
return
end
end
-- Update aggro
NPC.NPCValues.Aggro.Value = nearest.Name
local targetPos = getTargetPosition(nearest)
if targetPos then
local canSeeTarget = hasLineOfSight(NPC, nearest)
-- ALWAYS check for defensive reactions first (works at ANY range)
handleDefense(nearest, distance, canSeeTarget)
-- Handle combat/movement based on distance
if distance <= ATTACK_DISTANCE and canSeeTarget then
-- Close combat range
stopPathfinding()
hum:Move(Vector3.zero)
NPC.NPCValues.State.Value = "Attacking"
handleOffense(nearest, distance, canSeeTarget)
elseif distance <= STOP_DISTANCE and canSeeTarget then
-- Approach for attack
stopPathfinding()
NPC.NPCValues.State.Value = "Approaching"
if distance > ATTACK_DISTANCE then
hum:MoveTo(targetPos)
else
handleOffense(nearest, distance, canSeeTarget)
end
elseif distance <= 20 and distance >= WALK_DISTANCE and canSeeTarget then
-- Medium range - attempt running attack
if not StateModule.findAttribute(NPC, "LightAttack") then
if math.random() < 0.5 then
attemptRunningM1()
else
attemptAerial()
end
end
NPC.NPCValues.State.Value = "Running"
else
-- Long range - chase/pathfind
if canSeeTarget then
NPC.NPCValues.State.Value = "Running"
else
NPC.NPCValues.State.Value = "Chasing"
end
-- Enhanced pathfinding logic
local needsNewPath = false
if not lastTarget or lastTarget ~= nearest then
needsNewPath = true
elseif lastTargetPos then
local positionDelta = (lastTargetPos - targetPos).Magnitude
if positionDelta > TARGET_POSITION_THRESHOLD then
needsNewPath = true
end
else
needsNewPath = true
end
if needsNewPath then
lastTarget, lastTargetPos = nearest, targetPos
if isPathfinding then
stopPathfinding()
task.wait(0.05)
end
startPathfinding(nearest)
end
end
else
-- No valid target position - enter search mode briefly
if isActivelyTracking and (now - lastSightTime) <= TRACKING_GRACE_PERIOD then
NPC.NPCValues.State.Value = "Searching"
else
-- Give up
NPC.NPCValues.Aggro.Value = ""
NPC.NPCValues.State.Value = "Idle"
lastTarget, lastTargetPos = nil, nil
stopPathfinding()
end
end
else
-- No target found - handle persistence and cleanup
if isActivelyTracking and currentTarget then
local timeSinceLastSight = now - lastSightTime
if timeSinceLastSight <= CHASE_DURATION then
-- Continue search briefly (keep weapon equipped)
NPC.NPCValues.State.Value = "Searching"
NPC.NPCValues.Aggro.Value = currentTarget.Name
else
-- Give up tracking
NPC.NPCValues.Aggro.Value = ""
NPC.NPCValues.State.Value = "Idle"
lastTarget, lastTargetPos = nil, nil
stopPathfinding()
if StateModule.findAttribute(NPC, "Sprint") then
StateModule.removeAttribute(NPC, "Sprint")
end
-- Unequip weapon after losing target
task.delay(3, function()
if not currentTarget or not isActivelyTracking then
unequipWeapon()
end
end)
end
else
-- Idle state
NPC.NPCValues.Aggro.Value = ""
NPC.NPCValues.State.Value = "Idle"
lastTarget, lastTargetPos = nil, nil
stopPathfinding()
if StateModule.findAttribute(NPC, "Sprint") then
StateModule.removeAttribute(NPC, "Sprint")
end
-- Unequip weapon when idle
if hasEquippedWeapon then
task.delay(2, function()
if not currentTarget or not isActivelyTracking then
unequipWeapon()
end
end)
end
end
end
end)
cleanup:Add(aiConn)
-- Enhanced movement and rotation system
local moveConn = RunService.Heartbeat:Connect(function()
if not currentTarget or not isActivelyTracking then
hum:Move(Vector3.zero)
return
end
-- Check if movement is allowed
local isDisabled = StateModule.findAttribute(NPC, "IsDodging")
or StateModule.findAttribute(NPC, "Dashing")
or StateModule.findAttribute(NPC, "Aerial")
if not isDisabled then
local targetHRP = currentTarget:FindFirstChild("HumanoidRootPart")
local movePos = nil
-- Determine movement target
if targetHRP and hasLineOfSight(NPC, currentTarget) then
movePos = targetHRP.Position
elseif lastKnownPosition then
movePos = lastKnownPosition
elseif targetHRP then
movePos = targetHRP.Position
end
if movePos then
local direction = (movePos - root.Position)
direction = Vector3.new(direction.X, 0, direction.Z)
-- Smooth rotation towards target
if targetHRP then
local lookAtCF = CFrame.lookAt(root.Position, Vector3.new(targetHRP.Position.X, root.Position.Y, targetHRP.Position.Z))
root.CFrame = lookAtCF
end
if direction.Magnitude > 0 then
hum.AutoRotate = true
local distance = direction.Magnitude
if distance > ATTACK_DISTANCE then
-- Full speed movement when far
hum:Move(direction.Unit)
else
-- Controlled movement when close
hum:Move(Vector3.zero)
end
end
end
else
hum:Move(Vector3.zero)
end
end)
cleanup:Add(moveConn)
-- Cleanup connections
NPC:SetAttribute('Knocked', false)
NPC:SetAttribute('Gripped', false)
cleanup:Add(NPC:GetAttributeChangedSignal("Gripped"):Connect(function()
stopPathfinding()
cleanup:Destroy()
game.Debris:AddItem(NPC, 2)
end))
-- Handle death cleanup
cleanup:Add(NPC:GetAttributeChangedSignal("Ragdoll"):Connect(function()
if NPC:GetAttribute("Ragdoll") then
stopPathfinding()
currentTarget = nil
lastKnownPosition = nil
isActivelyTracking = false
NPC.NPCValues.State.Value = "Idle"
NPC.NPCValues.Aggro.Value = ""
end
end))
-- Monitor humanoid health for additional cleanup
cleanup:Add(hum.Died:Connect(function()
stopPathfinding()
currentTarget = nil
lastKnownPosition = nil
isActivelyTracking = false
cleanup:Destroy()
end))
end
-- Initialize the bandit AI
if NPC.PrimaryPart then
task.spawn(InitiateBandit)
else
NPC:GetPropertyChangedSignal("PrimaryPart"):Wait()
task.spawn(InitiateBandit)
end