Advertisement
g14ndev

NoobAI module

Mar 15th, 2025
431
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 18.24 KB | Gaming | 0 0
  1. -- this module handles noob ai behaviour in addition to genetics
  2. -- author: Gixnly
  3.  
  4.  
  5.  
  6.  
  7. local NoobAI = {}
  8. NoobAI.__index = NoobAI
  9.  
  10. -- 💡 SERVICES
  11. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  12. local Players = game:GetService("Players")
  13. local RunService = game:GetService("RunService")
  14. local Debris = game:GetService("Debris")
  15. local ServerScriptService = game:GetService("ServerScriptService")
  16. local DataManager = require(ServerScriptService.DataManager)
  17. local LootManager = require(ServerScriptService.LootManager)
  18.  
  19. -- Global table to track all NoobAI instances, keyed by Model
  20. local ActiveNoobs = {}
  21.  
  22. -----------------------------------------------------------
  23. -- Utility function: randomColorVariation
  24. -- Returns a Color3 that is a slight variation of the base color.
  25. -----------------------------------------------------------
  26. local function randomColorVariation(baseColor, variation)
  27.     local r = math.clamp(baseColor.R + (math.random()-0.5)*variation, 0, 1)
  28.     local g = math.clamp(baseColor.G + (math.random()-0.5)*variation, 0, 1)
  29.     local b = math.clamp(baseColor.B + (math.random()-0.5)*variation, 0, 1)
  30.     return Color3.new(r, g, b)
  31. end
  32.  
  33. -----------------------------------------------------------
  34. -- Utility function: averageValue
  35. -- Returns the average of two numbers plus a small random variation.
  36. -----------------------------------------------------------
  37. local function averageValue(val1, val2, variation)
  38.     return ((val1 + val2) / 2) + (math.random()-0.5)*variation
  39. end
  40.  
  41. -----------------------------------------------------------
  42. -- Constructor: NoobAI.new
  43. -----------------------------------------------------------
  44. function NoobAI.new(spawnPosition, geneticParents)
  45.     local noobTemplate = ReplicatedStorage:FindFirstChild("Noobs") and ReplicatedStorage.Noobs:FindFirstChild("Noob")
  46.     assert(noobTemplate, "⚠️ Noob template missing from ReplicatedStorage.Noobs!")
  47.  
  48.     local model = noobTemplate:Clone()
  49.     model:SetPrimaryPartCFrame(CFrame.new(spawnPosition))
  50.     model.Parent = workspace
  51.     model:SetAttribute("CanDamage", true)
  52.  
  53.     local humanoid = model:FindFirstChildOfClass("Humanoid")
  54.     local rootPart = model:FindFirstChild("HumanoidRootPart")
  55.     local animationsFolder = model:FindFirstChild("Animations")
  56.     local lootFolder = model:FindFirstChild("Loot")
  57.  
  58.     assert(humanoid, "⚠️ Noob template is missing a Humanoid!")
  59.     assert(rootPart, "⚠️ Noob template is missing a HumanoidRootPart!")
  60.     assert(animationsFolder, "⚠️ Noob template is missing an 'Animations' folder!")
  61.  
  62.     humanoid.BreakJointsOnDeath = false
  63.  
  64.     local self = setmetatable({}, NoobAI)
  65.     self.Model = model
  66.     self.Humanoid = humanoid
  67.     self.RootPart = rootPart
  68.     self.Animations = animationsFolder
  69.     self.LootFolder = lootFolder
  70.     self.Target = nil
  71.     self.IsScared = false
  72.     self.LastAnimation = nil
  73.     self.CurrentAnimationTrack = nil
  74.     self.FleeDestination = nil
  75.     self.Dead = false
  76.     self.AnimationTracks = {}
  77.     self.LastPlayerCheck = tick()
  78.     self.DespawnDelay = 30
  79.     self.DespawnRadius = 100
  80.     self.LastAttacker = nil
  81.     self.LastStateChange = tick() -- Track when state last changed
  82.     self.DespawnChanceMultiplier = 1 -- Starts at 1x chance
  83.  
  84.     local animator = humanoid:FindFirstChild("Animator") or Instance.new("Animator", humanoid)
  85.     self.Animator = animator
  86.  
  87.     local damageEvent = Instance.new("BindableEvent")
  88.     damageEvent.Name = "TakeDamageEvent"
  89.     damageEvent.Parent = model
  90.     damageEvent.Event:Connect(function(damage, attacker)
  91.         self:TakeDamage(damage, attacker)
  92.     end)
  93.  
  94.     local stats = model:FindFirstChild("Stats")
  95.     assert(stats, "⚠️ Noob template is missing a 'Stats' folder!")
  96.  
  97.     self.Genetics = {}
  98.     if geneticParents then
  99.         local parent1 = geneticParents[1]
  100.         local parent2 = geneticParents[2]
  101.         if math.random() < 0.5 then
  102.             self.Genetics.Gender = parent1.Gender
  103.         else
  104.             self.Genetics.Gender = parent2.Gender
  105.         end
  106.         if self.Genetics.Gender == "Male" then
  107.             local baseBlue = Color3.new(0.2, 0.2, 0.8)
  108.             self.Genetics.TorsoColor = randomColorVariation(parent1.TorsoColor and Color3.new(
  109.                 averageValue(parent1.TorsoColor.R, parent2.TorsoColor.R, 0.1),
  110.                 averageValue(parent1.TorsoColor.G, parent2.TorsoColor.G, 0.1),
  111.                 averageValue(parent1.TorsoColor.B, parent2.TorsoColor.B, 0.1)
  112.                 ) or baseBlue, 0.05)
  113.         else
  114.             local basePink = Color3.new(0.9, 0.6, 0.7)
  115.             self.Genetics.TorsoColor = randomColorVariation(parent1.TorsoColor and Color3.new(
  116.                 averageValue(parent1.TorsoColor.R, parent2.TorsoColor.R, 0.1),
  117.                 averageValue(parent1.TorsoColor.G, parent2.TorsoColor.G, 0.1),
  118.                 averageValue(parent1.TorsoColor.B, parent2.TorsoColor.B, 0.1)
  119.                 ) or basePink, 0.05)
  120.         end
  121.         local baseYellow = Color3.new(1, 0.9, 0.4)
  122.         self.Genetics.HeadColor = randomColorVariation(parent1.HeadColor and Color3.new(
  123.             averageValue(parent1.HeadColor.R, parent2.HeadColor.R, 0.1),
  124.             averageValue(parent1.HeadColor.G, parent2.HeadColor.G, 0.1),
  125.             averageValue(parent1.HeadColor.B, parent2.HeadColor.B, 0.1)
  126.             ) or baseYellow, 0.05)
  127.         self.Genetics.ArmColor = self.Genetics.HeadColor
  128.         self.Genetics.HealthModifier = averageValue(parent1.HealthModifier or 100, parent2.HealthModifier or 100, 5)
  129.         self.Genetics.ScaredDamageModifier = averageValue(parent1.ScaredDamageModifier or 15, parent2.ScaredDamageModifier or 15, 2)
  130.         self.Genetics.FieldOfVisionModifier = averageValue(parent1.FieldOfVisionModifier or 20, parent2.FieldOfVisionModifier or 20, 2)
  131.     else
  132.         if math.random() < 0.5 then
  133.             self.Genetics.Gender = "Male"
  134.         else
  135.             self.Genetics.Gender = "Female"
  136.         end
  137.         if self.Genetics.Gender == "Male" then
  138.             local baseBlue = Color3.new(0.2, 0.2, 0.8)
  139.             self.Genetics.TorsoColor = randomColorVariation(baseBlue, 0.05)
  140.         else
  141.             local basePink = Color3.new(0.9, 0.6, 0.7)
  142.             self.Genetics.TorsoColor = randomColorVariation(basePink, 0.05)
  143.         end
  144.         local baseYellow = Color3.new(1, 0.9, 0.4)
  145.         self.Genetics.HeadColor = randomColorVariation(baseYellow, 0.05)
  146.         self.Genetics.ArmColor = self.Genetics.HeadColor
  147.         self.Genetics.HealthModifier = math.random(90, 110)
  148.         self.Genetics.ScaredDamageModifier = math.random(13, 17)
  149.         self.Genetics.FieldOfVisionModifier = math.random(18, 22)
  150.     end
  151.  
  152.     local torso = model:FindFirstChild("Torso")
  153.     if torso then
  154.         torso.Color = self.Genetics.TorsoColor
  155.     end
  156.     local head = model:FindFirstChild("Head")
  157.     if head then
  158.         head.Color = self.Genetics.HeadColor
  159.     end
  160.     local leftArm = model:FindFirstChild("Left Arm")
  161.     local rightArm = model:FindFirstChild("Right Arm")
  162.     if leftArm then
  163.         leftArm.Color = self.Genetics.ArmColor
  164.     end
  165.     if rightArm then
  166.         rightArm.Color = self.Genetics.ArmColor
  167.     end
  168.  
  169.     self.Config = {
  170.         WalkSpeed    = (stats:FindFirstChild("WalkSpeed")    and stats.WalkSpeed.Value)    or 8,
  171.         RunSpeed     = (stats:FindFirstChild("RunSpeed")     and stats.RunSpeed.Value)     or 16,
  172.         Aggressive   = (stats:FindFirstChild("Aggressive")   and stats.Aggressive.Value)   or (math.random() < 0.2),
  173.         AttackRadius = (stats:FindFirstChild("AttackRadius") and stats.AttackRadius.Value) or 5,
  174.         FieldOfVision= ((stats:FindFirstChild("FieldOfVision")and stats.FieldOfVision.Value) or 20) * (self.Genetics.FieldOfVisionModifier/100),
  175.         Health       = ((stats:FindFirstChild("Health")       and stats.Health.Value)       or 100) * (self.Genetics.HealthModifier/100),
  176.         AttackDamage = (stats:FindFirstChild("AttackDamage") and stats.AttackDamage.Value) or 10,
  177.         ScaredDamage = ((stats:FindFirstChild("ScaredDamage") and stats.ScaredDamage.Value) or 15) * (self.Genetics.ScaredDamageModifier/100)
  178.     }
  179.  
  180.     humanoid.WalkSpeed = self.Config.WalkSpeed
  181.     humanoid:SetAttribute("CanDamage", true)
  182.  
  183.     self.State = "Idle"
  184.     self.LastUpdate = tick()
  185.     self.BaseReproductionCooldown = 30 -- Base value before doubling
  186.     self.ReproductionCooldown = self.BaseReproductionCooldown
  187.     self.LastReproductionTime = tick()
  188.     self.ReproductionCount = 0 -- Track number of times this noob has reproduced
  189.  
  190.     local aiInstanceValue = model:FindFirstChild("AIInstance")
  191.     if not aiInstanceValue then
  192.         aiInstanceValue = Instance.new("ObjectValue")
  193.         aiInstanceValue.Name = "AIInstance"
  194.         aiInstanceValue.Parent = model
  195.     end
  196.     aiInstanceValue.Value = model
  197.  
  198.     ActiveNoobs[model] = self
  199.  
  200.     spawn(function()
  201.         self:BehaviorLoop()
  202.     end)
  203.  
  204.     print("Noob Spawned")
  205.     print("Genetics:", self.Genetics)
  206.  
  207.     return self
  208. end
  209.  
  210. -----------------------------------------------------------
  211. -- BehaviorLoop
  212. -----------------------------------------------------------
  213. function NoobAI:BehaviorLoop()
  214.     while self.Model and self.Model.Parent and not self.Dead do
  215.         self.LastUpdate = tick()
  216.  
  217.         if self.Humanoid.Health < (self.Config.Health * 0.2) and self.State ~= "Fleeing" then
  218.             self.State = "Fleeing"
  219.         end
  220.  
  221.         if self.State == "Idle" then
  222.             self:IdleBehavior()
  223.             self:CheckReproduction()
  224.         elseif self.State == "Chasing" then
  225.             self:PlayAnimation("Run", true)
  226.             self:ChaseBehavior()
  227.         elseif self.State == "Attacking" then
  228.             self:PlayAnimation("Attack", false)
  229.             self:AttackBehavior()
  230.         elseif self.State == "Fleeing" then
  231.             if not self.IsScared then
  232.                 self.IsScared = true
  233.                 self:PlayAnimation("Scared", false)
  234.                 wait(1)
  235.             end
  236.             self:PlayAnimation("Run", true)
  237.             self:FleeBehavior()
  238.         end
  239.  
  240.         -- Update despawn logic
  241.         if self:IsPlayerInRange(self.DespawnRadius) then
  242.             self.LastPlayerCheck = tick()
  243.         else
  244.             if tick() - self.LastPlayerCheck >= self.DespawnDelay then
  245.                 self:Despawn()
  246.                 break
  247.             end
  248.         end
  249.  
  250.         -- Check continuous idle time for despawn chance
  251.         if self.State == "Idle" then
  252.             local idleTime = tick() - self.LastStateChange
  253.             if idleTime >= 60 then -- 1 minute
  254.                 local minutesPast = math.floor(idleTime / 60)
  255.                 self.DespawnChanceMultiplier = 2 ^ (minutesPast - 1) -- Doubles every minute after 1
  256.                 if not self:IsPlayerInRange(self.DespawnRadius) and math.random() < (0.1 * self.DespawnChanceMultiplier) then
  257.                     self:Despawn()
  258.                     break
  259.                 end
  260.             end
  261.         end
  262.  
  263.         wait(0.1)
  264.     end
  265. end
  266.  
  267. -----------------------------------------------------------
  268. -- CheckReproduction
  269. -----------------------------------------------------------
  270. function NoobAI:CheckReproduction()
  271.     if tick() - self.LastReproductionTime < self.ReproductionCooldown then
  272.         return
  273.     end
  274.  
  275.     for model, partner in pairs(ActiveNoobs) do
  276.         if model ~= self.Model and model.Parent then
  277.             if partner.Genetics.Gender ~= self.Genetics.Gender and partner.State == "Idle" then
  278.                 local distance = (partner.RootPart.Position - self.RootPart.Position).Magnitude
  279.                 if distance < 5 then
  280.                     self:Reproduce(partner)
  281.                     self.LastReproductionTime = tick()
  282.                     partner.LastReproductionTime = tick()
  283.                     return
  284.                 end
  285.             end
  286.         end
  287.     end
  288. end
  289.  
  290. -----------------------------------------------------------
  291. -- Reproduce
  292. -----------------------------------------------------------
  293. function NoobAI:Reproduce(partner)
  294.     local spawnPos = (self.RootPart.Position + partner.RootPart.Position) / 2 + Vector3.new(math.random(-2,2), 0, math.random(-2,2))
  295.     local newNoob = NoobAI.new(spawnPos, {self.Genetics, partner.Genetics})
  296.  
  297.     -- Double reproduction cooldown for both parents
  298.     self.ReproductionCount = self.ReproductionCount + 1
  299.     self.ReproductionCooldown = self.BaseReproductionCooldown * (2 ^ self.ReproductionCount)
  300.     partner.ReproductionCount = partner.ReproductionCount + 1
  301.     partner.ReproductionCooldown = partner.BaseReproductionCooldown * (2 ^ partner.ReproductionCount)
  302.  
  303.     print("Reproduction successful: new noob spawned at", spawnPos)
  304.     print(self.Model.Name .. " new cooldown: ", self.ReproductionCooldown)
  305.     print(partner.Model.Name .. " new cooldown: ", partner.ReproductionCooldown)
  306. end
  307.  
  308. -----------------------------------------------------------
  309. -- IsPlayerInRange
  310. -----------------------------------------------------------
  311. function NoobAI:IsPlayerInRange(range)
  312.     for _, player in pairs(Players:GetPlayers()) do
  313.         local char = player.Character
  314.         local hrp = char and char:FindFirstChild("HumanoidRootPart")
  315.         if hrp then
  316.             if (hrp.Position - self.RootPart.Position).Magnitude <= range then
  317.                 return true
  318.             end
  319.         end
  320.     end
  321.     return false
  322. end
  323.  
  324. -----------------------------------------------------------
  325. -- PlayAnimation
  326. -----------------------------------------------------------
  327. function NoobAI:PlayAnimation(animationName, shouldLoop)
  328.     if self.LastAnimation == animationName then
  329.         return
  330.     end
  331.     self.LastAnimation = animationName
  332.  
  333.     if self.CurrentAnimationTrack then
  334.         self.CurrentAnimationTrack:Stop()
  335.     end
  336.  
  337.     local track = self.AnimationTracks[animationName]
  338.     if not track then
  339.         local anim = self.Animations:FindFirstChild(animationName)
  340.         if anim and anim:IsA("Animation") then
  341.             track = self.Animator:LoadAnimation(anim)
  342.             self.AnimationTracks[animationName] = track
  343.         end
  344.     end
  345.  
  346.     if track then
  347.         track.Looped = shouldLoop
  348.         track:Play()
  349.         self.CurrentAnimationTrack = track
  350.     else
  351.         self.CurrentAnimationTrack = nil
  352.     end
  353. end
  354.  
  355. -----------------------------------------------------------
  356. -- IdleBehavior
  357. -----------------------------------------------------------
  358. function NoobAI:IdleBehavior()
  359.     if math.random() < 0.3 then
  360.         self:PlayAnimation("Walk", true)
  361.         local randomOffset = Vector3.new(math.random(-10, 10), 0, math.random(-10, 10))
  362.         local randomDestination = self.RootPart.Position + randomOffset
  363.         self:MoveTowards(randomDestination, self.Config.WalkSpeed)
  364.     else
  365.         self:PlayAnimation("Idle", true)
  366.     end
  367.  
  368.     local closestTarget = self:FindClosestEnemy()
  369.     local isAggressive = (self.Humanoid.Health >= (self.Config.Health * 0.2)) and self.Config.Aggressive
  370.     if closestTarget and isAggressive then
  371.         local distance = (closestTarget.Character.HumanoidRootPart.Position - self.RootPart.Position).Magnitude
  372.         if distance < self.Config.FieldOfVision then
  373.             self.State = "Chasing"
  374.             self.Target = closestTarget
  375.         end
  376.     end
  377. end
  378.  
  379. -----------------------------------------------------------
  380. -- FindClosestEnemy
  381. -----------------------------------------------------------
  382. function NoobAI:FindClosestEnemy()
  383.     local closestPlayer = nil
  384.     local closestDistance = math.huge
  385.  
  386.     for _, player in pairs(Players:GetPlayers()) do
  387.         if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
  388.             local distance = (player.Character.HumanoidRootPart.Position - self.RootPart.Position).Magnitude
  389.             if distance < closestDistance then
  390.                 closestDistance = distance
  391.                 closestPlayer = player
  392.             end
  393.         end
  394.     end
  395.  
  396.     return closestPlayer
  397. end
  398.  
  399. -----------------------------------------------------------
  400. -- ChaseBehavior
  401. -----------------------------------------------------------
  402. function NoobAI:ChaseBehavior()
  403.     if not self.Target or not self.Target.Character then
  404.         self.State = "Idle"
  405.         return
  406.     end
  407.  
  408.     if self.Humanoid.Health < (self.Config.Health * 0.2) then
  409.         self.State = "Fleeing"
  410.         return
  411.     end
  412.  
  413.     local targetPos = self.Target.Character.HumanoidRootPart.Position
  414.     self:MoveTowards(targetPos, self.Config.RunSpeed)
  415.  
  416.     local distance = (targetPos - self.RootPart.Position).Magnitude
  417.     if distance < (self.Config.AttackRadius + 1) then
  418.         self.State = "Attacking"
  419.     end
  420. end
  421.  
  422. -----------------------------------------------------------
  423. -- AttackBehavior
  424. -----------------------------------------------------------
  425. function NoobAI:AttackBehavior()
  426.     if self.Target and self.Target.Character then
  427.         local targetHumanoid = self.Target.Character:FindFirstChildOfClass("Humanoid")
  428.         if targetHumanoid then
  429.             targetHumanoid:TakeDamage(self.Config.AttackDamage)
  430.         end
  431.     end
  432.     wait(1)
  433.     self.State = "Idle"
  434. end
  435.  
  436. -----------------------------------------------------------
  437. -- FleeBehavior
  438. -----------------------------------------------------------
  439. function NoobAI:FleeBehavior()
  440.     if not self.Target or not self.Target.Character or not self.Target.Character:FindFirstChild("HumanoidRootPart") then
  441.         self.State = "Idle"
  442.         self.FleeDestination = nil
  443.         return
  444.     end
  445.  
  446.     if not self.FleeDestination then
  447.         local directionAway = (self.RootPart.Position - self.Target.Character.HumanoidRootPart.Position).Unit
  448.         self.FleeDestination = self.RootPart.Position + directionAway * math.random(15, 30)
  449.     end
  450.  
  451.     self:MoveTowards(self.FleeDestination, self.Config.RunSpeed)
  452.  
  453.     if (self.FleeDestination - self.RootPart.Position).Magnitude < 3 then
  454.         self.State = "Idle"
  455.         self.FleeDestination = nil
  456.     end
  457. end
  458.  
  459. -----------------------------------------------------------
  460. -- MoveTowards
  461. -----------------------------------------------------------
  462. function NoobAI:MoveTowards(targetPos, speed)
  463.     self.Humanoid.WalkSpeed = speed
  464.     self.Humanoid:MoveTo(targetPos)
  465. end
  466.  
  467. -----------------------------------------------------------
  468. -- TakeDamage
  469. -----------------------------------------------------------
  470. function NoobAI:TakeDamage(amount, attacker)
  471.     self.Humanoid:TakeDamage(amount)
  472.     self.LastAttacker = attacker
  473.     self.LastStateChange = tick() -- Reset idle timer on damage
  474.  
  475.     if self.Humanoid.Health <= 0 then
  476.         self:Die()
  477.     elseif self.Humanoid.Health < (self.Config.Health * 0.2) or (self.State ~= "Fleeing" and amount >= self.Config.ScaredDamage) then
  478.         self.State = "Fleeing"
  479.         self.Target = attacker
  480.         self.FleeDestination = nil
  481.     end
  482. end
  483.  
  484. -----------------------------------------------------------
  485. -- Die
  486. -----------------------------------------------------------
  487. function NoobAI:Die()
  488.     self.Dead = true
  489.     self.RootPart.Anchored = true
  490.     self.Humanoid.PlatformStand = true
  491.     self:PlayAnimation("Die", false)
  492.  
  493.     if self.LastAttacker then
  494.         DataManager.AddKill(self.LastAttacker)
  495.     elseif self.Target then
  496.         DataManager.AddKill(self.Target)
  497.     else
  498.         print("No attacker recorded; kill not awarded.")
  499.     end
  500.  
  501.     LootManager.SpawnLootBag(1, 1, 2, 3, self.Model.PrimaryPart.Position)
  502.  
  503.     if self.CurrentAnimationTrack and self.CurrentAnimationTrack.Stopped then
  504.         self.CurrentAnimationTrack.Stopped:Wait()
  505.     else
  506.         wait(4)
  507.     end
  508.  
  509.     self:DestroyNoob()
  510. end
  511.  
  512. -----------------------------------------------------------
  513. -- Despawn
  514. -----------------------------------------------------------
  515. function NoobAI:Despawn()
  516.     self.Dead = true
  517.     self:DestroyNoob()
  518. end
  519.  
  520. -----------------------------------------------------------
  521. -- DestroyNoob
  522. -----------------------------------------------------------
  523. function NoobAI:DestroyNoob()
  524.     for name, track in pairs(self.AnimationTracks) do
  525.         track:Stop()
  526.         track:Destroy()
  527.     end
  528.     self.AnimationTracks = {}
  529.  
  530.     if self.Model then
  531.         self.Model:Destroy()
  532.     end
  533.  
  534.     ActiveNoobs[self.Model] = nil
  535. end
  536.  
  537. return NoobAI
Tags: Roblox
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement