No worries, it can be hard to conceptualize some of that kind of stuff.
I’ve done it quite a lot in many different forms so I get how that can be “weird.”
Biggest advice is do it SIMPLY. Even at the cost of performance starting out. When in doubt or you don’t know how to do it performantly, that’s ok just do it the un performant way to get it working and organize your code knowing your going to make it performant down the line.
Wanted to elaborate more on my idea said before, so I made a mock script of what it might look like. Again, I’m not the best lua coder so take my code more as an outline rather than working code lol
local TW = game:GetService("TweenService")
local MapBounds = {Vector2.new(0,0),Vector2.new(100,100)}
local MaxHeight = 100
local status = Instance.new("StringValue") -- Replaces the isChasing bool with something that can be used more generally
-- HOW STATUS WORKS:
-- Alternates between 3 values: Aggro (happens when the player screws with the NPC), Idle (happens at the end of movement/at respawn), and Moving (random movement)
-- Uses Changed Connect events to tell when to do each thing
status.Parent = script.Parent -- Feel free to customize this to fit your needs
local plrChar
local track1 = script.Parent.Humanoid.Animator:LoadAnimation(script.WalkAnim)
local track2 = script.Parent.Humanoid.Animator:LoadAnimation(script.IdleAnim)
local function getHeightAtPoint(position : Vector2)
local Pos3D = Vector3.new(position.X,MaxHeight,position.Y)
local result = workspace:Raycast(Pos3D,Vector3.new(0,-1,0)*(MaxHeight*1.1))
if result and result.Position then
return result.Position.Y
end
end
local function getRandomPosition()
local randX = math.random(MapBounds[1].X,MapBounds[2].X)
local randZ = math.random(MapBounds[1].Y,MapBounds[2].Y)
local height = getHeightAtPoint(Vector2.new(randX,randZ))
if not height then error("no height found") end
return Vector3.new(randX,height+1,randZ)
end
script.Parent.NPCHit.Event:Connect(function(char)
plrChar = char
status = "Aggro"
end)
script.Parent.Humanoid.Died:Connect(function()
for i, part in pairs(script.Parent:GetChildren()) do
if part:IsA("BasePart") then
TW:Create(part, TweenInfo.new(0.7), {Transparency=1}):Play()
end
end
task.wait(.7) -- Should probably change to a wait for child
script.Parent.Parent = game.ReplicatedStorage
status = "Idle"
end)
local function explore() -- Fell free to rename these crappy names. This is the "Moving" part of the script
status = "Moving"
track2:Stop()
track1:Play()
script.Parent.Humanoid:MoveTo(getRandomPosition())
script.Parent.Humanoid.MoveToFinished:Wait()
status = "Idle"
end
local function searchAndDestroy() -- AKA "isChasing". Again, sorry with the bad name-- I'm creatively bankrupt rn
track1:Stop()
track2:Play()
repeat
script.Parent.Humanoid:MoveTo(plrChar.HumanoidRootPart.Position)
track2:Stop()
track1:Play()
script.Parent.Humanoid.MoveToFinished:Wait()
until script.Parent.HumanoidRootPart.Position - plrChar.HumanoidRootPart.Position.Magnitude < 30
status = "Idle"
end
status.Changed:Connect(function(newValue)
if newValue == "Idle" then
explore()
elseif newValue == "Aggro" then
searchAndDestroy()
end
-- No Moving check because nothing's gonna happen if it sees its moving. Mainly there for conditioning checks and whatnot
end)