173 lines
5.2 KiB
Plaintext
173 lines
5.2 KiB
Plaintext
--[[
|
|
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 |