Hiojojwr

Untitled

Aug 10th, 2025
171
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 14.21 KB | None | 0 0
  1. --[[
  2.     ZombieController.server.lua
  3.     Drop this Server Script inside ServerScriptService.
  4.  
  5.     ✅ What this does
  6.     - Spawns & controls Zombies whose TEMPLATES live in ReplicatedStorage.
  7.     - Handles pathfinding, target acquisition, line-of-sight, and attacks (melee/shooter/explosive).
  8.     - Safely loads animations (no hard errors if an AnimationId is invalid or private).
  9.     - Keeps logic server-authoritative (sets network ownership to the server).
  10.  
  11.     🧩 Folder structure (expected)
  12.     ReplicatedStorage
  13.       └─ ZOMBIE_TEMPLATES                 -- <== your NPC templates live here (models with Humanoid)
  14.            ├─ Bomber
  15.            ├─ Clown
  16.            ├─ Cowboy
  17.            ├─ Cop
  18.            ├─ Crow
  19.            └─ Chef
  20.     Workspace
  21.       └─ ZOMBIES                          -- <== where live NPC instances will be parented (auto-created)
  22.  
  23.     💡 Notes
  24.     - Works if you manually place NPCs into Workspace/ZOMBIES as well (they will be controlled).
  25.     - If your templates already exist in Workspace/ZOMBIES, controller will just attach to them.
  26.     - If animations fail to load (ownership/permissions), NPCs still move/attack. Fix IDs later.
  27. --]]
  28.  
  29. ------------------------
  30. -- Services
  31. ------------------------
  32. local Players              = game:GetService("Players")
  33. local ReplicatedStorage    = game:GetService("ReplicatedStorage")
  34. local PathfindingService   = game:GetService("PathfindingService")
  35. local RunService           = game:GetService("RunService")
  36. local Debris               = game:GetService("Debris")
  37. local CollectionService    = game:GetService("CollectionService")
  38.  
  39. ------------------------
  40. -- Global settings
  41. ------------------------
  42. local PATH_RECALC_TIME       = 1.25   -- seconds between path recompute
  43. local MOVE_RETRY_TIME        = 0.25   -- retry MoveTo pulse
  44. local DEFAULT_ATTACK_RANGE   = 5
  45. local DEFAULT_ATTACK_COOLDOWN= 1.2
  46. local LOS_MAX_DISTANCE       = 200
  47. local CAN_SEE_TAG            = "Cannot_See" -- optional tag we can reuse
  48.  
  49. -- Server should own physics for smoother NPC motion
  50. local function setServerOwnership(root)
  51.     if typeof(root) == "Instance" and root:IsA("BasePart") then
  52.         pcall(function() root:SetNetworkOwner(nil) end)
  53.     end
  54. end
  55.  
  56. ------------------------
  57. -- Zombie type settings
  58. ------------------------
  59. local ZOMBIE_SETTINGS = {
  60.     Bomber = {AttackType="Explosive", Damage=30, AttackRange=6, AttackCooldown=2.0, WalkSpeed=10, RunSpeed=14},
  61.     Chef   = {AttackType="Melee",    Damage=15, AttackRange=4, AttackCooldown=1.0, WalkSpeed=9,  RunSpeed=12},
  62.     Clown  = {AttackType="Explosive", Damage=25, AttackRange=5, AttackCooldown=1.5, WalkSpeed=10, RunSpeed=13},
  63.     Cop    = {AttackType="Shooter",  Damage=10, AttackRange=16,AttackCooldown=1.5, WalkSpeed=10, RunSpeed=13},
  64.     Cowboy = {AttackType="Shooter",  Damage=12, AttackRange=18,AttackCooldown=1.5, WalkSpeed=10, RunSpeed=13},
  65.     Crow   = {AttackType="Melee",    Damage=20, AttackRange=4, AttackCooldown=1.2, WalkSpeed=11, RunSpeed=14},
  66. }
  67.  
  68. ------------------------
  69. -- Folders (templates & live)
  70. ------------------------
  71. local TEMPLATE_FOLDER = ReplicatedStorage:WaitForChild("ZOMBIE_TEMPLATES", 10)
  72. assert(TEMPLATE_FOLDER, "[ZombieController] Missing ReplicatedStorage/ZOMBIE_TEMPLATES folder!")
  73.  
  74. local LIVE_FOLDER = workspace:FindFirstChild("ZOMBIES")
  75. if not LIVE_FOLDER then
  76.     LIVE_FOLDER = Instance.new("Folder")
  77.     LIVE_FOLDER.Name = "ZOMBIES"
  78.     LIVE_FOLDER.Parent = workspace
  79. end
  80.  
  81. ------------------------
  82. -- Utilities
  83. ------------------------
  84.  
  85. -- Safe Animator retrieval
  86. local function getAnimator(humanoid)
  87.     local animator = humanoid:FindFirstChildOfClass("Animator")
  88.     if not animator then
  89.         animator = Instance.new("Animator")
  90.         animator.Parent = humanoid
  91.     end
  92.     return animator
  93. end
  94.  
  95. -- Safe animation cache on the model
  96. local function buildAnimTable(zombieModel)
  97.     local t = {}
  98.     local hum = zombieModel:FindFirstChildOfClass("Humanoid")
  99.     if not hum then return t end
  100.     local animator = getAnimator(hum)
  101.  
  102.     local animFolder = zombieModel:FindFirstChild("Animations")
  103.     if not animFolder then return t end
  104.  
  105.     for _, animObj in ipairs(animFolder:GetChildren()) do
  106.         if animObj:IsA("Animation") and animObj.AnimationId ~= "" then
  107.             local ok, track = pcall(function()
  108.                 return animator:LoadAnimation(animObj)
  109.             end)
  110.             if ok and track then
  111.                 t[animObj.Name] = track
  112.             else
  113.                 warn(("[ZombieController] Failed to load animation '%s' (%s) on %s")
  114.                     :format(animObj.Name, tostring(animObj.AnimationId), zombieModel.Name))
  115.             end
  116.         end
  117.     end
  118.     return t
  119. end
  120.  
  121. local function playAnim(animTable, name, looped)
  122.     local tr = animTable[name]
  123.     if not tr then return end
  124.     if typeof(looped) == "boolean" then tr.Looped = looped end
  125.     tr:Play()
  126. end
  127.  
  128. local function stopAnim(animTable, name)
  129.     local tr = animTable[name]
  130.     if tr then tr:Stop() end
  131. end
  132.  
  133. -- Basic nearest target (players only)
  134. local function getNearestPlayer(rootPart)
  135.     local nearestChar, nearestDist = nil, math.huge
  136.     for _, plr in ipairs(Players:GetPlayers()) do
  137.         local ch = plr.Character
  138.         local hum = ch and ch:FindFirstChildOfClass("Humanoid")
  139.         local hrp = ch and ch:FindFirstChild("HumanoidRootPart")
  140.         if hum and hrp and hum.Health > 0 then
  141.             local d = (hrp.Position - rootPart.Position).Magnitude
  142.             if d < nearestDist then
  143.                 nearestDist = d
  144.                 nearestChar = ch
  145.             end
  146.         end
  147.     end
  148.     return nearestChar, nearestDist
  149. end
  150.  
  151. -- Simple line-of-sight using raycast
  152. local function hasLineOfSight(fromPos, toPos, ignoreList)
  153.     local params = RaycastParams.new()
  154.     params.FilterType = Enum.RaycastFilterType.Exclude
  155.     params.FilterDescendantsInstances = ignoreList or {}
  156.     local result = workspace:Raycast(fromPos, (toPos - fromPos), params)
  157.     return result == nil
  158. end
  159.  
  160. -- Step along waypoints with MoveTo
  161. local function followPath(humanoid, root, goalPosition)
  162.     local path = PathfindingService:CreatePath({
  163.         AgentHeight = humanoid.HipHeight + (root.Size.Y),
  164.         AgentRadius = math.max(2, root.Size.X * 0.5),
  165.         AgentCanJump = false,
  166.         AgentMaxSlope = 45
  167.     })
  168.  
  169.     path:ComputeAsync(root.Position, goalPosition)
  170.     if path.Status ~= Enum.PathStatus.Success then
  171.         humanoid:MoveTo(goalPosition) -- fallback
  172.         return
  173.     end
  174.  
  175.     local waypoints = path:GetWaypoints()
  176.     for i = 1, #waypoints do
  177.         humanoid:MoveTo(waypoints[i].Position)
  178.         local reached = humanoid.MoveToFinished:Wait()
  179.         if not reached then break end
  180.     end
  181. end
  182.  
  183. ------------------------
  184. -- Attack behaviours
  185. ------------------------
  186.  
  187. local function doMeleeAttack(attackerModel, targetChar, damage)
  188.     local hum = targetChar:FindFirstChildOfClass("Humanoid")
  189.     if hum and hum.Health > 0 then
  190.         hum:TakeDamage(damage)
  191.     end
  192. end
  193.  
  194. local function doShooterAttack(attackerModel, targetChar, damage)
  195.     -- Minimalistic: we raycast line between attacker head and target torso, then apply damage.
  196.     local head = attackerModel:FindFirstChild("Head")
  197.     local hrp  = targetChar:FindFirstChild("HumanoidRootPart")
  198.     local hum  = targetChar:FindFirstChildOfClass("Humanoid")
  199.     if not (head and hrp and hum and hum.Health > 0) then return end
  200.  
  201.     local params = RaycastParams.new()
  202.     params.FilterType = Enum.RaycastFilterType.Exclude
  203.     params.FilterDescendantsInstances = {attackerModel}
  204.  
  205.     local res = workspace:Raycast(head.Position, (hrp.Position - head.Position).Unit * 300, params)
  206.     local didHit = res and res.Instance and res.Instance:IsDescendantOf(targetChar)
  207.     if didHit then hum:TakeDamage(damage) end
  208. end
  209.  
  210. local function doExplosiveAttack(attackerModel, targetChar, damage, radius)
  211.     local root = attackerModel:FindFirstChild("HumanoidRootPart")
  212.     if not root then return end
  213.  
  214.     local explosion = Instance.new("Explosion")
  215.     explosion.BlastRadius = radius or 10
  216.     explosion.BlastPressure = 0
  217.     explosion.DestroyJointRadiusPercent = 0
  218.     explosion.ExplosionType = Enum.ExplosionType.NoCraters
  219.     explosion.Position = root.Position
  220.     explosion.Visible = false
  221.  
  222.     explosion.Hit:Connect(function(part, dist)
  223.         local c = part and part.Parent
  224.         local hum = c and c:FindFirstChildOfClass("Humanoid")
  225.         if hum and not c:FindFirstChild("IsZombie") then
  226.             local minD = damage * 0.7
  227.             local maxD = damage
  228.             local falloff = (1 - math.clamp(dist / explosion.BlastRadius, 0, 1))
  229.             hum:TakeDamage(minD + (maxD - minD) * falloff)
  230.         end
  231.     end)
  232.  
  233.     explosion.Parent = workspace
  234.     Debris:AddItem(explosion, 2)
  235. end
  236.  
  237. ------------------------
  238. -- Core controller per zombie instance
  239. ------------------------
  240. local function controlZombie(zombieModel)
  241.     local hum = zombieModel:FindFirstChildOfClass("Humanoid")
  242.     local root = zombieModel:FindFirstChild("HumanoidRootPart")
  243.     if not (hum and root) then return end
  244.  
  245.     -- Match by template name or fall back to model name
  246.     local zType = ZOMBIE_SETTINGS[zombieModel.Name] and zombieModel.Name
  247.                   or (ZOMBIE_SETTINGS[zombieModel:GetAttribute("Type")] and zombieModel:GetAttribute("Type"))
  248.     local settings = ZOMBIE_SETTINGS[zType or zombieModel.Name]
  249.     if not settings then
  250.         warn(("[ZombieController] No settings for zombie '%s'"):format(zombieModel.Name))
  251.         return
  252.     end
  253.  
  254.     setServerOwnership(root)
  255.     hum:SetStateEnabled(Enum.HumanoidStateType.FallingDown, false)
  256.     hum:SetStateEnabled(Enum.HumanoidStateType.Ragdoll, false)
  257.     hum.WalkSpeed = settings.WalkSpeed or 10
  258.  
  259.     local anims = buildAnimTable(zombieModel)
  260.     playAnim(anims, "Spawn")
  261.     task.delay(1.0, function() stopAnim(anims, "Spawn") end)
  262.  
  263.     local lastPathTime = 0
  264.     local canAttack = true
  265.  
  266.     -- Main loop
  267.     task.spawn(function()
  268.         while zombieModel.Parent and hum.Health > 0 do
  269.             local targetChar, distance = getNearestPlayer(root)
  270.             if targetChar then
  271.                 local targetHRP = targetChar:FindFirstChild("HumanoidRootPart")
  272.                 if targetHRP then
  273.                     -- Movement
  274.                     if tick() - lastPathTime > PATH_RECALC_TIME then
  275.                         lastPathTime = tick()
  276.                         playAnim(anims, "Walk", true)
  277.                         followPath(hum, root, targetHRP.Position)
  278.                     else
  279.                         hum:MoveTo(targetHRP.Position)
  280.                     end
  281.  
  282.                     -- LOS check (optional but recommended)
  283.                     local los = hasLineOfSight(root.Position, targetHRP.Position, {zombieModel})
  284.                     if not los and distance > 8 then
  285.                         -- No LOS → still path toward target
  286.                     end
  287.  
  288.                     -- Attack
  289.                     local attackRange = settings.AttackRange or DEFAULT_ATTACK_RANGE
  290.                     if distance <= attackRange and canAttack then
  291.                         canAttack = false
  292.                         stopAnim(anims, "Walk")
  293.                         playAnim(anims, "Attack")
  294.  
  295.                         if settings.AttackType == "Melee" then
  296.                             doMeleeAttack(zombieModel, targetChar, settings.Damage)
  297.                         elseif settings.AttackType == "Shooter" then
  298.                             doShooterAttack(zombieModel, targetChar, settings.Damage)
  299.                         elseif settings.AttackType == "Explosive" then
  300.                             doExplosiveAttack(zombieModel, targetChar, settings.Damage, 10)
  301.                             -- self-destruct flavor (optional)
  302.                             hum.Health = 0
  303.                         end
  304.  
  305.                         task.delay(settings.AttackCooldown or DEFAULT_ATTACK_COOLDOWN, function()
  306.                             canAttack = true
  307.                         end)
  308.                     end
  309.                 end
  310.             else
  311.                 stopAnim(anims, "Walk")
  312.                 playAnim(anims, "Idle", true)
  313.             end
  314.  
  315.             RunService.Heartbeat:Wait()
  316.         end
  317.  
  318.         -- Death cleanup anim
  319.         stopAnim(anims, "Walk")
  320.         playAnim(anims, "Death")
  321.         Debris:AddItem(zombieModel, 4)
  322.     end)
  323. end
  324.  
  325. ------------------------
  326. -- Spawning helpers
  327. ------------------------
  328.  
  329. -- Clone & spawn one template
  330. local function spawnZombieFromTemplate(template)
  331.     if not (template and template:IsA("Model")) then return end
  332.     local clone = template:Clone()
  333.     clone.Parent = LIVE_FOLDER
  334.     local root = clone:FindFirstChild("HumanoidRootPart")
  335.     if root then
  336.         -- Spawn near template's PrimaryPart or at 0,0,0
  337.         if template.PrimaryPart then
  338.             clone:PivotTo(template.PrimaryPart.CFrame)
  339.         else
  340.             clone:PivotTo(CFrame.new(0, 5, 0))
  341.         end
  342.     end
  343.     controlZombie(clone)
  344. end
  345.  
  346. -- Auto-connect to any new models appearing in LIVE_FOLDER
  347. LIVE_FOLDER.ChildAdded:Connect(function(child)
  348.     task.defer(function()
  349.         if child:IsA("Model") and child:FindFirstChildOfClass("Humanoid") then
  350.             controlZombie(child)
  351.         end
  352.     end)
  353. end)
  354.  
  355. ------------------------
  356. -- Boot
  357. ------------------------
  358.  
  359. -- 1) Control existing NPCs in LIVE_FOLDER (if you already placed some there)
  360. for _, m in ipairs(LIVE_FOLDER:GetChildren()) do
  361.     if m:IsA("Model") and m:FindFirstChildOfClass("Humanoid") then
  362.         controlZombie(m)
  363.     end
  364. end
  365.  
  366. -- 2) (Optional) Auto-spawn one instance per template at start
  367. for _, template in ipairs(TEMPLATE_FOLDER:GetChildren()) do
  368.     if template:IsA("Model") and ZOMBIE_SETTINGS[template.Name] then
  369.         spawnZombieFromTemplate(template)
  370.     else
  371.         -- ignore unknown templates; add their settings above if you want them controlled
  372.     end
  373. end
  374.  
  375. -- 3) If you add new templates later, auto-spawn a copy immediately (optional)
  376. TEMPLATE_FOLDER.ChildAdded:Connect(function(newTemplate)
  377.     if newTemplate:IsA("Model") then
  378.         task.wait(0.1)
  379.         if ZOMBIE_SETTINGS[newTemplate.Name] then
  380.             spawnZombieFromTemplate(newTemplate)
  381.         end
  382.     end
  383. end)
  384.  
Advertisement
Add Comment
Please, Sign In to add comment