Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- NPC System
- -- Main
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
- local CollectionService = game:GetService("CollectionService")
- local Constants = require(script.Modules.Constants)
- local PositionGenerator = require(script.Modules.PositionGenerator)
- local NPCAnimationHandler = require(script.Modules.NPCAnimationHandler)
- local NPCSpawner = require(script.Modules.NPCSpawner)
- local function initializeSystems()
- local walkableAreas = workspace:WaitForChild("Walkable")
- PositionGenerator:Initialize(walkableAreas)
- NPCSpawner:Initialize()
- local spawnCount = 0
- for _, spawnPoint in pairs(CollectionService:GetTagged("elderlyspawn")) do
- spawnCount += 1
- spawnPoint:SetAttribute("SpawnID", spawnCount)
- spawnPoint:SetAttribute("Current", 0)
- NPCSpawner:StartSpawnLoop(spawnPoint)
- end
- print("NPC System initialized with", spawnCount, "spawn points")
- end
- initializeSystems()
- -- Constants (Module)
- local Constants = {}
- Constants.PUSH_FORCE = 25
- Constants.PUSH_UP_FORCE = 15
- Constants.BODY_VELOCITY_FORCE = Vector3.new(4000, 4000, 4000)
- Constants.PUSH_DURATION = 3
- Constants.PUSH_RECOVERY_TIME = 1
- Constants.AGENT_RADIUS = 2.25
- Constants.AGENT_HEIGHT = 2
- Constants.AGENT_CAN_JUMP = false
- Constants.AGENT_CAN_CLIMP = false
- Constants.DESTINATION_THRESHOLD = 6
- Constants.WANDER_WAIT_TIME = {1, 4}
- Constants.NPC_MIN_WORTH = 5
- Constants.NPC_MAX_WORTH = 15
- Constants.PATH_COSTS = {
- walkable = 0,
- crossing = 0,
- unwalkable = 500,
- unwalkableINF = 80,
- Car = math.huge,
- }
- Constants.NPC_NAMES = {
- Black = {
- Male = {
- "Malik",
- "Jamal",
- "DeShawn",
- "Tyrone",
- "Darius",
- },
- Female = {
- "Aaliyah",
- "Monique",
- "Kiara",
- "Latasha",
- "Zuri",
- },
- },
- White = {
- Male = {
- "Jake",
- "Kyle",
- "Brandon",
- "Ryan",
- "Chad",
- },
- Female = {
- "Emily",
- "Jessica",
- "Ashley",
- "Lauren",
- "Hannah",
- },
- },
- }
- Constants.NPC_COLORS = {
- Black = {
- Color3.fromRGB(45, 34, 30),
- Color3.fromRGB(60, 44, 38),
- Color3.fromRGB(85, 60, 50),
- Color3.fromRGB(100, 75, 60),
- Color3.fromRGB(120, 90, 70),
- },
- White = {
- Color3.fromRGB(245, 222, 179),
- Color3.fromRGB(240, 210, 180),
- Color3.fromRGB(225, 195, 160),
- Color3.fromRGB(210, 180, 140),
- Color3.fromRGB(195, 160, 130),
- },
- }
- Constants.NPC_LAST_NAMES = {
- Black = {
- 'West',
- 'Jefferson',
- 'Jackson',
- 'Robinson',
- 'Johnson',
- 'Carter',
- 'Walker',
- 'Freeman',
- 'Banks',
- 'Jenkins',
- },
- White = {
- 'Anderson',
- 'Thompson',
- 'Miller',
- 'Wilson',
- 'Taylor',
- 'Clark',
- 'Harris',
- 'Campbell',
- 'Mitchell',
- },
- }
- return Constants
- -- Currency Manager (Module)
- local CurrencyManager = {}
- function CurrencyManager:AddCurrency(player, amount)
- if not player or not amount or amount <= 0 then
- return false
- end
- local leaderstats = player:FindFirstChild("leaderstats")
- if not leaderstats or not leaderstats:IsA("Folder") then
- return false
- end
- local currency = leaderstats:FindFirstChild("Granny Bucks")
- if not currency then
- return false
- end
- currency.Value += amount
- return true
- end
- function CurrencyManager:HandleNPCRemoval(npc)
- local userID = npc:GetAttribute("PushedBy")
- if not userID then return end
- local success, player = pcall(function()
- return game.Players:GetPlayerByUserId(userID)
- end)
- if success and player then
- local worth = npc:GetAttribute("Worth") or 0
- self:AddCurrency(player, worth)
- else
- warn("Could not find player who pushed NPC (ID: " .. tostring(userID) .. ")")
- end
- end
- return CurrencyManager
- -- Animations (Module)
- local NPCAnimationHandler = {}
- local RunService = game:GetService("RunService")
- function NPCAnimationHandler:SetupAnimations(npc)
- local humanoid: Humanoid = npc:FindFirstChildOfClass("Humanoid")
- local hrp: BasePart? = npc:WaitForChild("HumanoidRootPart")
- if not humanoid or not hrp then return end
- RunService.Heartbeat:Connect(function()
- if hrp.AssemblyLinearVelocity.Magnitude > .5 and not npc:GetAttribute("beingPushed") then
- self:PlayWalkingAnimation(npc, humanoid)
- else
- self:StopAllAnimations(humanoid)
- end
- end)
- end
- function NPCAnimationHandler:PlayWalkingAnimation(npc, humanoid)
- local walkingAnimation = npc:FindFirstChild("Walking")
- if not walkingAnimation then return end
- local animation = humanoid:LoadAnimation(walkingAnimation)
- local runningAnimations = humanoid:GetPlayingAnimationTracks()
- if runningAnimations and #runningAnimations > 0 then
- for _, anim in pairs(runningAnimations) do
- if not anim.IsPlaying then
- animation:Play()
- end
- end
- else
- animation:Play()
- end
- end
- function NPCAnimationHandler:StopAllAnimations(humanoid)
- local runningAnimations = humanoid:GetPlayingAnimationTracks()
- for _, animation in pairs(runningAnimations) do
- if animation.IsPlaying then
- animation:Stop()
- end
- end
- end
- return NPCAnimationHandler
- -- Pathfinding (Module)
- local PathfindingService = game:GetService("PathfindingService")
- local Constants = require(script.Parent.Constants)
- local PositionGenerator = require(script.Parent.PositionGenerator)
- local NPCPathfinding = {}
- function NPCPathfinding:StartWandering(npc)
- local humanoid = npc:WaitForChild("Humanoid")
- local rootPart = npc:WaitForChild("HumanoidRootPart")
- task.spawn(function()
- while npc.Parent and humanoid.Parent and rootPart.Parent do
- local destination = PositionGenerator:GetRandomPosition()
- self:MoveToDestination(humanoid, rootPart, destination)
- local waitTime = math.random(Constants.WANDER_WAIT_TIME[1], Constants.WANDER_WAIT_TIME[2])
- task.wait(waitTime)
- end
- end)
- end
- function NPCPathfinding:MoveToDestination(humanoid, rootPart, destination)
- local reached = false
- while not reached and rootPart.Parent do
- local path = PathfindingService:CreatePath({
- AgentRadius = Constants.AGENT_RADIUS,
- AgentHeight = Constants.AGENT_HEIGHT,
- AgentCanJump = Constants.AGENT_CAN_JUMP,
- AgentCanClimb = Constants.AGENT_CAN_CLIMP,
- Costs = Constants.PATH_COSTS
- })
- local success, errorMessage = pcall(function()
- path:ComputeAsync(rootPart.Position, destination)
- end)
- if success and path.Status == Enum.PathStatus.Success then
- local waypoints = path:GetWaypoints()
- for _, waypoint in ipairs(waypoints) do
- if not rootPart.Parent then break end
- if (rootPart.Position - destination).Magnitude < Constants.DESTINATION_THRESHOLD then
- reached = true
- break
- end
- humanoid:MoveTo(waypoint.Position)
- local reachedWaypoint = humanoid.MoveToFinished:Wait()
- if not reachedWaypoint then
- break
- end
- end
- else
- warn("Path creation failed: ", path.Status or errorMessage)
- break
- end
- if (rootPart.Position - destination).Magnitude < Constants.DESTINATION_THRESHOLD then
- reached = true
- end
- task.wait(0.1)
- end
- end
- return NPCPathfinding
- -- Push Physics (Module)
- local Constants = require(script.Parent.Constants)
- local NPCPhysics = {}
- function NPCPhysics:PushNPC(npc, player)
- local humanoid = npc:FindFirstChildOfClass("Humanoid")
- local rootPart = npc:FindFirstChild("HumanoidRootPart")
- local playerCharacter = player.Character
- if not self:ValidatePushConditions(humanoid, rootPart, playerCharacter) then
- return false
- end
- local playerRootPart = playerCharacter:FindFirstChild("HumanoidRootPart")
- if not playerRootPart then return false end
- rootPart:SetAttribute("beingPushed", true)
- npc:SetAttribute("PushedBy", player.UserId)
- local direction = (rootPart.Position - playerRootPart.Position).Unit
- local constraints, motors = self:SetupRagdoll(npc)
- self:ApplyPushForce(rootPart, direction)
- self:ScheduleRecovery(npc, humanoid, constraints, motors)
- return true
- end
- function NPCPhysics:ValidatePushConditions(humanoid, rootPart, playerCharacter)
- if not humanoid or not rootPart or not playerCharacter then
- return false
- end
- if rootPart:GetAttribute("beingPushed") then
- return false
- end
- return true
- end
- function NPCPhysics:SetupRagdoll(npc)
- local constraints = {}
- local motors = {}
- for _, motor in pairs(npc:GetDescendants()) do
- if motor:IsA("Motor6D") and motor.Name ~= "Root" then
- motors[motor] = {
- enabled = motor.Enabled,
- part0 = motor.Part0,
- part1 = motor.Part1,
- c0 = motor.C0,
- c1 = motor.C1
- }
- local constraint = self:CreateConstraintForMotor(motor, npc)
- if constraint then
- table.insert(constraints, constraint)
- end
- motor.Enabled = false
- end
- end
- return constraints, motors
- end
- function NPCPhysics:CreateConstraintForMotor(motor, npc)
- local constraint = Instance.new("BallSocketConstraint")
- constraint.Attachment0 = motor.Part0 and motor.Part0:FindFirstChild(motor.Name.."RigAttachment")
- constraint.Attachment1 = motor.Part1 and motor.Part1:FindFirstChild(motor.Name.."Attachment")
- if not constraint.Attachment0 and motor.Part0 then
- local att = Instance.new("Attachment")
- att.Name = motor.Name.."RigAttachment"
- att.CFrame = motor.C0
- att.Parent = motor.Part0
- constraint.Attachment0 = att
- end
- if not constraint.Attachment1 and motor.Part1 then
- local att = Instance.new("Attachment")
- att.Name = motor.Name.."Attachment"
- att.CFrame = motor.C1
- att.Parent = motor.Part1
- constraint.Attachment1 = att
- end
- if constraint.Attachment0 and constraint.Attachment1 then
- constraint.Parent = npc
- return constraint
- end
- return nil
- end
- function NPCPhysics:ApplyPushForce(rootPart, direction)
- local humanoid = rootPart.Parent:FindFirstChildOfClass("Humanoid")
- if humanoid then
- humanoid:ChangeState(Enum.HumanoidStateType.Physics)
- humanoid.PlatformStand = true
- end
- local bodyVelocity = Instance.new("BodyVelocity")
- bodyVelocity.MaxForce = Constants.BODY_VELOCITY_FORCE
- bodyVelocity.Velocity = direction * Constants.PUSH_FORCE + Vector3.new(0, Constants.PUSH_UP_FORCE, 0)
- bodyVelocity.Parent = rootPart
- game:GetService("Debris"):AddItem(bodyVelocity, 0.1)
- end
- function NPCPhysics:ScheduleRecovery(npc, humanoid, constraints, motors)
- task.spawn(function()
- task.wait(Constants.PUSH_DURATION)
- for _, constraint in pairs(constraints) do
- if constraint.Parent then
- constraint:Destroy()
- end
- end
- for motor, info in pairs(motors) do
- if motor.Parent then
- motor.Enabled = info.enabled
- end
- end
- if humanoid.Parent then
- humanoid.PlatformStand = false
- humanoid:ChangeState(Enum.HumanoidStateType.Running)
- end
- task.wait(Constants.PUSH_RECOVERY_TIME)
- local rootPart = npc:FindFirstChild("HumanoidRootPart")
- if rootPart and rootPart.Parent then
- rootPart:SetAttribute("beingPushed", nil)
- npc:SetAttribute("PushedBy", nil)
- end
- end)
- end
- return NPCPhysics
- -- NPC Spawn (Module)
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
- local Constants = require(script.Parent.Constants)
- local NPCAnimationHandler = require(script.Parent.NPCAnimationHandler)
- local NPCPathfinding = require(script.Parent.NPCPathfinding)
- local NPCPhysics = require(script.Parent.NPCPhysics)
- local CurrencyManager = require(script.Parent.CurrencyManager)
- local NPCSpawner = {}
- function NPCSpawner:Initialize()
- self.wNPCs = workspace:WaitForChild("NPCs")
- self.npcFolder = ReplicatedStorage:WaitForChild("NPCs")
- self.onNPCCreated = ReplicatedStorage:WaitForChild("OnNPCCreated")
- self.pushButton = ReplicatedStorage:WaitForChild("PushButton")
- self.currentNPCs = 0
- self.spawnPoints = {}
- end
- function NPCSpawner:SpawnNPC(spawnPoint, count)
- if not spawnPoint or count <= 0 then return end
- spawnPoint:SetAttribute("Current", spawnPoint:GetAttribute("Current") + count)
- self.currentNPCs += count
- local npcOptions = self.npcFolder:GetChildren()
- if #npcOptions == 0 then
- warn("No NPC models found in ReplicatedStorage!")
- return
- end
- for i = 1, count do
- local npcModel = npcOptions[math.random(1, #npcOptions)]:Clone()
- self:SetupNPC(npcModel, spawnPoint)
- end
- end
- function NPCSpawner:SetupNPC(npc, spawnPoint)
- npc.Parent = self.wNPCs
- npc:SetAttribute("SpawnedBy", spawnPoint:GetAttribute("SpawnID"))
- npc:SetAttribute("Worth", math.random(Constants.NPC_MIN_WORTH, Constants.NPC_MAX_WORTH))
- local humanoid = npc:FindFirstChildOfClass("Humanoid")
- if humanoid then
- humanoid.Died:Connect(function()
- self:RemoveNPC(spawnPoint, npc)
- end)
- end
- local bodyColors: BodyColors = npc:FindFirstChildOfClass("BodyColors")
- local color = math.random(1,2) if color == 1 then color = "Black" else color = "White" end
- local gender = npc:GetAttribute("Gender")
- local firstName = Constants.NPC_NAMES[color][gender][math.random(1, #Constants.NPC_NAMES[color][gender])]
- local lastName = Constants.NPC_LAST_NAMES[color][math.random(1, #Constants.NPC_LAST_NAMES[color])]
- local skinColor = Constants.NPC_COLORS[color][math.random(1, #Constants.NPC_COLORS[color])]
- npc.Name = firstName.." "..lastName
- for _, prop in ipairs({"HeadColor3", "LeftArmColor3", "RightArmColor3", "LeftLegColor3", "RightLegColor3", "TorsoColor3"}) do
- bodyColors[prop] = skinColor
- end
- local screamSound: Sound = ReplicatedStorage:WaitForChild("CrashSounds")["SCREAM "..math.random(1,3).." "..gender]:Clone()
- screamSound.Parent = npc
- screamSound.Name = "ScreamSound"
- local pushButton = self.pushButton:Clone()
- pushButton.Parent = npc.UpperTorso
- pushButton.Triggered:Connect(function(player)
- if player.Character and player.Character.PrimaryPart then
- NPCPhysics:PushNPC(npc, player)
- end
- end)
- NPCAnimationHandler:SetupAnimations(npc)
- self:PositionNPC(npc, spawnPoint)
- NPCPathfinding:StartWandering(npc)
- self.onNPCCreated:FireAllClients(npc)
- end
- function NPCSpawner:PositionNPC(npc, spawnPoint)
- local offsetX = math.random(-(spawnPoint.Size.X/2), spawnPoint.Size.X/2)
- local offsetZ = math.random(-(spawnPoint.Size.Z/2), spawnPoint.Size.Z/2)
- local offset = CFrame.new(offsetX, 1, offsetZ)
- npc:SetPrimaryPartCFrame(spawnPoint.CFrame * offset)
- end
- function NPCSpawner:RemoveNPC(spawnPoint, npc)
- if not npc or not npc.Parent then return end
- CurrencyManager:HandleNPCRemoval(npc)
- spawnPoint:SetAttribute("Current", math.max(0, spawnPoint:GetAttribute("Current") - 1))
- self.currentNPCs = math.max(0, self.currentNPCs - 1)
- task.wait(0.5)
- npc:Destroy()
- end
- function NPCSpawner:StartSpawnLoop(spawnPoint)
- task.spawn(function()
- while spawnPoint.Parent do
- local min = spawnPoint:GetAttribute("Min") or 0
- local max = spawnPoint:GetAttribute("Max") or 0
- local current = spawnPoint:GetAttribute("Current") or 0
- local spawnTime = spawnPoint:GetAttribute("SpawnTime") or 5
- if current < min then
- self:SpawnNPC(spawnPoint, min - current)
- end
- if current < max then
- self:SpawnNPC(spawnPoint, 1)
- task.wait(spawnTime)
- else
- task.wait(1)
- end
- end
- end)
- end
- return NPCSpawner
- -- Random Position Generator (Module)
- local PositionGenerator = {}
- local allGreen = {}
- local greenKeys = {}
- function PositionGenerator:Initialize(greenFolder)
- local num = 0
- for _, wall in pairs(greenFolder:GetChildren()) do
- num += 1
- local sizeX = wall.Size.X
- local positionX = wall.CFrame.Position.X
- local sizeZ = wall.Size.Z
- local positionZ = wall.CFrame.Position.Z
- local key = wall.Name .. tostring(num)
- allGreen[key] = {
- sizeX = sizeX,
- sizeZ = sizeZ,
- positionX = positionX,
- positionZ = positionZ
- }
- table.insert(greenKeys, key)
- end
- end
- function PositionGenerator:GetRandomPosition()
- if #greenKeys == 0 then
- warn("No walkable areas defined!")
- return Vector3.new(0, 1, 0)
- end
- local randomKey = greenKeys[math.random(1, #greenKeys)]
- local data = allGreen[randomKey]
- local offsetX = (math.random() - 0.5) * data.sizeX
- local offsetZ = (math.random() - 0.5) * data.sizeZ
- return Vector3.new(data.positionX + offsetX, 1, data.positionZ + offsetZ)
- end
- return PositionGenerator
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement