NPC Script
Key RequiredUniversal

NPC Script

Universal ScriptKiritopetKiritopet 1.4K views 161 executesUpdated Jul 8, 2026
Unrated
00
Lua Script•646 lines (23088 chars)
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local UserInputService = game:GetService("UserInputService")

local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local rootPart = character:WaitForChild("HumanoidRootPart")

local moveDirection = Vector3.new(math.random() * 2 - 1, 0, math.random() * 2 - 1).Unit
local isStanding = false
local standStart = 0
local standDuration = 0
local walkDuration = 0
local walkUntil = 0
local checkCooldown = 0
local centerPosition = nil
local wanderRadius = 0
local lastPositions = {}
local stuckDetected = false
local stuckCounter = 0
local maxStuckTime = 3
local obstacleMemory = {}
local lastObstacleTime = 0
local obstacleMemoryDuration = 5
local emotionalState = "neutral"
local emotionalStateTimer = math.random(50, 150) / 10
local emotionalStateDuration = math.random(50, 150) / 10
local curiosityLevel = 0.5
local socialAwareness = 0.5
local personalityTraits = {}
local frameCount = 0
local lastFrameUpdate = 0
local utilityScores = {}
local nearbyPlayers = {}
local playerDetectionRange = 15
local personalSpace = 4
local imitationChance = 0.3
local curiosityTarget = nil
local curiosityTimer = 0
local boredomLevel = 0.3

local npcEnabled = true

personalityTraits = {
    patience = 0.7 + math.random() * 0.25,
    sociability = 0.2 + math.random() * 0.5,
    curiosity = 0.3 + math.random() * 0.5,
    caution = 0.4 + math.random() * 0.5,
    energy = 0.5 + math.random() * 0.4,
    consistency = 0.6 + math.random() * 0.35,
    playfulness = 0.3 + math.random() * 0.5,
    anxiety = 0.1 + math.random() * 0.4,
    focus = 0.5 + math.random() * 0.4,
    adaptability = 0.4 + math.random() * 0.5
}

local ScreenGui = Instance.new("ScreenGui")
ScreenGui.Name = "NPCControl"
ScreenGui.ResetOnSpawn = false
ScreenGui.IgnoreGuiInset = true

pcall(function()
    if player.PlayerGui:FindFirstChild("NPCControl") then
        player.PlayerGui:FindFirstChild("NPCControl"):Destroy()
    end
end)

ScreenGui.Parent = player.PlayerGui

local function createBorder(parent, zIndex)
    local left = Instance.new("Frame")
    left.Size = UDim2.new(0, 1, 1, 0)
    left.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
    left.BorderSizePixel = 0
    left.ZIndex = zIndex
    left.Active = false
    left.Parent = parent

    local top = Instance.new("Frame")
    top.Size = UDim2.new(1, 0, 0, 1)
    top.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
    top.BorderSizePixel = 0
    top.ZIndex = zIndex
    top.Active = false
    top.Parent = parent

    local right = Instance.new("Frame")
    right.Size = UDim2.new(0, 1, 1, 0)
    right.Position = UDim2.new(1, -1, 0, 0)
    right.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
    right.BorderSizePixel = 0
    right.ZIndex = zIndex
    right.Active = false
    right.Parent = parent

    local bottom = Instance.new("Frame")
    bottom.Size = UDim2.new(1, 0, 0, 1)
    bottom.Position = UDim2.new(0, 0, 1, -1)
    bottom.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
    bottom.BorderSizePixel = 0
    bottom.ZIndex = zIndex
    bottom.Active = false
    bottom.Parent = parent
end

local mainFrame = Instance.new("Frame")
mainFrame.Size = UDim2.new(0, 240, 0, 50)
mainFrame.Position = UDim2.new(0.5, -120, 0, 50)
mainFrame.BackgroundColor3 = Color3.fromRGB(12, 12, 12)
mainFrame.BorderSizePixel = 0
mainFrame.ZIndex = 100
mainFrame.ClipsDescendants = true
mainFrame.Parent = ScreenGui

createBorder(mainFrame, 200)

local titleBar = Instance.new("Frame")
titleBar.Size = UDim2.new(1, -2, 0, 22)
titleBar.Position = UDim2.new(0, 1, 0, 1)
titleBar.BackgroundColor3 = Color3.fromRGB(12, 12, 12)
titleBar.BorderSizePixel = 0
titleBar.ZIndex = 101
titleBar.Parent = mainFrame

local titleLabel = Instance.new("TextLabel")
titleLabel.Size = UDim2.new(1, -30, 1, 0)
titleLabel.Position = UDim2.new(0, 8, 0, 0)
titleLabel.BackgroundTransparency = 1
titleLabel.Text = "NPC | Control"
titleLabel.TextColor3 = Color3.fromRGB(180, 180, 180)
titleLabel.Font = Enum.Font.Code
titleLabel.TextSize = 12
titleLabel.TextXAlignment = Enum.TextXAlignment.Left
titleLabel.ZIndex = 102
titleLabel.Parent = titleBar

local collapseButton = Instance.new("TextButton")
collapseButton.Size = UDim2.new(0, 22, 0, 22)
collapseButton.Position = UDim2.new(1, -22, 0, 0)
collapseButton.BackgroundColor3 = Color3.fromRGB(25, 25, 25)
collapseButton.BorderSizePixel = 0
collapseButton.Text = "-"
collapseButton.TextColor3 = Color3.fromRGB(200, 200, 200)
collapseButton.Font = Enum.Font.Code
collapseButton.TextSize = 14
collapseButton.ZIndex = 102
collapseButton.Parent = titleBar

createBorder(collapseButton, 103)

local titleDivider = Instance.new("Frame")
titleDivider.Size = UDim2.new(1, -2, 0, 1)
titleDivider.Position = UDim2.new(0, 1, 0, 23)
titleDivider.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
titleDivider.BorderSizePixel = 0
titleDivider.ZIndex = 200
titleDivider.Parent = mainFrame

local contentFrame = Instance.new("Frame")
contentFrame.Size = UDim2.new(1, -2, 1, -25)
contentFrame.Position = UDim2.new(0, 1, 0, 24)
contentFrame.BackgroundTransparency = 1
contentFrame.ZIndex = 101
contentFrame.Parent = mainFrame

local isDragging = false
local dragStart = nil
local frameStartPos = nil

local function startDrag(input)
    isDragging = true
    dragStart = input.Position
    frameStartPos = mainFrame.Position
end

local function stopDrag()
    isDragging = false
end

titleBar.InputBegan:Connect(function(input)
    if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
        startDrag(input)
    end
end)

titleBar.InputEnded:Connect(function(input)
    if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
        stopDrag()
    end
end)

titleLabel.InputBegan:Connect(function(input)
    if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
        startDrag(input)
    end
end)

titleLabel.InputEnded:Connect(function(input)
    if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
        stopDrag()
    end
end)

UserInputService.InputChanged:Connect(function(input)
    if isDragging and (input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseMovement) then
        local delta = input.Position - dragStart
        mainFrame.Position = UDim2.new(
            frameStartPos.X.Scale,
            frameStartPos.X.Offset + delta.X,
            frameStartPos.Y.Scale,
            frameStartPos.Y.Offset + delta.Y
        )
    end
end)

UserInputService.InputEnded:Connect(function(input)
    if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
        stopDrag()
    end
end)

collapseButton.MouseButton1Click:Connect(function()
    if mainFrame.Size.Y.Offset > 24 then
        mainFrame:TweenSize(UDim2.new(0, 240, 0, 24), "Out", "Quad", 0.2, true)
        collapseButton.Text = "+"
        contentFrame.Visible = false
    else
        mainFrame:TweenSize(UDim2.new(0, 240, 0, 50), "Out", "Quad", 0.2, true)
        collapseButton.Text = "-"
        contentFrame.Visible = true
    end
end)

local function createToggle(toggleText, yPosition, default, callback)
    local button = Instance.new("TextButton")
    button.Size = UDim2.new(0, 210, 0, 22)
    button.Position = UDim2.new(0.5, -105, 0, yPosition)
    button.BackgroundColor3 = default and Color3.fromRGB(25, 45, 25) or Color3.fromRGB(30, 30, 30)
    button.Text = toggleText .. ": " .. (default and "ON" or "OFF")
    button.TextColor3 = Color3.fromRGB(200, 200, 200)
    button.Font = Enum.Font.Code
    button.TextSize = 11
    button.BorderSizePixel = 0
    button.ZIndex = 150
    button.Active = true
    button.Selectable = true
    button.Parent = contentFrame
    createBorder(button, 149)

    local state = default
    local debounce = false
    local function toggle()
        if debounce then return end
        debounce = true
        state = not state
        button.Text = toggleText .. ": " .. (state and "ON" or "OFF")
        button.BackgroundColor3 = state and Color3.fromRGB(25, 45, 25) or Color3.fromRGB(30, 30, 30)
        callback(state)
        task.delay(0.3, function()
            debounce = false
        end)
    end

    button.Activated:Connect(toggle)
    return button
end

createToggle("NPC MODE", 2, true, function(state)
    npcEnabled = state
    if not npcEnabled and humanoid then
        humanoid:MoveTo(rootPart.Position)
    end
end)

local function isValidCharacter()
    if not character or not character.Parent then return false end
    if not humanoid or not humanoid.Parent then return false end
    if not rootPart or not rootPart.Parent then return false end
    if humanoid.Health <= 0 then return false end
    return true
end

local function findMapCenter()
    local parts = {}
    local children = Workspace:GetChildren()
    for _, child in ipairs(children) do
        if child:IsA("BasePart") and child.CanCollide and child.Transparency < 0.5 and child.Size.Magnitude > 3 then
            table.insert(parts, child)
        end
        if child:IsA("Model") then
            for _, descendant in ipairs(child:GetDescendants()) do
                if descendant:IsA("BasePart") and descendant.CanCollide and descendant.Transparency < 0.5 and descendant.Size.Magnitude > 3 then
                    table.insert(parts, descendant)
                end
            end
        end
    end
    if #parts == 0 then
        return rootPart.Position, 20
    end
    local minX, maxX = math.huge, -math.huge
    local minZ, maxZ = math.huge, -math.huge
    for _, part in ipairs(parts) do
        local pos = part.Position
        local size = part.Size
        minX = math.min(minX, pos.X - size.X / 2)
        maxX = math.max(maxX, pos.X + size.X / 2)
        minZ = math.min(minZ, pos.Z - size.Z / 2)
        maxZ = math.max(maxZ, pos.Z + size.Z / 2)
    end
    local center = Vector3.new((minX + maxX) / 2, rootPart.Position.Y, (minZ + maxZ) / 2)
    local radius = math.min((maxX - minX) / 2, (maxZ - minZ) / 2) * 0.6
    return center, math.max(radius, 10)
end

local function getRandomDirection()
    local angle = math.random() * math.pi * 2
    return Vector3.new(math.cos(angle), 0, math.sin(angle))
end

local function isBlocked(direction, distance)
    if not isValidCharacter() then return false end
    local rayParams = RaycastParams.new()
    rayParams.FilterType = Enum.RaycastFilterType.Blacklist
    rayParams.FilterDescendantsInstances = {character}
    local rayOrigin = rootPart.Position + Vector3.new(0, 1, 0)
    local rayResult = Workspace:Raycast(rayOrigin, direction * distance, rayParams)
    if not rayResult then return false end
    local hit = rayResult.Instance
    if hit and hit.CanCollide and hit.Transparency < 0.5 and hit.Size.Magnitude > 0.5 then
        return true
    end
    if hit and hit.Parent then
        local hitPlayer = Players:GetPlayerFromCharacter(hit.Parent)
        if hitPlayer then return true end
    end
    return false
end

local function isEdgeAhead(direction, distance)
    if not isValidCharacter() then return false end
    local checkPosition = rootPart.Position + direction * distance + Vector3.new(0, 0.5, 0)
    local rayParams = RaycastParams.new()
    rayParams.FilterType = Enum.RaycastFilterType.Blacklist
    rayParams.FilterDescendantsInstances = {character}
    local rayOrigin = checkPosition + Vector3.new(0, 3, 0)
    local rayResult = Workspace:Raycast(rayOrigin, Vector3.new(0, -15, 0), rayParams)
    if not rayResult then return true end
    local hitPart = rayResult.Instance
    if hitPart and (not hitPart.CanCollide or hitPart.Transparency >= 0.5) then return true end
    local heightDiff = math.abs(rootPart.Position.Y - rayResult.Position.Y)
    if heightDiff > 4 then return true end
    return false
end

local function getDirectionAwayFromBarrier()
    if not isValidCharacter() then return -moveDirection end
    local rayParams = RaycastParams.new()
    rayParams.FilterType = Enum.RaycastFilterType.Blacklist
    rayParams.FilterDescendantsInstances = {character}
    local bestDir = nil
    local bestDist = 0
    for i = 1, 36 do
        local angle = math.random() * math.pi * 2
        local dir = Vector3.new(math.cos(angle), 0, math.sin(angle))
        local rayOrigin = rootPart.Position + Vector3.new(0, 1, 0)
        local rayResult = Workspace:Raycast(rayOrigin, dir * 10, rayParams)
        local dist = rayResult and (rayResult.Position - rayOrigin).Magnitude or 10
        if dist > bestDist and not isEdgeAhead(dir, 5) then
            bestDist = dist
            bestDir = dir
        end
    end
    if bestDir then return bestDir end
    local perpDir = moveDirection:Cross(Vector3.new(0, 1, 0))
    if math.random() < 0.5 then perpDir = -perpDir end
    local newDir = (-moveDirection + perpDir).Unit
    if isEdgeAhead(newDir, 5) or isBlocked(newDir, 3) then
        newDir = -moveDirection
    end
    return newDir
end

local function getWanderDirection()
    if not centerPosition then
        centerPosition, wanderRadius = findMapCenter()
    end
    if not isValidCharacter() then return getRandomDirection() end
    local toCenter = centerPosition - rootPart.Position
    toCenter = Vector3.new(toCenter.X, 0, toCenter.Z)
    local distToCenter = toCenter.Magnitude
    if distToCenter > wanderRadius then
        return toCenter.Unit
    else
        local baseAngle = math.atan2(toCenter.Z, toCenter.X)
        local angleVariation = math.random() * math.pi / 3 - math.pi / 6
        local directionChoice = math.random()
        local newAngle
        if directionChoice < 0.33 then
            newAngle = baseAngle + math.pi / 2 + angleVariation
        elseif directionChoice < 0.66 then
            newAngle = baseAngle - math.pi / 2 + angleVariation
        else
            newAngle = baseAngle + angleVariation
        end
        local newDir = Vector3.new(math.cos(newAngle), 0, math.sin(newAngle))
        if isEdgeAhead(newDir, 5) then
            newDir = getDirectionAwayFromBarrier()
        end
        return newDir
    end
end

local function checkStuck()
    if not isValidCharacter() then return end
    table.insert(lastPositions, rootPart.Position)
    if #lastPositions > 30 then table.remove(lastPositions, 1) end
    if #lastPositions >= 30 then
        local totalMovement = 0
        for i = 2, #lastPositions do
            totalMovement = totalMovement + (lastPositions[i] - lastPositions[i-1]).Magnitude
        end
        if totalMovement < 0.5 then
            stuckCounter = stuckCounter + 0.1
            if stuckCounter >= maxStuckTime then
                stuckDetected = true
                stuckCounter = 0
            end
        else
            stuckCounter = math.max(0, stuckCounter - 0.2)
            stuckDetected = false
        end
    end
end

local function updateNearbyPlayers()
    nearbyPlayers = {}
    if not isValidCharacter() then return end
    for _, plr in ipairs(Players:GetPlayers()) do
        if plr ~= player then
            local char = plr.Character
            if char then
                local hrp = char:FindFirstChild("HumanoidRootPart")
                if hrp then
                    local dist = (rootPart.Position - hrp.Position).Magnitude
                    if dist < playerDetectionRange then
                        table.insert(nearbyPlayers, {player = plr, distance = dist, position = hrp.Position})
                    end
                end
            end
        end
    end
end

local function updateBehaviorTree()
    utilityScores = {}
    if not isValidCharacter() then
        utilityScores.idleStand = 10
        return "idleStand"
    end
    utilityScores.avoidEdge = (isEdgeAhead(moveDirection, 4) and not isStanding) and 11 or 0
    utilityScores.avoidObstacle = (isBlocked(moveDirection, 4) and not isStanding) and 10 or 0
    utilityScores.escapeStuck = stuckDetected and 9 or 0
    utilityScores.returnToCenter = (centerPosition and (centerPosition - rootPart.Position).Magnitude > wanderRadius) and 8 or 0
    utilityScores.wanderRandomly = (not isStanding and not stuckDetected) and 5 or 0
    utilityScores.idleStand = isStanding and 3 or 0
    utilityScores.explore = (emotionalState == "curious" or emotionalState == "energetic" or emotionalState == "playful") and 6 or 0
    utilityScores.socialInteraction = (socialAwareness > 0.5 and #nearbyPlayers > 0) and 4 or 0
    utilityScores.imitate = (math.random() < imitationChance and #nearbyPlayers > 0) and 2 or 0
    utilityScores.curious = (curiosityLevel > 0.7) and 7 or 0
    local highestUtility = 0
    local bestBehavior = "idleStand"
    for behavior, utility in pairs(utilityScores) do
        if utility > highestUtility then
            highestUtility = utility
            bestBehavior = behavior
        end
    end
    return bestBehavior
end

local function executeBehavior(behavior)
    if not isValidCharacter() then return end
    if behavior == "avoidEdge" then
        moveDirection = getDirectionAwayFromBarrier()
        isStanding = true
        standStart = tick()
        standDuration = 2
    elseif behavior == "avoidObstacle" then
        moveDirection = getDirectionAwayFromBarrier()
        if math.random() < personalityTraits.caution then
            isStanding = true
            standStart = tick()
            standDuration = 2
        end
    elseif behavior == "escapeStuck" then
        moveDirection = getDirectionAwayFromBarrier()
        isStanding = false
        walkDuration = math.random(20, 35) / 10
        walkUntil = tick() + walkDuration
        stuckDetected = false
        lastPositions = {}
    elseif behavior == "returnToCenter" then
        local toCenter = (centerPosition - rootPart.Position).Unit
        moveDirection = toCenter
        isStanding = false
        walkDuration = math.random(30, 50) / 10
        walkUntil = tick() + walkDuration
    elseif behavior == "wanderRandomly" then
        if not isStanding then
            local targetPos = rootPart.Position + moveDirection * 10
            humanoid:MoveTo(targetPos)
        end
    elseif behavior == "idleStand" then
        humanoid:MoveTo(rootPart.Position)
    elseif behavior == "explore" then
        if not isStanding then
            local targetPos = rootPart.Position + moveDirection * 15
            humanoid:MoveTo(targetPos)
        end
    elseif behavior == "socialInteraction" then
        if #nearbyPlayers > 0 then
            local target = nearbyPlayers[1]
            local dirToPlayer = (target.position - rootPart.Position).Unit
            if target.distance < personalSpace then
                moveDirection = getDirectionAwayFromBarrier()
            else
                moveDirection = dirToPlayer
            end
        end
    elseif behavior == "imitate" then
        if #nearbyPlayers > 0 and math.random() < 0.3 then
            local target = nearbyPlayers[math.random(1, #nearbyPlayers)]
            local targetChar = target.player.Character
            if targetChar then
                local targetHRP = targetChar:FindFirstChild("HumanoidRootPart")
                if targetHRP then
                    local targetVelocity = targetHRP.Velocity
                    if targetVelocity.Magnitude > 1 then
                        moveDirection = targetVelocity.Unit
                    end
                end
            end
        end
    elseif behavior == "curious" then
        if not curiosityTarget then
            local randomAngle = math.random() * math.pi * 2
            local randomDist = math.random(10, 30)
            curiosityTarget = rootPart.Position + Vector3.new(math.cos(randomAngle), 0, math.sin(randomAngle)) * randomDist
            curiosityTimer = math.random(30, 80) / 10
        end
        if curiosityTarget then
            moveDirection = (curiosityTarget - rootPart.Position).Unit
            curiosityTimer = curiosityTimer - 0.1
            if curiosityTimer <= 0 then
                curiosityTarget = nil
            end
        end
    end
end

local function onHeartbeat()
    if not isValidCharacter() then
        if player.Character and player.Character ~= character then
            character = player.Character
            humanoid = character:WaitForChild("Humanoid")
            rootPart = character:WaitForChild("HumanoidRootPart")
            centerPosition, wanderRadius = findMapCenter()
            moveDirection = getWanderDirection()
            isStanding = false
            walkDuration = math.random(25, 50) / 10
            walkUntil = tick() + walkDuration
            checkCooldown = 0
            lastPositions = {}
            stuckDetected = false
            stuckCounter = 0
            obstacleMemory = {}
        end
        return
    end
    if not npcEnabled then return end
    local now = tick()
    if now - lastFrameUpdate < 0.05 then return end
    lastFrameUpdate = now
    checkStuck()
    updateNearbyPlayers()
    local bestBehavior = updateBehaviorTree()
    executeBehavior(bestBehavior)
    if checkCooldown > now then return end
    if isStanding then
        humanoid:MoveTo(rootPart.Position)
        if now - standStart >= standDuration then
            isStanding = false
            moveDirection = getWanderDirection()
            walkDuration = math.random(25, 50) / 10
            walkUntil = now + walkDuration
        end
        return
    end
    if isEdgeAhead(moveDirection, 4) then
        moveDirection = getDirectionAwayFromBarrier()
        isStanding = true
        standStart = now
        standDuration = 2
        checkCooldown = now + 0.3
        return
    end
    if isBlocked(moveDirection, 4) then
        moveDirection = getDirectionAwayFromBarrier()
        isStanding = true
        standStart = now
        standDuration = 2
        checkCooldown = now + 0.3
        return
    end
    humanoid:MoveTo(rootPart.Position + moveDirection * 10)
    if now >= walkUntil then
        isStanding = true
        standStart = now
        standDuration = math.random(15, 30) / 10
    end
end

centerPosition, wanderRadius = findMapCenter()
moveDirection = getWanderDirection()
walkDuration = math.random(25, 50) / 10
walkUntil = tick() + walkDuration
RunService.Heartbeat:Connect(onHeartbeat)

player.CharacterAdded:Connect(function(newCharacter)
    character = newCharacter
    humanoid = character:WaitForChild("Humanoid")
    rootPart = character:WaitForChild("HumanoidRootPart")
    centerPosition, wanderRadius = findMapCenter()
    moveDirection = getWanderDirection()
    isStanding = false
    walkDuration = math.random(25, 50) / 10
    walkUntil = tick() + walkDuration
    checkCooldown = 0
    lastPositions = {}
    stuckDetected = false
    stuckCounter = 0
    obstacleMemory = {}
end)

Features & Information

NPC script
#No key

Community Comments (1)

kgbcatkgbcatJul 10, 2026

i may be confused but this script makes you just act like an npc?

More Scripts for Universal Scripts