The npc is working well in chase, but when i running to small rooms or inside part the npc just afk…
heres script
local NPC = script.Parent
local Humanoid: Humanoid = NPC:WaitForChild("Humanoid")
local HRP: BasePart = NPC:WaitForChild("HumanoidRootPart")
local Players = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")
local rng = Random.new()
local PATROL_MIN_DELAY = 3
local PATROL_MAX_DELAY = 10
local PATROL_STEP_MIN = 7
local PATROL_STEP_MAX = 12
local PATROL_SLICE_MIN = 0.7
local PATROL_SLICE_MAX = 1.2
local PATROL_TRIES = 6
local DETECTION_RANGE = 30
local DROP_RANGE = 500
local REQUIRE_LINE_OF_SIGHT = true
local FOV_DEG = 120
local WALK_SPEED = 12
local CHASE_SPEED_MIN = 22
local CHASE_SPEED_MAX = 22
local CHASE_SPEED_BUFFER = 2
local DIRECT_UPDATE_DT = 0.12
local LEAD_TIME = 0.22
local PATH_REPATH_COOLDOWN = 0.22
local PATH_TARGET_MOVE = 2.8
local PATH_WP_REACH = 2.0
local PATH_MOVE_REISSUE = 0.65
local TARGET_REEVALUATE_DT = 0.35
local LOSE_TARGET_AFTER = 3.0
local SEARCH_MAX_TIME = 6.0
local SEARCH_WANDER_RADIUS = 10
local SEARCH_SLICE = 1.2
local STUCK_TIMEOUT = 0.85
local STUCK_MIN_MOVE = 0.12
local UNSTUCK_SIDESTEP = 3.8
local UNSTUCK_BACKSTEP = 2.8
local UNSTUCK_JUMP_COOLDOWN = 0.8
local UNSTUCK_TELEPORT_COOLDOWN = 1.4
pcall(function()
HRP:SetNetworkOwner(nil)
end)
Humanoid.WalkSpeed = WALK_SPEED
Humanoid.AutoRotate = true
Humanoid.AutoJumpEnabled = false
local headPart: BasePart? = NPC:FindFirstChild("Head") :: any
local excludeSelf = { NPC }
local losParams = RaycastParams.new()
losParams.FilterType = Enum.RaycastFilterType.Exclude
losParams.FilterDescendantsInstances = excludeSelf
local groundParams = RaycastParams.new()
groundParams.FilterType = Enum.RaycastFilterType.Exclude
groundParams.FilterDescendantsInstances = excludeSelf
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Exclude
overlapParams.FilterDescendantsInstances = excludeSelf
local cosHalfFOV = math.cos(math.rad(FOV_DEG * 0.5))
local detectionRangeSq = DETECTION_RANGE * DETECTION_RANGE
local dropRangeSq = DROP_RANGE * DROP_RANGE
local function isAliveHumanoid(h: Humanoid?)
return h ~= nil and h.Parent ~= nil and h.Health > 0
end
local function getModelHumanoidRoot(model: Model)
local hum = model:FindFirstChildOfClass("Humanoid")
local root = model:FindFirstChild("HumanoidRootPart")
if hum and root and root:IsA("BasePart") then
return hum, root
end
return nil, nil
end
local function hasLineOfSight(targetRoot: BasePart)
if not headPart or not headPart.Parent then
headPart = NPC:FindFirstChild("Head") :: any
end
local origin = (headPart and headPart:IsA("BasePart")) and headPart.Position or HRP.Position
local direction = targetRoot.Position - origin
local hit = workspace:Raycast(origin, direction, losParams)
if not hit then
return true
end
return hit.Instance and hit.Instance:IsDescendantOf(targetRoot.Parent)
end
local function inFOV(targetPos: Vector3)
local toTarget = targetPos - HRP.Position
local mag = toTarget.Magnitude
if mag < 0.001 then
return true
end
return HRP.CFrame.LookVector:Dot(toTarget) >= (mag * cosHalfFOV)
end
local function snapToGround(pos: Vector3): Vector3
local origin = pos + Vector3.new(0, 10, 0)
local hit = workspace:Raycast(origin, Vector3.new(0, -250, 0), groundParams)
if hit then
return Vector3.new(pos.X, hit.Position.Y, pos.Z)
end
return pos
end
local function canDetect(root: BasePart)
local diff = root.Position - HRP.Position
local d2 = diff:Dot(diff)
if d2 > detectionRangeSq then
return false
end
if not inFOV(root.Position) then
return false
end
if REQUIRE_LINE_OF_SIGHT and not hasLineOfSight(root) then
return false
end
return true
end
local function pickNearestTarget()
local bestHum: Humanoid? = nil
local bestRoot: BasePart? = nil
local bestDistSq = math.huge
local hrpPos = HRP.Position
for _, plr in ipairs(Players:GetPlayers()) do
local char = plr.Character
if char and char ~= NPC then
local hum, root = getModelHumanoidRoot(char)
if isAliveHumanoid(hum) then
local diff = root.Position - hrpPos
local d2 = diff:Dot(diff)
if d2 < bestDistSq then
bestDistSq = d2
bestHum = hum
bestRoot = root
end
end
end
end
return bestHum, bestRoot, (bestDistSq < math.huge) and math.sqrt(bestDistSq) or math.huge
end
local function buildPath()
return PathfindingService:CreatePath({
AgentRadius = 3,
AgentHeight = 5,
AgentCanJump = true,
AgentCanClimb = true,
WaypointSpacing = 4,
})
end
local lastJumpAt = 0.0
local lastTeleportAt = 0.0
local function trySafeNudge()
local now = os.clock()
if now - lastTeleportAt < UNSTUCK_TELEPORT_COOLDOWN then
return false
end
local base = HRP.Position
local dirs = {
Vector3.new(1,0,0), Vector3.new(-1,0,0),
Vector3.new(0,0,1), Vector3.new(0,0,-1),
Vector3.new(1,0,1).Unit, Vector3.new(-1,0,1).Unit,
Vector3.new(1,0,-1).Unit, Vector3.new(-1,0,-1).Unit,
}
local radii = { 3.5, 5.5, 7.5 }
for _, r in ipairs(radii) do
for _, d in ipairs(dirs) do
local flat = Vector3.new(d.X, 0, d.Z)
local candXZ = base + flat * r
local ground = snapToGround(Vector3.new(candXZ.X, base.Y, candXZ.Z))
local gy = ground.Y
local y = gy + Humanoid.HipHeight + (HRP.Size.Y * 0.5) + 0.6
local cand = Vector3.new(candXZ.X, y, candXZ.Z)
local parts = workspace:GetPartBoundsInBox(CFrame.new(cand), Vector3.new(3, 5, 3), overlapParams)
local blocked = false
for _, p in ipairs(parts) do
if p and p:IsA("BasePart") and p.CanCollide then
if p.Position.Y > gy + 0.25 then
blocked = true
break
end
end
end
if not blocked then
lastTeleportAt = now
local cf = CFrame.new(cand, cand + HRP.CFrame.LookVector)
NPC:PivotTo(cf)
return true
end
end
end
return false
end
local function doUnstuck()
local now = os.clock()
if now - lastJumpAt >= UNSTUCK_JUMP_COOLDOWN then
lastJumpAt = now
Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
end
local right = HRP.CFrame.RightVector
local forward = HRP.CFrame.LookVector
local sideSign = (rng:NextInteger(0, 1) == 0) and -1 or 1
local side = Vector3.new(right.X, 0, right.Z) * (UNSTUCK_SIDESTEP * sideSign)
local back = Vector3.new(forward.X, 0, forward.Z) * (-UNSTUCK_BACKSTEP)
Humanoid:MoveTo(HRP.Position + side)
task.wait(0.12)
Humanoid:MoveTo(HRP.Position + back)
task.wait(0.12)
if trySafeNudge() then
task.wait(0.08)
end
end
local function followPathSlice(destination: Vector3, maxTime: number, shouldAbort: (() -> boolean)?)
local path = buildPath()
local ok = pcall(function()
path:ComputeAsync(HRP.Position, destination)
end)
if not ok or path.Status ~= Enum.PathStatus.Success then
return false
end
local waypoints = path:GetWaypoints()
if #waypoints == 0 then
return false
end
local blocked = false
local blockedConn = path.Blocked:Connect(function()
blocked = true
end)
local function cleanup()
if blockedConn then
blockedConn:Disconnect()
end
end
local started = os.clock()
local stuckT = 0.0
local lastPos = HRP.Position
for _, wp in ipairs(waypoints) do
if os.clock() - started >= maxTime then
cleanup()
return true
end
if shouldAbort and shouldAbort() then
cleanup()
return false
end
if blocked then
cleanup()
return false
end
if wp.Action == Enum.PathWaypointAction.Jump then
Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
end
local t = 0.0
while (HRP.Position - wp.Position).Magnitude > PATH_WP_REACH do
local dt = task.wait(0.05)
if os.clock() - started >= maxTime then
cleanup()
return true
end
if shouldAbort and shouldAbort() then
cleanup()
return false
end
if blocked then
cleanup()
return false
end
local moved = (HRP.Position - lastPos).Magnitude
lastPos = HRP.Position
if moved < STUCK_MIN_MOVE then
stuckT += dt
else
stuckT = 0
end
if stuckT >= STUCK_TIMEOUT then
cleanup()
doUnstuck()
return false
end
t += dt
if t >= PATH_MOVE_REISSUE then
t = 0
Humanoid:MoveTo(wp.Position)
elseif t <= 0.06 then
Humanoid:MoveTo(wp.Position)
end
end
end
cleanup()
return true
end
local function directChaseStep(targetRoot: BasePart, maxTime: number, shouldAbort: (() -> boolean)?)
local started = os.clock()
local stuckT = 0.0
local lastPos = HRP.Position
while os.clock() - started < maxTime do
if shouldAbort and shouldAbort() then
return false
end
local diff = targetRoot.Position - HRP.Position
if diff:Dot(diff) > dropRangeSq then
return false
end
local v = targetRoot.AssemblyLinearVelocity
local lead = Vector3.new(v.X, 0, v.Z) * LEAD_TIME
Humanoid:MoveTo(targetRoot.Position + lead)
local dt = task.wait(DIRECT_UPDATE_DT)
local moved = (HRP.Position - lastPos).Magnitude
lastPos = HRP.Position
if moved < STUCK_MIN_MOVE then
stuckT += dt
else
stuckT = 0
end
if stuckT >= STUCK_TIMEOUT then
doUnstuck()
return false
end
end
return true
end
local state = "PATROL"
local nextPatrolAt = os.clock() + rng:NextNumber(PATROL_MIN_DELAY, PATROL_MAX_DELAY)
local chaseHum: Humanoid? = nil
local chaseRoot: BasePart? = nil
local lastSeenAt = 0.0
local lastKnownPos = HRP.Position
local lastTargetEval = 0.0
local lastPathAt = 0.0
local lastPlannedPos = HRP.Position
local searchStartedAt = 0.0
local searchAnchor = HRP.Position
while isAliveHumanoid(Humanoid) do
local now = os.clock()
if state == "PATROL" then
Humanoid.WalkSpeed = WALK_SPEED
local hum, root, _ = pickNearestTarget()
if hum and root and canDetect(root) then
state = "CHASE"
chaseHum, chaseRoot = hum, root
lastSeenAt = now
lastKnownPos = root.Position
lastPathAt = 0
lastPlannedPos = root.Position
else
if now >= nextPatrolAt then
nextPatrolAt = now + rng:NextNumber(PATROL_MIN_DELAY, PATROL_MAX_DELAY)
local step = rng:NextNumber(PATROL_STEP_MIN, PATROL_STEP_MAX)
local dir = Vector3.new(rng:NextNumber(-1, 1), 0, rng:NextNumber(-1, 1))
if dir.Magnitude < 0.05 then
dir = Vector3.new(1, 0, 0)
end
dir = dir.Unit
local dest = HRP.Position + dir * step
dest = snapToGround(Vector3.new(dest.X, HRP.Position.Y, dest.Z))
local slice = rng:NextNumber(PATROL_SLICE_MIN, PATROL_SLICE_MAX)
local tries = PATROL_TRIES
while tries > 0 do
local th, tr, _ = pickNearestTarget()
if th and tr and canDetect(tr) then
state = "CHASE"
chaseHum, chaseRoot = th, tr
lastSeenAt = now
lastKnownPos = tr.Position
lastPathAt = 0
lastPlannedPos = tr.Position
break
end
local ok = followPathSlice(dest, slice, function()
local th2, tr2, _ = pickNearestTarget()
return th2 and tr2 and canDetect(tr2)
end)
if ok then
break
end
tries -= 1
local ndir = Vector3.new(rng:NextNumber(-1, 1), 0, rng:NextNumber(-1, 1))
if ndir.Magnitude < 0.05 then
ndir = Vector3.new(0, 0, 1)
end
ndir = ndir.Unit
dest = HRP.Position + ndir * step
dest = snapToGround(Vector3.new(dest.X, HRP.Position.Y, dest.Z))
end
end
end
elseif state == "CHASE" then
if not (chaseHum and chaseRoot and isAliveHumanoid(chaseHum)) then
state = "SEARCH"
searchStartedAt = now
searchAnchor = lastKnownPos
chaseHum, chaseRoot = nil, nil
else
if now - lastTargetEval >= TARGET_REEVALUATE_DT then
lastTargetEval = now
local nh, nr, _ = pickNearestTarget()
if nh and nr and isAliveHumanoid(nh) then
local curD = (chaseRoot.Position - HRP.Position).Magnitude
local newD = (nr.Position - HRP.Position).Magnitude
if newD + 3 < curD then
chaseHum, chaseRoot = nh, nr
end
end
end
local diff = chaseRoot.Position - HRP.Position
if diff:Dot(diff) > dropRangeSq then
state = "PATROL"
chaseHum, chaseRoot = nil, nil
nextPatrolAt = now + rng:NextNumber(PATROL_MIN_DELAY, PATROL_MAX_DELAY)
else
local los = (not REQUIRE_LINE_OF_SIGHT) or hasLineOfSight(chaseRoot)
if los then
lastSeenAt = now
end
lastKnownPos = chaseRoot.Position
if now - lastSeenAt > LOSE_TARGET_AFTER then
state = "SEARCH"
searchStartedAt = now
searchAnchor = lastKnownPos
chaseHum, chaseRoot = nil, nil
else
local targetSpeed = chaseHum.WalkSpeed
local desiredSpeed = math.clamp(math.max(CHASE_SPEED_MIN, targetSpeed + CHASE_SPEED_BUFFER), CHASE_SPEED_MIN, CHASE_SPEED_MAX)
Humanoid.WalkSpeed = desiredSpeed
if los then
directChaseStep(chaseRoot, DIRECT_UPDATE_DT, function()
return chaseRoot == nil or not chaseRoot.Parent
end)
else
if now - lastPathAt >= PATH_REPATH_COOLDOWN then
local planned = chaseRoot.Position
if (planned - lastPlannedPos).Magnitude >= PATH_TARGET_MOVE then
lastPlannedPos = planned
end
lastPathAt = now
followPathSlice(lastPlannedPos, 0.9, function()
if not (chaseHum and chaseRoot and isAliveHumanoid(chaseHum)) then
return true
end
if (chaseRoot.Position - HRP.Position).Magnitude > DROP_RANGE then
return true
end
if (not REQUIRE_LINE_OF_SIGHT) or hasLineOfSight(chaseRoot) then
return true
end
if (chaseRoot.Position - lastPlannedPos).Magnitude > PATH_TARGET_MOVE then
return true
end
return false
end)
else
task.wait(0.05)
end
end
end
end
end
elseif state == "SEARCH" then
Humanoid.WalkSpeed = WALK_SPEED
local hum, root, _ = pickNearestTarget()
if hum and root and canDetect(root) then
state = "CHASE"
chaseHum, chaseRoot = hum, root
lastSeenAt = now
lastKnownPos = root.Position
lastPathAt = 0
lastPlannedPos = root.Position
else
if now - searchStartedAt >= SEARCH_MAX_TIME then
state = "PATROL"
nextPatrolAt = now + rng:NextNumber(PATROL_MIN_DELAY, PATROL_MAX_DELAY)
else
local target = searchAnchor
if (HRP.Position - searchAnchor).Magnitude < 3.5 then
local a = rng:NextNumber(0, math.pi * 2)
local r = rng:NextNumber(4, SEARCH_WANDER_RADIUS)
local off = Vector3.new(math.cos(a) * r, 0, math.sin(a) * r)
target = searchAnchor + off
target = snapToGround(Vector3.new(target.X, HRP.Position.Y, target.Z))
end
followPathSlice(target, SEARCH_SLICE, function()
local th2, tr2, _ = pickNearestTarget()
return th2 and tr2 and canDetect(tr2)
end)
end
end
end
task.wait(0.03)
end
