Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- this module handles noob ai behaviour in addition to genetics
- -- author: Gixnly
- local NoobAI = {}
- NoobAI.__index = NoobAI
- -- 💡 SERVICES
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
- local Players = game:GetService("Players")
- local RunService = game:GetService("RunService")
- local Debris = game:GetService("Debris")
- local ServerScriptService = game:GetService("ServerScriptService")
- local DataManager = require(ServerScriptService.DataManager)
- local LootManager = require(ServerScriptService.LootManager)
- -- Global table to track all NoobAI instances, keyed by Model
- local ActiveNoobs = {}
- -----------------------------------------------------------
- -- Utility function: randomColorVariation
- -- Returns a Color3 that is a slight variation of the base color.
- -----------------------------------------------------------
- local function randomColorVariation(baseColor, variation)
- local r = math.clamp(baseColor.R + (math.random()-0.5)*variation, 0, 1)
- local g = math.clamp(baseColor.G + (math.random()-0.5)*variation, 0, 1)
- local b = math.clamp(baseColor.B + (math.random()-0.5)*variation, 0, 1)
- return Color3.new(r, g, b)
- end
- -----------------------------------------------------------
- -- Utility function: averageValue
- -- Returns the average of two numbers plus a small random variation.
- -----------------------------------------------------------
- local function averageValue(val1, val2, variation)
- return ((val1 + val2) / 2) + (math.random()-0.5)*variation
- end
- -----------------------------------------------------------
- -- Constructor: NoobAI.new
- -----------------------------------------------------------
- function NoobAI.new(spawnPosition, geneticParents)
- local noobTemplate = ReplicatedStorage:FindFirstChild("Noobs") and ReplicatedStorage.Noobs:FindFirstChild("Noob")
- assert(noobTemplate, "⚠️ Noob template missing from ReplicatedStorage.Noobs!")
- local model = noobTemplate:Clone()
- model:SetPrimaryPartCFrame(CFrame.new(spawnPosition))
- model.Parent = workspace
- model:SetAttribute("CanDamage", true)
- local humanoid = model:FindFirstChildOfClass("Humanoid")
- local rootPart = model:FindFirstChild("HumanoidRootPart")
- local animationsFolder = model:FindFirstChild("Animations")
- local lootFolder = model:FindFirstChild("Loot")
- assert(humanoid, "⚠️ Noob template is missing a Humanoid!")
- assert(rootPart, "⚠️ Noob template is missing a HumanoidRootPart!")
- assert(animationsFolder, "⚠️ Noob template is missing an 'Animations' folder!")
- humanoid.BreakJointsOnDeath = false
- local self = setmetatable({}, NoobAI)
- self.Model = model
- self.Humanoid = humanoid
- self.RootPart = rootPart
- self.Animations = animationsFolder
- self.LootFolder = lootFolder
- self.Target = nil
- self.IsScared = false
- self.LastAnimation = nil
- self.CurrentAnimationTrack = nil
- self.FleeDestination = nil
- self.Dead = false
- self.AnimationTracks = {}
- self.LastPlayerCheck = tick()
- self.DespawnDelay = 30
- self.DespawnRadius = 100
- self.LastAttacker = nil
- self.LastStateChange = tick() -- Track when state last changed
- self.DespawnChanceMultiplier = 1 -- Starts at 1x chance
- local animator = humanoid:FindFirstChild("Animator") or Instance.new("Animator", humanoid)
- self.Animator = animator
- local damageEvent = Instance.new("BindableEvent")
- damageEvent.Name = "TakeDamageEvent"
- damageEvent.Parent = model
- damageEvent.Event:Connect(function(damage, attacker)
- self:TakeDamage(damage, attacker)
- end)
- local stats = model:FindFirstChild("Stats")
- assert(stats, "⚠️ Noob template is missing a 'Stats' folder!")
- self.Genetics = {}
- if geneticParents then
- local parent1 = geneticParents[1]
- local parent2 = geneticParents[2]
- if math.random() < 0.5 then
- self.Genetics.Gender = parent1.Gender
- else
- self.Genetics.Gender = parent2.Gender
- end
- if self.Genetics.Gender == "Male" then
- local baseBlue = Color3.new(0.2, 0.2, 0.8)
- self.Genetics.TorsoColor = randomColorVariation(parent1.TorsoColor and Color3.new(
- averageValue(parent1.TorsoColor.R, parent2.TorsoColor.R, 0.1),
- averageValue(parent1.TorsoColor.G, parent2.TorsoColor.G, 0.1),
- averageValue(parent1.TorsoColor.B, parent2.TorsoColor.B, 0.1)
- ) or baseBlue, 0.05)
- else
- local basePink = Color3.new(0.9, 0.6, 0.7)
- self.Genetics.TorsoColor = randomColorVariation(parent1.TorsoColor and Color3.new(
- averageValue(parent1.TorsoColor.R, parent2.TorsoColor.R, 0.1),
- averageValue(parent1.TorsoColor.G, parent2.TorsoColor.G, 0.1),
- averageValue(parent1.TorsoColor.B, parent2.TorsoColor.B, 0.1)
- ) or basePink, 0.05)
- end
- local baseYellow = Color3.new(1, 0.9, 0.4)
- self.Genetics.HeadColor = randomColorVariation(parent1.HeadColor and Color3.new(
- averageValue(parent1.HeadColor.R, parent2.HeadColor.R, 0.1),
- averageValue(parent1.HeadColor.G, parent2.HeadColor.G, 0.1),
- averageValue(parent1.HeadColor.B, parent2.HeadColor.B, 0.1)
- ) or baseYellow, 0.05)
- self.Genetics.ArmColor = self.Genetics.HeadColor
- self.Genetics.HealthModifier = averageValue(parent1.HealthModifier or 100, parent2.HealthModifier or 100, 5)
- self.Genetics.ScaredDamageModifier = averageValue(parent1.ScaredDamageModifier or 15, parent2.ScaredDamageModifier or 15, 2)
- self.Genetics.FieldOfVisionModifier = averageValue(parent1.FieldOfVisionModifier or 20, parent2.FieldOfVisionModifier or 20, 2)
- else
- if math.random() < 0.5 then
- self.Genetics.Gender = "Male"
- else
- self.Genetics.Gender = "Female"
- end
- if self.Genetics.Gender == "Male" then
- local baseBlue = Color3.new(0.2, 0.2, 0.8)
- self.Genetics.TorsoColor = randomColorVariation(baseBlue, 0.05)
- else
- local basePink = Color3.new(0.9, 0.6, 0.7)
- self.Genetics.TorsoColor = randomColorVariation(basePink, 0.05)
- end
- local baseYellow = Color3.new(1, 0.9, 0.4)
- self.Genetics.HeadColor = randomColorVariation(baseYellow, 0.05)
- self.Genetics.ArmColor = self.Genetics.HeadColor
- self.Genetics.HealthModifier = math.random(90, 110)
- self.Genetics.ScaredDamageModifier = math.random(13, 17)
- self.Genetics.FieldOfVisionModifier = math.random(18, 22)
- end
- local torso = model:FindFirstChild("Torso")
- if torso then
- torso.Color = self.Genetics.TorsoColor
- end
- local head = model:FindFirstChild("Head")
- if head then
- head.Color = self.Genetics.HeadColor
- end
- local leftArm = model:FindFirstChild("Left Arm")
- local rightArm = model:FindFirstChild("Right Arm")
- if leftArm then
- leftArm.Color = self.Genetics.ArmColor
- end
- if rightArm then
- rightArm.Color = self.Genetics.ArmColor
- end
- self.Config = {
- WalkSpeed = (stats:FindFirstChild("WalkSpeed") and stats.WalkSpeed.Value) or 8,
- RunSpeed = (stats:FindFirstChild("RunSpeed") and stats.RunSpeed.Value) or 16,
- Aggressive = (stats:FindFirstChild("Aggressive") and stats.Aggressive.Value) or (math.random() < 0.2),
- AttackRadius = (stats:FindFirstChild("AttackRadius") and stats.AttackRadius.Value) or 5,
- FieldOfVision= ((stats:FindFirstChild("FieldOfVision")and stats.FieldOfVision.Value) or 20) * (self.Genetics.FieldOfVisionModifier/100),
- Health = ((stats:FindFirstChild("Health") and stats.Health.Value) or 100) * (self.Genetics.HealthModifier/100),
- AttackDamage = (stats:FindFirstChild("AttackDamage") and stats.AttackDamage.Value) or 10,
- ScaredDamage = ((stats:FindFirstChild("ScaredDamage") and stats.ScaredDamage.Value) or 15) * (self.Genetics.ScaredDamageModifier/100)
- }
- humanoid.WalkSpeed = self.Config.WalkSpeed
- humanoid:SetAttribute("CanDamage", true)
- self.State = "Idle"
- self.LastUpdate = tick()
- self.BaseReproductionCooldown = 30 -- Base value before doubling
- self.ReproductionCooldown = self.BaseReproductionCooldown
- self.LastReproductionTime = tick()
- self.ReproductionCount = 0 -- Track number of times this noob has reproduced
- local aiInstanceValue = model:FindFirstChild("AIInstance")
- if not aiInstanceValue then
- aiInstanceValue = Instance.new("ObjectValue")
- aiInstanceValue.Name = "AIInstance"
- aiInstanceValue.Parent = model
- end
- aiInstanceValue.Value = model
- ActiveNoobs[model] = self
- spawn(function()
- self:BehaviorLoop()
- end)
- print("Noob Spawned")
- print("Genetics:", self.Genetics)
- return self
- end
- -----------------------------------------------------------
- -- BehaviorLoop
- -----------------------------------------------------------
- function NoobAI:BehaviorLoop()
- while self.Model and self.Model.Parent and not self.Dead do
- self.LastUpdate = tick()
- if self.Humanoid.Health < (self.Config.Health * 0.2) and self.State ~= "Fleeing" then
- self.State = "Fleeing"
- end
- if self.State == "Idle" then
- self:IdleBehavior()
- self:CheckReproduction()
- elseif self.State == "Chasing" then
- self:PlayAnimation("Run", true)
- self:ChaseBehavior()
- elseif self.State == "Attacking" then
- self:PlayAnimation("Attack", false)
- self:AttackBehavior()
- elseif self.State == "Fleeing" then
- if not self.IsScared then
- self.IsScared = true
- self:PlayAnimation("Scared", false)
- wait(1)
- end
- self:PlayAnimation("Run", true)
- self:FleeBehavior()
- end
- -- Update despawn logic
- if self:IsPlayerInRange(self.DespawnRadius) then
- self.LastPlayerCheck = tick()
- else
- if tick() - self.LastPlayerCheck >= self.DespawnDelay then
- self:Despawn()
- break
- end
- end
- -- Check continuous idle time for despawn chance
- if self.State == "Idle" then
- local idleTime = tick() - self.LastStateChange
- if idleTime >= 60 then -- 1 minute
- local minutesPast = math.floor(idleTime / 60)
- self.DespawnChanceMultiplier = 2 ^ (minutesPast - 1) -- Doubles every minute after 1
- if not self:IsPlayerInRange(self.DespawnRadius) and math.random() < (0.1 * self.DespawnChanceMultiplier) then
- self:Despawn()
- break
- end
- end
- end
- wait(0.1)
- end
- end
- -----------------------------------------------------------
- -- CheckReproduction
- -----------------------------------------------------------
- function NoobAI:CheckReproduction()
- if tick() - self.LastReproductionTime < self.ReproductionCooldown then
- return
- end
- for model, partner in pairs(ActiveNoobs) do
- if model ~= self.Model and model.Parent then
- if partner.Genetics.Gender ~= self.Genetics.Gender and partner.State == "Idle" then
- local distance = (partner.RootPart.Position - self.RootPart.Position).Magnitude
- if distance < 5 then
- self:Reproduce(partner)
- self.LastReproductionTime = tick()
- partner.LastReproductionTime = tick()
- return
- end
- end
- end
- end
- end
- -----------------------------------------------------------
- -- Reproduce
- -----------------------------------------------------------
- function NoobAI:Reproduce(partner)
- local spawnPos = (self.RootPart.Position + partner.RootPart.Position) / 2 + Vector3.new(math.random(-2,2), 0, math.random(-2,2))
- local newNoob = NoobAI.new(spawnPos, {self.Genetics, partner.Genetics})
- -- Double reproduction cooldown for both parents
- self.ReproductionCount = self.ReproductionCount + 1
- self.ReproductionCooldown = self.BaseReproductionCooldown * (2 ^ self.ReproductionCount)
- partner.ReproductionCount = partner.ReproductionCount + 1
- partner.ReproductionCooldown = partner.BaseReproductionCooldown * (2 ^ partner.ReproductionCount)
- print("Reproduction successful: new noob spawned at", spawnPos)
- print(self.Model.Name .. " new cooldown: ", self.ReproductionCooldown)
- print(partner.Model.Name .. " new cooldown: ", partner.ReproductionCooldown)
- end
- -----------------------------------------------------------
- -- IsPlayerInRange
- -----------------------------------------------------------
- function NoobAI:IsPlayerInRange(range)
- for _, player in pairs(Players:GetPlayers()) do
- local char = player.Character
- local hrp = char and char:FindFirstChild("HumanoidRootPart")
- if hrp then
- if (hrp.Position - self.RootPart.Position).Magnitude <= range then
- return true
- end
- end
- end
- return false
- end
- -----------------------------------------------------------
- -- PlayAnimation
- -----------------------------------------------------------
- function NoobAI:PlayAnimation(animationName, shouldLoop)
- if self.LastAnimation == animationName then
- return
- end
- self.LastAnimation = animationName
- if self.CurrentAnimationTrack then
- self.CurrentAnimationTrack:Stop()
- end
- local track = self.AnimationTracks[animationName]
- if not track then
- local anim = self.Animations:FindFirstChild(animationName)
- if anim and anim:IsA("Animation") then
- track = self.Animator:LoadAnimation(anim)
- self.AnimationTracks[animationName] = track
- end
- end
- if track then
- track.Looped = shouldLoop
- track:Play()
- self.CurrentAnimationTrack = track
- else
- self.CurrentAnimationTrack = nil
- end
- end
- -----------------------------------------------------------
- -- IdleBehavior
- -----------------------------------------------------------
- function NoobAI:IdleBehavior()
- if math.random() < 0.3 then
- self:PlayAnimation("Walk", true)
- local randomOffset = Vector3.new(math.random(-10, 10), 0, math.random(-10, 10))
- local randomDestination = self.RootPart.Position + randomOffset
- self:MoveTowards(randomDestination, self.Config.WalkSpeed)
- else
- self:PlayAnimation("Idle", true)
- end
- local closestTarget = self:FindClosestEnemy()
- local isAggressive = (self.Humanoid.Health >= (self.Config.Health * 0.2)) and self.Config.Aggressive
- if closestTarget and isAggressive then
- local distance = (closestTarget.Character.HumanoidRootPart.Position - self.RootPart.Position).Magnitude
- if distance < self.Config.FieldOfVision then
- self.State = "Chasing"
- self.Target = closestTarget
- end
- end
- end
- -----------------------------------------------------------
- -- FindClosestEnemy
- -----------------------------------------------------------
- function NoobAI:FindClosestEnemy()
- local closestPlayer = nil
- local closestDistance = math.huge
- for _, player in pairs(Players:GetPlayers()) do
- if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
- local distance = (player.Character.HumanoidRootPart.Position - self.RootPart.Position).Magnitude
- if distance < closestDistance then
- closestDistance = distance
- closestPlayer = player
- end
- end
- end
- return closestPlayer
- end
- -----------------------------------------------------------
- -- ChaseBehavior
- -----------------------------------------------------------
- function NoobAI:ChaseBehavior()
- if not self.Target or not self.Target.Character then
- self.State = "Idle"
- return
- end
- if self.Humanoid.Health < (self.Config.Health * 0.2) then
- self.State = "Fleeing"
- return
- end
- local targetPos = self.Target.Character.HumanoidRootPart.Position
- self:MoveTowards(targetPos, self.Config.RunSpeed)
- local distance = (targetPos - self.RootPart.Position).Magnitude
- if distance < (self.Config.AttackRadius + 1) then
- self.State = "Attacking"
- end
- end
- -----------------------------------------------------------
- -- AttackBehavior
- -----------------------------------------------------------
- function NoobAI:AttackBehavior()
- if self.Target and self.Target.Character then
- local targetHumanoid = self.Target.Character:FindFirstChildOfClass("Humanoid")
- if targetHumanoid then
- targetHumanoid:TakeDamage(self.Config.AttackDamage)
- end
- end
- wait(1)
- self.State = "Idle"
- end
- -----------------------------------------------------------
- -- FleeBehavior
- -----------------------------------------------------------
- function NoobAI:FleeBehavior()
- if not self.Target or not self.Target.Character or not self.Target.Character:FindFirstChild("HumanoidRootPart") then
- self.State = "Idle"
- self.FleeDestination = nil
- return
- end
- if not self.FleeDestination then
- local directionAway = (self.RootPart.Position - self.Target.Character.HumanoidRootPart.Position).Unit
- self.FleeDestination = self.RootPart.Position + directionAway * math.random(15, 30)
- end
- self:MoveTowards(self.FleeDestination, self.Config.RunSpeed)
- if (self.FleeDestination - self.RootPart.Position).Magnitude < 3 then
- self.State = "Idle"
- self.FleeDestination = nil
- end
- end
- -----------------------------------------------------------
- -- MoveTowards
- -----------------------------------------------------------
- function NoobAI:MoveTowards(targetPos, speed)
- self.Humanoid.WalkSpeed = speed
- self.Humanoid:MoveTo(targetPos)
- end
- -----------------------------------------------------------
- -- TakeDamage
- -----------------------------------------------------------
- function NoobAI:TakeDamage(amount, attacker)
- self.Humanoid:TakeDamage(amount)
- self.LastAttacker = attacker
- self.LastStateChange = tick() -- Reset idle timer on damage
- if self.Humanoid.Health <= 0 then
- self:Die()
- elseif self.Humanoid.Health < (self.Config.Health * 0.2) or (self.State ~= "Fleeing" and amount >= self.Config.ScaredDamage) then
- self.State = "Fleeing"
- self.Target = attacker
- self.FleeDestination = nil
- end
- end
- -----------------------------------------------------------
- -- Die
- -----------------------------------------------------------
- function NoobAI:Die()
- self.Dead = true
- self.RootPart.Anchored = true
- self.Humanoid.PlatformStand = true
- self:PlayAnimation("Die", false)
- if self.LastAttacker then
- DataManager.AddKill(self.LastAttacker)
- elseif self.Target then
- DataManager.AddKill(self.Target)
- else
- print("No attacker recorded; kill not awarded.")
- end
- LootManager.SpawnLootBag(1, 1, 2, 3, self.Model.PrimaryPart.Position)
- if self.CurrentAnimationTrack and self.CurrentAnimationTrack.Stopped then
- self.CurrentAnimationTrack.Stopped:Wait()
- else
- wait(4)
- end
- self:DestroyNoob()
- end
- -----------------------------------------------------------
- -- Despawn
- -----------------------------------------------------------
- function NoobAI:Despawn()
- self.Dead = true
- self:DestroyNoob()
- end
- -----------------------------------------------------------
- -- DestroyNoob
- -----------------------------------------------------------
- function NoobAI:DestroyNoob()
- for name, track in pairs(self.AnimationTracks) do
- track:Stop()
- track:Destroy()
- end
- self.AnimationTracks = {}
- if self.Model then
- self.Model:Destroy()
- end
- ActiveNoobs[self.Model] = nil
- end
- return NoobAI
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement