173 lines
5.2 KiB
Plaintext
Raw Normal View History

2025-06-29 23:59:43 +08:00
--[[
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