--[[ Evercyan @ March 2023 AI This script handles the physical behavior of mobs, notably Following and Jumping. Mobs automatically follow any player within range, but once attacked, they will no longer follow the closest player within range for the follow session, and will instead start attacking the player which recently attacked it. ]] --> Services local Players = game:GetService("Players") --> Dependencies local MobList = require(script.Parent.MobList) --> Variables local ActiveMobs = {} local AI = {} ---- UTILITY FUNCTIONS --------------------------------------------------------- local RaycastParams = RaycastParams.new() RaycastParams.FilterDescendantsInstances = { workspace:WaitForChild("Mobs"), workspace:WaitForChild("Characters") } RaycastParams.RespectCanCollide = true RaycastParams.IgnoreWater = true local function GetTableLength(t): number local n = 0 for _ in t do n += 1 end return n end -------------------------------------------------------------------------------- -- Gets the closest player within range to the given mob. If the closest player is the player the mob is already following, distance is 2x. -- Note: If streaming is enabled, MoveTo may produce undesired results if the max distance is ever higher than streaming minimum, such as the enemy -- appearing to 'idle' if the current distance/magnitude is between the streaming minimum and the max follow distance (including 2x possibility) function AI:GetClosestPlayer(Mob: MobList.Mob): (Player?, number?) local Closest = {Player = nil, Magnitude = math.huge} local ActivePlayer -- We retain a reference to this, so they have to get further away from the mob to stop following, instead of the usual distance. if Mob.Enemy.WalkToPart then local Player = Players:GetPlayerFromCharacter(Mob.Enemy.WalkToPart.Parent) if Player then ActivePlayer = Player end end for _, Player in Players:GetPlayers() do local Character = Player.Character if not Character then continue end local Magnitude = (Character:GetPivot().Position - Mob.Instance:GetPivot().Position).Magnitude local MaxDistance = (ActivePlayer == Player and (Mob.Config.FollowDistance or 32) * 2) or Mob.Config.FollowDistance or 32 if Magnitude <= MaxDistance and Magnitude < Closest.Magnitude then Closest.Player = Player Closest.Magnitude = Magnitude end end return Closest.Player, Closest.Player and Closest.Magnitude end -- Adds the mob to the ActiveMobs table. This table runs a few times per second and updates follow & jump code for active mobs. -- Active mobs are unanchored, and anchored back when they're not active (not within follow range of any player) function AI:StartTracking(Mob) if not Mob.Destroyed then ActiveMobs[Mob.Instance] = Mob end end -- Requests the mob to perform a jump if there's an obstacle in the way of it. function AI:RequestJump(Mob) if not Mob.Instance then return end local Root: BasePart = Mob.Root local JumpHeight: number = (Mob.Enemy.JumpPower ^ 2) / (2 * workspace.Gravity) * .8 local HipPoint: CFrame = Root.CFrame + (Root.CFrame.LookVector*(Root.Size.Z/2-0.2)) + (Root.CFrame.UpVector/-Root.Size.Y/2) local RaycastResult = workspace:Raycast( HipPoint.Position, HipPoint.LookVector * Mob.Enemy.WalkSpeed/4, RaycastParams ) if RaycastResult then -- There is an obstacle, but they should be able to jump over it! if RaycastResult.Instance ~= workspace.Terrain then local PartHeight: number = (RaycastResult.Instance.Position + Vector3.new(0, RaycastResult.Instance.Size.Y/2, 0)).Y if (HipPoint.Position.Y + JumpHeight) > PartHeight then Mob.Enemy.Jump = true end else Mob.Enemy.Jump = true end end end -------------------------------------------------------------------------------- -- Code to begin tracking, evenly spread out task.defer(function() local iterationsPerSec = 4 while true do local Mobn = GetTableLength(MobList) local Quota = Mobn / (60/iterationsPerSec) for _, Mob in MobList do if not Mob.isDead and not Mob.Destroyed then local Player = AI:GetClosestPlayer(Mob) if Player and not ActiveMobs[Mob.Instance] then task.defer(AI.StartTracking, AI, Mob) Quota -= 1 if Quota < 1 then Quota += Mobn / (60/iterationsPerSec) task.wait() end end end end task.wait() end end) -- Code to continue tracking & jumping on Active Mobs task.defer(function() while true do task.wait(1/4) for MobInstance, Mob in ActiveMobs do task.spawn(function() if not Mob.isDead and not Mob.Destroyed then local Player, m = AI:GetClosestPlayer(Mob) local Enemy = Mob.Enemy -- Simulation if Mob.Root.Anchored then Mob.Root.Anchored = false end if not Mob.Root:GetNetworkOwner() then Mob.Root:SetNetworkOwner(Player) task.wait(0.05) -- Give physics more time so it doesn't appear as choppy end -- Tracking if Player and Enemy then Enemy:MoveTo(Player.Character:GetPivot().Position, Player.Character.PrimaryPart) else ActiveMobs[MobInstance] = nil Mob.Root:SetNetworkOwner() Enemy:MoveTo(Mob.Origin.Position) end -- Jumping AI:RequestJump(Mob) else ActiveMobs[MobInstance] = nil end end) end end end) return AI