Advertisement
TasosNotTacos

NPC System (For RoDevs)

Jul 7th, 2025
297
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 15.87 KB | Source Code | 0 0
  1. -- NPC System
  2. -- Main
  3. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  4. local CollectionService = game:GetService("CollectionService")
  5.  
  6. local Constants = require(script.Modules.Constants)
  7. local PositionGenerator = require(script.Modules.PositionGenerator)
  8. local NPCAnimationHandler = require(script.Modules.NPCAnimationHandler)
  9. local NPCSpawner = require(script.Modules.NPCSpawner)
  10.  
  11. local function initializeSystems()
  12.     local walkableAreas = workspace:WaitForChild("Walkable")
  13.     PositionGenerator:Initialize(walkableAreas)
  14.  
  15.     NPCSpawner:Initialize()
  16.  
  17.     local spawnCount = 0
  18.     for _, spawnPoint in pairs(CollectionService:GetTagged("elderlyspawn")) do
  19.         spawnCount += 1
  20.         spawnPoint:SetAttribute("SpawnID", spawnCount)
  21.         spawnPoint:SetAttribute("Current", 0)
  22.  
  23.         NPCSpawner:StartSpawnLoop(spawnPoint)
  24.     end
  25.  
  26.     print("NPC System initialized with", spawnCount, "spawn points")
  27. end
  28.  
  29. initializeSystems()
  30.  
  31. -- Constants (Module)
  32.  
  33. local Constants = {}
  34.  
  35. Constants.PUSH_FORCE = 25
  36. Constants.PUSH_UP_FORCE = 15
  37. Constants.BODY_VELOCITY_FORCE = Vector3.new(4000, 4000, 4000)
  38. Constants.PUSH_DURATION = 3
  39. Constants.PUSH_RECOVERY_TIME = 1
  40.  
  41. Constants.AGENT_RADIUS = 2.25
  42. Constants.AGENT_HEIGHT = 2
  43. Constants.AGENT_CAN_JUMP = false
  44. Constants.AGENT_CAN_CLIMP = false
  45. Constants.DESTINATION_THRESHOLD = 6
  46. Constants.WANDER_WAIT_TIME = {1, 4}
  47.  
  48. Constants.NPC_MIN_WORTH = 5
  49. Constants.NPC_MAX_WORTH = 15
  50.  
  51. Constants.PATH_COSTS = {
  52.     walkable = 0,
  53.     crossing = 0,
  54.     unwalkable = 500,
  55.     unwalkableINF = 80,
  56.     Car = math.huge,
  57. }
  58.  
  59. Constants.NPC_NAMES = {
  60.     Black = {
  61.         Male = {
  62.             "Malik",
  63.             "Jamal",
  64.             "DeShawn",
  65.             "Tyrone",
  66.             "Darius",
  67.         },
  68.         Female = {
  69.             "Aaliyah",
  70.             "Monique",
  71.             "Kiara",
  72.             "Latasha",
  73.             "Zuri",
  74.         },
  75.     },
  76.     White = {
  77.         Male = {
  78.             "Jake",
  79.             "Kyle",
  80.             "Brandon",
  81.             "Ryan",
  82.             "Chad",
  83.         },
  84.         Female = {
  85.             "Emily",
  86.             "Jessica",
  87.             "Ashley",
  88.             "Lauren",
  89.             "Hannah",
  90.         },
  91.     },
  92. }
  93. Constants.NPC_COLORS = {
  94.     Black = {
  95.         Color3.fromRGB(45, 34, 30),
  96.         Color3.fromRGB(60, 44, 38),
  97.         Color3.fromRGB(85, 60, 50),
  98.         Color3.fromRGB(100, 75, 60),
  99.         Color3.fromRGB(120, 90, 70),
  100.     },
  101.     White = {
  102.         Color3.fromRGB(245, 222, 179),
  103.         Color3.fromRGB(240, 210, 180),
  104.         Color3.fromRGB(225, 195, 160),
  105.         Color3.fromRGB(210, 180, 140),
  106.         Color3.fromRGB(195, 160, 130),
  107.     },
  108. }
  109. Constants.NPC_LAST_NAMES = {
  110.     Black = {
  111.         'West',
  112.         'Jefferson',
  113.         'Jackson',
  114.         'Robinson',
  115.         'Johnson',
  116.         'Carter',
  117.         'Walker',
  118.         'Freeman',
  119.         'Banks',
  120.         'Jenkins',
  121.     },
  122.     White = {
  123.         'Anderson',
  124.         'Thompson',
  125.         'Miller',
  126.         'Wilson',
  127.         'Taylor',
  128.         'Clark',
  129.         'Harris',
  130.         'Campbell',
  131.         'Mitchell',
  132.     },
  133. }
  134.  
  135. return Constants
  136.  
  137. -- Currency Manager (Module)
  138.  
  139. local CurrencyManager = {}
  140.  
  141. function CurrencyManager:AddCurrency(player, amount)
  142.     if not player or not amount or amount <= 0 then
  143.         return false
  144.     end
  145.  
  146.     local leaderstats = player:FindFirstChild("leaderstats")
  147.     if not leaderstats or not leaderstats:IsA("Folder") then
  148.         return false
  149.     end
  150.  
  151.     local currency = leaderstats:FindFirstChild("Granny Bucks")
  152.     if not currency then
  153.         return false
  154.     end
  155.  
  156.     currency.Value += amount
  157.     return true
  158. end
  159.  
  160. function CurrencyManager:HandleNPCRemoval(npc)
  161.     local userID = npc:GetAttribute("PushedBy")
  162.     if not userID then return end
  163.  
  164.     local success, player = pcall(function()
  165.         return game.Players:GetPlayerByUserId(userID)
  166.     end)
  167.  
  168.     if success and player then
  169.         local worth = npc:GetAttribute("Worth") or 0
  170.         self:AddCurrency(player, worth)
  171.     else
  172.         warn("Could not find player who pushed NPC (ID: " .. tostring(userID) .. ")")
  173.     end
  174. end
  175.  
  176. return CurrencyManager
  177.  
  178. -- Animations (Module)
  179.  
  180. local NPCAnimationHandler = {}
  181. local RunService = game:GetService("RunService")
  182.  
  183. function NPCAnimationHandler:SetupAnimations(npc)
  184.     local humanoid: Humanoid = npc:FindFirstChildOfClass("Humanoid")
  185.     local hrp: BasePart? = npc:WaitForChild("HumanoidRootPart")
  186.     if not humanoid or not hrp then return end
  187.  
  188.     RunService.Heartbeat:Connect(function()
  189.         if hrp.AssemblyLinearVelocity.Magnitude > .5 and not npc:GetAttribute("beingPushed") then
  190.             self:PlayWalkingAnimation(npc, humanoid)
  191.         else
  192.             self:StopAllAnimations(humanoid)
  193.         end
  194.     end)
  195. end
  196.  
  197. function NPCAnimationHandler:PlayWalkingAnimation(npc, humanoid)
  198.     local walkingAnimation = npc:FindFirstChild("Walking")
  199.     if not walkingAnimation then return end
  200.  
  201.     local animation = humanoid:LoadAnimation(walkingAnimation)
  202.     local runningAnimations = humanoid:GetPlayingAnimationTracks()
  203.  
  204.     if runningAnimations and #runningAnimations > 0 then
  205.         for _, anim in pairs(runningAnimations) do
  206.             if not anim.IsPlaying then
  207.                 animation:Play()
  208.             end
  209.         end
  210.     else
  211.         animation:Play()
  212.     end
  213. end
  214.  
  215. function NPCAnimationHandler:StopAllAnimations(humanoid)
  216.     local runningAnimations = humanoid:GetPlayingAnimationTracks()
  217.     for _, animation in pairs(runningAnimations) do
  218.         if animation.IsPlaying then
  219.             animation:Stop()
  220.         end
  221.     end
  222. end
  223.  
  224. return NPCAnimationHandler
  225.  
  226. -- Pathfinding (Module)
  227.  
  228. local PathfindingService = game:GetService("PathfindingService")
  229. local Constants = require(script.Parent.Constants)
  230. local PositionGenerator = require(script.Parent.PositionGenerator)
  231.  
  232. local NPCPathfinding = {}
  233.  
  234. function NPCPathfinding:StartWandering(npc)
  235.     local humanoid = npc:WaitForChild("Humanoid")
  236.     local rootPart = npc:WaitForChild("HumanoidRootPart")
  237.  
  238.     task.spawn(function()
  239.         while npc.Parent and humanoid.Parent and rootPart.Parent do
  240.             local destination = PositionGenerator:GetRandomPosition()
  241.             self:MoveToDestination(humanoid, rootPart, destination)
  242.  
  243.             local waitTime = math.random(Constants.WANDER_WAIT_TIME[1], Constants.WANDER_WAIT_TIME[2])
  244.             task.wait(waitTime)
  245.         end
  246.     end)
  247. end
  248.  
  249. function NPCPathfinding:MoveToDestination(humanoid, rootPart, destination)
  250.     local reached = false
  251.  
  252.     while not reached and rootPart.Parent do
  253.         local path = PathfindingService:CreatePath({
  254.             AgentRadius = Constants.AGENT_RADIUS,
  255.             AgentHeight = Constants.AGENT_HEIGHT,
  256.             AgentCanJump = Constants.AGENT_CAN_JUMP,
  257.             AgentCanClimb = Constants.AGENT_CAN_CLIMP,
  258.             Costs = Constants.PATH_COSTS
  259.         })
  260.  
  261.         local success, errorMessage = pcall(function()
  262.             path:ComputeAsync(rootPart.Position, destination)
  263.         end)
  264.  
  265.         if success and path.Status == Enum.PathStatus.Success then
  266.             local waypoints = path:GetWaypoints()
  267.  
  268.             for _, waypoint in ipairs(waypoints) do
  269.                 if not rootPart.Parent then break end
  270.  
  271.                 if (rootPart.Position - destination).Magnitude < Constants.DESTINATION_THRESHOLD then
  272.                     reached = true
  273.                     break
  274.                 end
  275.  
  276.                 humanoid:MoveTo(waypoint.Position)
  277.                 local reachedWaypoint = humanoid.MoveToFinished:Wait()
  278.                 if not reachedWaypoint then
  279.                     break
  280.                 end
  281.             end
  282.         else
  283.             warn("Path creation failed: ", path.Status or errorMessage)
  284.             break
  285.         end
  286.  
  287.         if (rootPart.Position - destination).Magnitude < Constants.DESTINATION_THRESHOLD then
  288.             reached = true
  289.         end
  290.  
  291.         task.wait(0.1)
  292.     end
  293. end
  294.  
  295. return NPCPathfinding
  296.  
  297. -- Push Physics (Module)
  298.  
  299. local Constants = require(script.Parent.Constants)
  300.  
  301. local NPCPhysics = {}
  302.  
  303. function NPCPhysics:PushNPC(npc, player)
  304.     local humanoid = npc:FindFirstChildOfClass("Humanoid")
  305.     local rootPart = npc:FindFirstChild("HumanoidRootPart")
  306.     local playerCharacter = player.Character
  307.  
  308.     if not self:ValidatePushConditions(humanoid, rootPart, playerCharacter) then
  309.         return false
  310.     end
  311.  
  312.     local playerRootPart = playerCharacter:FindFirstChild("HumanoidRootPart")
  313.     if not playerRootPart then return false end
  314.  
  315.     rootPart:SetAttribute("beingPushed", true)
  316.     npc:SetAttribute("PushedBy", player.UserId)
  317.  
  318.     local direction = (rootPart.Position - playerRootPart.Position).Unit
  319.     local constraints, motors = self:SetupRagdoll(npc)
  320.  
  321.     self:ApplyPushForce(rootPart, direction)
  322.     self:ScheduleRecovery(npc, humanoid, constraints, motors)
  323.  
  324.     return true
  325. end
  326.  
  327. function NPCPhysics:ValidatePushConditions(humanoid, rootPart, playerCharacter)
  328.     if not humanoid or not rootPart or not playerCharacter then
  329.         return false
  330.     end
  331.     if rootPart:GetAttribute("beingPushed") then
  332.         return false
  333.     end
  334.     return true
  335. end
  336.  
  337. function NPCPhysics:SetupRagdoll(npc)
  338.     local constraints = {}
  339.     local motors = {}
  340.  
  341.     for _, motor in pairs(npc:GetDescendants()) do
  342.         if motor:IsA("Motor6D") and motor.Name ~= "Root" then
  343.             motors[motor] = {
  344.                 enabled = motor.Enabled,
  345.                 part0 = motor.Part0,
  346.                 part1 = motor.Part1,
  347.                 c0 = motor.C0,
  348.                 c1 = motor.C1
  349.             }
  350.  
  351.             local constraint = self:CreateConstraintForMotor(motor, npc)
  352.             if constraint then
  353.                 table.insert(constraints, constraint)
  354.             end
  355.  
  356.             motor.Enabled = false
  357.         end
  358.     end
  359.  
  360.     return constraints, motors
  361. end
  362.  
  363. function NPCPhysics:CreateConstraintForMotor(motor, npc)
  364.     local constraint = Instance.new("BallSocketConstraint")
  365.     constraint.Attachment0 = motor.Part0 and motor.Part0:FindFirstChild(motor.Name.."RigAttachment")
  366.     constraint.Attachment1 = motor.Part1 and motor.Part1:FindFirstChild(motor.Name.."Attachment")
  367.  
  368.     if not constraint.Attachment0 and motor.Part0 then
  369.         local att = Instance.new("Attachment")
  370.         att.Name = motor.Name.."RigAttachment"
  371.         att.CFrame = motor.C0
  372.         att.Parent = motor.Part0
  373.         constraint.Attachment0 = att
  374.     end
  375.  
  376.     if not constraint.Attachment1 and motor.Part1 then
  377.         local att = Instance.new("Attachment")
  378.         att.Name = motor.Name.."Attachment"
  379.         att.CFrame = motor.C1
  380.         att.Parent = motor.Part1
  381.         constraint.Attachment1 = att
  382.     end
  383.  
  384.     if constraint.Attachment0 and constraint.Attachment1 then
  385.         constraint.Parent = npc
  386.         return constraint
  387.     end
  388.  
  389.     return nil
  390. end
  391.  
  392. function NPCPhysics:ApplyPushForce(rootPart, direction)
  393.     local humanoid = rootPart.Parent:FindFirstChildOfClass("Humanoid")
  394.     if humanoid then
  395.         humanoid:ChangeState(Enum.HumanoidStateType.Physics)
  396.         humanoid.PlatformStand = true
  397.     end
  398.  
  399.     local bodyVelocity = Instance.new("BodyVelocity")
  400.     bodyVelocity.MaxForce = Constants.BODY_VELOCITY_FORCE
  401.     bodyVelocity.Velocity = direction * Constants.PUSH_FORCE + Vector3.new(0, Constants.PUSH_UP_FORCE, 0)
  402.     bodyVelocity.Parent = rootPart
  403.  
  404.     game:GetService("Debris"):AddItem(bodyVelocity, 0.1)
  405. end
  406.  
  407. function NPCPhysics:ScheduleRecovery(npc, humanoid, constraints, motors)
  408.     task.spawn(function()
  409.         task.wait(Constants.PUSH_DURATION)
  410.  
  411.         for _, constraint in pairs(constraints) do
  412.             if constraint.Parent then
  413.                 constraint:Destroy()
  414.             end
  415.         end
  416.  
  417.         for motor, info in pairs(motors) do
  418.             if motor.Parent then
  419.                 motor.Enabled = info.enabled
  420.             end
  421.         end
  422.  
  423.         if humanoid.Parent then
  424.             humanoid.PlatformStand = false
  425.             humanoid:ChangeState(Enum.HumanoidStateType.Running)
  426.         end
  427.  
  428.         task.wait(Constants.PUSH_RECOVERY_TIME)
  429.  
  430.         local rootPart = npc:FindFirstChild("HumanoidRootPart")
  431.         if rootPart and rootPart.Parent then
  432.             rootPart:SetAttribute("beingPushed", nil)
  433.             npc:SetAttribute("PushedBy", nil)
  434.         end
  435.     end)
  436. end
  437.  
  438. return NPCPhysics
  439.  
  440. -- NPC Spawn (Module)
  441.  
  442. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  443. local Constants = require(script.Parent.Constants)
  444. local NPCAnimationHandler = require(script.Parent.NPCAnimationHandler)
  445. local NPCPathfinding = require(script.Parent.NPCPathfinding)
  446. local NPCPhysics = require(script.Parent.NPCPhysics)
  447. local CurrencyManager = require(script.Parent.CurrencyManager)
  448.  
  449. local NPCSpawner = {}
  450.  
  451. function NPCSpawner:Initialize()
  452.     self.wNPCs = workspace:WaitForChild("NPCs")
  453.     self.npcFolder = ReplicatedStorage:WaitForChild("NPCs")
  454.     self.onNPCCreated = ReplicatedStorage:WaitForChild("OnNPCCreated")
  455.     self.pushButton = ReplicatedStorage:WaitForChild("PushButton")
  456.  
  457.     self.currentNPCs = 0
  458.     self.spawnPoints = {}
  459. end
  460.  
  461. function NPCSpawner:SpawnNPC(spawnPoint, count)
  462.     if not spawnPoint or count <= 0 then return end
  463.  
  464.     spawnPoint:SetAttribute("Current", spawnPoint:GetAttribute("Current") + count)
  465.     self.currentNPCs += count
  466.  
  467.     local npcOptions = self.npcFolder:GetChildren()
  468.     if #npcOptions == 0 then
  469.         warn("No NPC models found in ReplicatedStorage!")
  470.         return
  471.     end
  472.  
  473.     for i = 1, count do
  474.         local npcModel = npcOptions[math.random(1, #npcOptions)]:Clone()
  475.         self:SetupNPC(npcModel, spawnPoint)
  476.     end
  477. end
  478.  
  479. function NPCSpawner:SetupNPC(npc, spawnPoint)
  480.     npc.Parent = self.wNPCs
  481.     npc:SetAttribute("SpawnedBy", spawnPoint:GetAttribute("SpawnID"))
  482.     npc:SetAttribute("Worth", math.random(Constants.NPC_MIN_WORTH, Constants.NPC_MAX_WORTH))
  483.  
  484.     local humanoid = npc:FindFirstChildOfClass("Humanoid")
  485.     if humanoid then
  486.         humanoid.Died:Connect(function()
  487.             self:RemoveNPC(spawnPoint, npc)
  488.         end)
  489.     end
  490.  
  491.     local bodyColors: BodyColors = npc:FindFirstChildOfClass("BodyColors")
  492.    
  493.     local color = math.random(1,2) if color == 1 then color = "Black" else color = "White" end
  494.     local gender = npc:GetAttribute("Gender")
  495.     local firstName = Constants.NPC_NAMES[color][gender][math.random(1, #Constants.NPC_NAMES[color][gender])]
  496.     local lastName = Constants.NPC_LAST_NAMES[color][math.random(1, #Constants.NPC_LAST_NAMES[color])]
  497.    
  498.     local skinColor = Constants.NPC_COLORS[color][math.random(1, #Constants.NPC_COLORS[color])]
  499.    
  500.     npc.Name = firstName.." "..lastName
  501.     for _, prop in ipairs({"HeadColor3", "LeftArmColor3", "RightArmColor3", "LeftLegColor3", "RightLegColor3", "TorsoColor3"}) do
  502.         bodyColors[prop] = skinColor
  503.     end
  504.  
  505.     local screamSound: Sound = ReplicatedStorage:WaitForChild("CrashSounds")["SCREAM "..math.random(1,3).." "..gender]:Clone()
  506.     screamSound.Parent = npc
  507.     screamSound.Name = "ScreamSound"
  508.  
  509.     local pushButton = self.pushButton:Clone()
  510.     pushButton.Parent = npc.UpperTorso
  511.     pushButton.Triggered:Connect(function(player)
  512.         if player.Character and player.Character.PrimaryPart then
  513.             NPCPhysics:PushNPC(npc, player)
  514.         end
  515.     end)
  516.  
  517.     NPCAnimationHandler:SetupAnimations(npc)
  518.  
  519.     self:PositionNPC(npc, spawnPoint)
  520.  
  521.     NPCPathfinding:StartWandering(npc)
  522.  
  523.     self.onNPCCreated:FireAllClients(npc)
  524. end
  525.  
  526. function NPCSpawner:PositionNPC(npc, spawnPoint)
  527.     local offsetX = math.random(-(spawnPoint.Size.X/2), spawnPoint.Size.X/2)
  528.     local offsetZ = math.random(-(spawnPoint.Size.Z/2), spawnPoint.Size.Z/2)
  529.     local offset = CFrame.new(offsetX, 1, offsetZ)
  530.  
  531.     npc:SetPrimaryPartCFrame(spawnPoint.CFrame * offset)
  532. end
  533.  
  534. function NPCSpawner:RemoveNPC(spawnPoint, npc)
  535.     if not npc or not npc.Parent then return end
  536.  
  537.     CurrencyManager:HandleNPCRemoval(npc)
  538.  
  539.     spawnPoint:SetAttribute("Current", math.max(0, spawnPoint:GetAttribute("Current") - 1))
  540.     self.currentNPCs = math.max(0, self.currentNPCs - 1)
  541.  
  542.     task.wait(0.5)
  543.     npc:Destroy()
  544. end
  545.  
  546. function NPCSpawner:StartSpawnLoop(spawnPoint)
  547.     task.spawn(function()
  548.         while spawnPoint.Parent do
  549.             local min = spawnPoint:GetAttribute("Min") or 0
  550.             local max = spawnPoint:GetAttribute("Max") or 0
  551.             local current = spawnPoint:GetAttribute("Current") or 0
  552.             local spawnTime = spawnPoint:GetAttribute("SpawnTime") or 5
  553.  
  554.             if current < min then
  555.                 self:SpawnNPC(spawnPoint, min - current)
  556.             end
  557.  
  558.             if current < max then
  559.                 self:SpawnNPC(spawnPoint, 1)
  560.                 task.wait(spawnTime)
  561.             else
  562.                 task.wait(1)
  563.             end
  564.         end
  565.     end)
  566. end
  567.  
  568. return NPCSpawner
  569.  
  570. -- Random Position Generator (Module)
  571.  
  572. local PositionGenerator = {}
  573.  
  574. local allGreen = {}
  575. local greenKeys = {}
  576.  
  577. function PositionGenerator:Initialize(greenFolder)
  578.     local num = 0
  579.     for _, wall in pairs(greenFolder:GetChildren()) do
  580.         num += 1
  581.         local sizeX = wall.Size.X
  582.         local positionX = wall.CFrame.Position.X
  583.         local sizeZ = wall.Size.Z
  584.         local positionZ = wall.CFrame.Position.Z
  585.  
  586.         local key = wall.Name .. tostring(num)
  587.         allGreen[key] = {
  588.             sizeX = sizeX,
  589.             sizeZ = sizeZ,
  590.             positionX = positionX,
  591.             positionZ = positionZ
  592.         }
  593.         table.insert(greenKeys, key)
  594.     end
  595. end
  596.  
  597. function PositionGenerator:GetRandomPosition()
  598.     if #greenKeys == 0 then
  599.         warn("No walkable areas defined!")
  600.         return Vector3.new(0, 1, 0)
  601.     end
  602.  
  603.     local randomKey = greenKeys[math.random(1, #greenKeys)]
  604.     local data = allGreen[randomKey]
  605.  
  606.     local offsetX = (math.random() - 0.5) * data.sizeX
  607.     local offsetZ = (math.random() - 0.5) * data.sizeZ
  608.  
  609.     return Vector3.new(data.positionX + offsetX, 1, data.positionZ + offsetZ)
  610. end
  611.  
  612. return PositionGenerator
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement