local x={}x.AreaV2=require(script:WaitForChild("\065\114\101\097\086\050"))x.AreaV7=require(script:WaitForChild("\065\114\101\097\086\055"))local X=require(script:WaitForChild("\079\098\106\101\099\116\084\114\097\099\107\101\114"))local C=game:GetService("\082\117\110\083\101\114\118\105\099\101")local b=game:GetService("\080\108\097\121\101\114\115")local L={}L.Settings={}L.Settings.Heartbeat=5 L.Settings.FolderName="\090\111\110\101\084\114\097\099\107\101\114\058 \065\114\101\097\115"L.Settings.FrontCenterPosition=false L.Settings.Part={Transparency=.7;Color=Color3.fromRGB(255,85,255);CastShadow=false;CanCollide=false,Anchored=true}L.Settings.AutoAddPlayersCharacter=true L.Settings.TrackedObjects={}local t={}local d={}t.__index=d local function n(x)local X,C,b=x:ToEulerAnglesYXZ()X,C,b=math.round(math.deg(X)),math.round(math.deg(C)),math.round(math.deg(b))return X%90==0 and(C%90==0 and b%90==0)end local function r(x)if typeof(x)=="\073\110\115\116\097\110\099\101"then return n(x.CFrame)elseif typeof(x)=="\067\070\114\097\109\101"then return n(x)end return false end function L.addArea(X,...)local C={...}if#C==0 then error("\087\114\111\110\103 \103\105\118\101\110 \112\097\114\097\109\101\116\101\114\115")elseif d[X]then error("\084\104\097\116 \110\097\109\101 \097\108\114\101\097\100\121 \101\120\105\115\116\115")else local b=setmetatable({},t)if typeof(C[#C])=="\115\116\114\105\110\103"and x[C[#C]]then b.Area=x[table.remove(C)].new(unpack(C))else b.Area=r(C[1])and x.AreaV2.new(...)or x.AreaV7.new(...)end b.enter=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")b.leave=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")b.onEnter=b.enter.Event b.onLeave=b.leave.Event b.TrackedObjectKeys={}function b.getObjects(x)local X={}for x in pairs(x.TrackedObjectKeys)do table.insert(X,L.Settings.TrackedObjects[x])end return X end d[X]=b return b end end function L.removeArea(x)d[x]=nil end function L.getArea(x)return d[x]end function L.addTrackedObject(x,X,C)if L.Settings.TrackedObjects[X or x]then error("\079\098\106\101\099\116\075\101\121 \047 \111\098\106\101\099\116 \097\108\114\101\097\100\121 \101\120\105\115\116\115 \097\115 \107\101\121\044 \105\116 \109\117\115\116 \098\101 \117\110\105\113\117\101")else L.setTrackedObject(x,X,C)end end function L.setTrackedObject(x,C,b)L.Settings.TrackedObjects[C or x]=X.new(x,b)end function L.removeTrackedObject(x)L.Settings.TrackedObjects[x]=nil end local h={}local function A(x)table.insert(h,x.CharacterAdded:Connect(function(X)L.addTrackedObject((X:WaitForChild("\072\117\109\097\110\111\105\100\082\111\111\116\080\097\114\116")).Parent,x)end))table.insert(h,x.CharacterRemoving:Connect(function()L.removeTrackedObject(x)end))end local function O()if C:IsClient()then A(b.LocalPlayer)else table.insert(h,b.PlayerAdded:Connect(function(x)A(x)end))end end local function y()for x,X in ipairs(h)do X:Disconnect()end h={}for x,X in ipairs(b:GetPlayers())do L.removeTrackedObject(X)end end O()function L.setAutoAddCharacter(x)if x==not L.Settings.AutoAddPlayersCharacter then if x then O()else y()end L.Settings.AutoAddPlayersCharacter=x else warn("\065\117\116\111\065\100\100\067\104\097\114\097\099\116\101\114 \105\115 \097\108\114\101\097\100\121 \105\110 \116\104\105\115 \115\116\097\116\101")end end function L.getAreas(x)local X={}for C,b in pairs(d)do if b.TrackedObjectKeys[x]then table.insert(X,b)end end return X end function L.switchMakeAreasVisible()local x=workspace:FindFirstChild(L.Settings.FolderName)if x then x:Destroy()else local x=Instance.new("\070\111\108\100\101\114")x.Name=L.Settings.FolderName x.Parent=workspace for X,C in pairs(d)do local b=Instance.new("\080\097\114\116")for x,X in pairs(L.Settings.Part)do b[x]=X end b.Name=X b.CFrame,b.Size=C.Area:getCF_Size()b.Parent=x end end end local function P()for x,X in pairs(L.Settings.TrackedObjects)do(coroutine.wrap(function()for C,b in pairs(d)do local t,d=b.Area:isInArea(L.Settings.FrontCenterPosition and X:getFCP()or X:getPosition()),b.TrackedObjectKeys[x]if not d and t then b.TrackedObjectKeys[x]=true b.enter:Fire(x)break elseif d and not t then b.TrackedObjectKeys[x]=nil b.leave:Fire(x)break end end end))()end end local l=0 C.Heartbeat:Connect(function(x)l+=x if l>=1/L.Settings.Heartbeat then l=0 P()end end)return L
-- @Author: VerdommeMan, see https://github.com/VerdommeMan/OT-AM for more information
-- @Version: 1.0.0

local shapes = {}
shapes.AreaV2 = require(script:WaitForChild("AreaV2"))
shapes.AreaV7 = require(script:WaitForChild("AreaV7"))
local ObjectTracker = require(script:WaitForChild("ObjectTracker"))

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local module = {}

--settings, can be changed directly instead of using the setters (read/write)
module.Settings = {}
module.Settings.Heartbeat = 5 --max 60
module.Settings.FolderName = "ZoneTracker: Areas" -- name used for the folder where the parts will be stored in for making Areas visible
module.Settings.FrontCenterPosition = false -- so instead of using the center of part to calculate if a player is inside an  area it will use the FrontCenterPosition (only if Size is available)
module.Settings.Part = {  --contains the props of the part that will generated for that Area when its made visible
    Transparency = 0.7,
    Color = Color3.fromRGB(255, 85, 255),
    CastShadow = false,
    CanCollide = false,
    Anchored = true
}

--settings, cant be changed directly (read only)
module.Settings.AutoAddPlayersCharacter = true -- if this is set to false then the person must manually add the player characters he wants to track
module.Settings.TrackedObjects = {} -- tracked objects are saved here

-- other stuff
local mtAreas = {} -- different mt table because i dont want to pollute Areas with metamethods
local Areas = {} -- a list of areas
mtAreas.__index = Areas

local function canUseAreaV2(cf) --
    local x, y, z = cf:ToEulerAnglesYXZ()
    x , y, z = math.round(math.deg(x)),math.round(math.deg(y)), math.round(math.deg(z))
    return x%90 == 0 and y%90 ==0 and z%90 == 0
end

local function checkIfAutoDetermineWhichArea(arg)
    if typeof(arg) == "Instance" then
        return canUseAreaV2(arg.CFrame)
    elseif typeof(arg) == "CFrame" then
        return canUseAreaV2(arg)
    end
    return false -- cant determine for these types
end

function module.addArea(uniqueName, ...) -- first param needs to be unique key for the area, then you add the constructor parameters and as last you have an optional override
    local args = {...}

    if #args == 0 then
        error("Wrong given parameters")
    elseif Areas[uniqueName] then
        error("That name already exists")
    else
        local area = setmetatable({}, mtAreas) -- mt allows ppl to access other areas from this table

        if typeof(args[#args]) == "string" and shapes[args[#args]] then -- the optional override, only works if the given string is a real type of Area
            area.Area = shapes[table.remove(args)].new(unpack(args))
        else
            area.Area = checkIfAutoDetermineWhichArea(args[1]) and shapes.AreaV2.new(...) or shapes.AreaV7.new(...)
        end
        area.enter = Instance.new("BindableEvent")
        area.leave = Instance.new("BindableEvent")
        area.onEnter = area.enter.Event
        area.onLeave = area.leave.Event
        area.TrackedObjectKeys = {}

        function area:getObjects()
            local keys = {}
            for key in pairs(self.TrackedObjectKeys) do
                table.insert(keys, module.Settings.TrackedObjects[key])
            end
            return keys
        end

        Areas[uniqueName] = area
        return area
    end
end

function module.removeArea(identifier)
    Areas[identifier] = nil
end

function module.getArea(indentifier)
    return Areas[indentifier]
end

function module.addTrackedObject(object, objectKey, size) -- objectKey is optional, this is what is returned from the bindeable event and must be unique, second is the parameters for the TrackedObject
    if module.Settings.TrackedObjects[objectKey or object] then
        error("ObjectKey / object already exists as key, it must be unique")
    else
        module.setTrackedObject(object, objectKey, size)
    end
end

function module.setTrackedObject(object, objectKey, size) -- object is the item you wanna track, objectKey is optional, this is what is returned from the bindeable event and must be unique, third is optional if you give an item without an Size property you can give it one so that you can still use FCP feature
        module.Settings.TrackedObjects[objectKey or object] = ObjectTracker.new(object , size)
end

function module.removeTrackedObject(objectKey)
    module.Settings.TrackedObjects[objectKey] = nil
end

local playerCharEvents = {} -- keeps a table of RBXScriptConnections

local function addCharEvents(player)
    table.insert(playerCharEvents, player.CharacterAdded:Connect(function(character)
        module.addTrackedObject(character:WaitForChild("HumanoidRootPart").Parent, player) -- in my testing sometimes the PrimaryPart isnt set fast enough
    end))
    table.insert(playerCharEvents, player.CharacterRemoving:Connect(function()
        module.removeTrackedObject(player)
    end))
end


local function addPlayerCharEvents() -- intended behavoir it only starts tracking new players that join, if you want to track already joind players then you must add them yourself
    if RunService:IsClient() then -- different behavoir depending on executed on client or server (client only adds his char while server adds everyones char)
       addCharEvents(Players.LocalPlayer)
    else
        table.insert(playerCharEvents, Players.PlayerAdded:Connect(function(player)
            addCharEvents(player)
        end))
    end
end

local function removePlayerCharEvents() -- intended behavior leave event never fires when shutdown and player was in an Area
    for _, event in ipairs(playerCharEvents) do
        event:Disconnect()
    end
    playerCharEvents = {}
    for _, player in ipairs(Players:GetPlayers()) do
        module.removeTrackedObject(player)
    end
end

addPlayerCharEvents()

function module.setAutoAddCharacter(bool) -- set this to false if you want to manually add the characters, you can turn it back on by setting it true again
    if bool == not module.Settings.AutoAddPlayersCharacter then
        if bool then
            addPlayerCharEvents()
        else
            removePlayerCharEvents()
        end
        module.Settings.AutoAddPlayersCharacter = bool
    else
        warn("AutoAddCharacter is already in this state")
    end
end

function module.getAreas(objectKey)
    local areas = {}

    for _, area in pairs(Areas) do
        if area.TrackedObjectKeys[objectKey] then
            table.insert(areas, area)
        end
    end
    return areas
end

function module.switchMakeAreasVisible() -- call it to make the areas visible, call it again to make the areas invisible
    local folder = workspace:FindFirstChild(module.Settings.FolderName)
    if folder then
        folder:Destroy()
    else
        local newFolder = Instance.new("Folder")
        newFolder.Name = module.Settings.FolderName
        newFolder.Parent = workspace

        for key, area in pairs(Areas) do
            local part = Instance.new("Part")

            for prop, value in pairs(module.Settings.Part) do
                part[prop] = value
            end
            part.Name = key
            part.CFrame , part.Size = area.Area:getCF_Size()
            part.Parent = newFolder
        end
    end
end

local function coreLoop()
    for key, to in pairs(module.Settings.TrackedObjects) do
        coroutine.wrap(function()
            for _, area in pairs(Areas) do
                local contains, object = area.Area:isInArea(module.Settings.FrontCenterPosition and to:getFCP() or to:getPosition()), area.TrackedObjectKeys[key]
                if not object and contains then
                    area.TrackedObjectKeys[key] = true
                    area.enter:Fire(key)
                    break
                elseif object and not contains then
                    area.TrackedObjectKeys[key] = nil
                    area.leave:Fire(key)
                    break
                end
            end
        end)()
    end
end

local sumDt = 0
RunService.Heartbeat:Connect(function(dt)
    sumDt += dt
    if sumDt >= 1 / module.Settings.Heartbeat then
        sumDt = 0
        coreLoop()
    end
end)

return module