-- Version 1.811 (optimized visualize, cached Enums, changed CheckForTimeout to CheckTimeout, Jump no long returns the waypoint position[unintended], OnDestroy now fires instantly when destroy is called, did a small change to the Humanoid Jump logic)

--[[

MIT License

Copyright (c) <2025> <grewsxb4>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

]]

local Path = require(script:WaitForChild("Path"))
local RS = game:GetService("RunService")
local Signal = require(script.Parent.Signal)

local module = {}

local NoobPath = {}
NoobPath.__index = NoobPath

local JUMP = Enum.PathWaypointAction.Jump
local AIR = Enum.Material.Air
local JUMPING = Enum.HumanoidStateType.Jumping
local FREEFALL = Enum.HumanoidStateType.Freefall

export type Location = Vector3 | BasePart | Model

local function GetPrimaryPivot(Model : Model)
	return Model.PrimaryPart and Model.PrimaryPart:GetPivot() or Model:GetPivot()
end

local function ToVector3(Location : Location)
	if typeof(Location) == "Vector3" then
		return Location
	elseif Location:IsA("Model") then
		return GetPrimaryPivot(Location).Position
	elseif Location:IsA("BasePart") then
		return Location:GetPivot().Position
	end

    return
end

local function RemoveNetworkOwner(Character : Model)
	local Descendants = Character:GetDescendants()
	for i = 1, #Descendants do
		local Item = Descendants[i]
		if Item:IsA("BasePart") and Item:CanSetNetworkOwnership() then
			Item:SetNetworkOwner(nil)
		end
	end
end

local function Default(AgentParams, NoobPath)
	NoobPath.Path = Path.new(AgentParams)
	NoobPath.Route = {}
	NoobPath.Index = 1
	NoobPath.Idle = true
	NoobPath.InAir = false
	NoobPath.Estimate = {}
	NoobPath.Destroying = false
	NoobPath.Goal = nil
	NoobPath.Partial = false

	NoobPath.Overide = Signal.new()
	NoobPath.Reached = Signal.new()
	NoobPath.WaypointReached = Signal.new()
	NoobPath.Error = Signal.new()
	NoobPath.Trapped = Signal.new()
	NoobPath.Stopped = Signal.new()
	NoobPath.OnDestroy = Signal.new()

	NoobPath.Timeout = false -- trigger trapped if didn't arrive in time, time is auto estimated using speed
	NoobPath.Speed = 16 -- default speed
	NoobPath.Visualize = false -- if there are many npcs with visualize on, it can create lag & disrupt pathfind
end

local Server = RS:IsServer()

-- Constructor
function module.new(Character: Model, AgentParams: Path.AgentParams?, Move: (Vector3) -> nil, Jump: (() -> nil)?, JumpFinished: RBXScriptSignal, MoveFinished: RBXScriptSignal)
	if Server then
		RemoveNetworkOwner(Character)
	end

	local self = setmetatable({
		Character = Character,

		Move = Move,
		Jump = Jump,
		MoveFinished = MoveFinished,
		JumpFinished = JumpFinished,

		MoveFinishedC = nil,
		JumpFinishedC = nil,

	}, NoobPath)

	Default(AgentParams, self)

	self:Activate()

	return self
end

-- Constructor With Default Humanoid Signals
function module.Humanoid(Character : Model, AgentParams : Path.AgentParams?, Precise : boolean?)
	local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
	local Move = function(WaypointPosition) Humanoid:MoveTo(WaypointPosition) end

	local JumpFinished = Signal.new()
	local MoveFinished = Humanoid.MoveToFinished

	local self = module.new(Character, AgentParams, Move, nil, JumpFinished, MoveFinished)

	local Jump = function()
		if Humanoid.FloorMaterial ~= AIR then
			Humanoid:ChangeState(JUMPING)
			return
		end

		local C : RBXScriptConnection
		local A : RBXScriptConnection
		local B : RBXScriptConnection

		if Precise then
			C = self.Overide:Connect(function()
				C:Disconnect()
				A:Disconnect()
				B:Disconnect()
			end)
		else
			C = self.Overide:Connect(function()
				C:Disconnect()
				A:Disconnect()
				B:Disconnect()
				self.InAir = false -- this allow the agent to still compute path even in air, which is often inaccurate. However, it works exceptionally well when given a relatively low jump power and obstacles that fits with such a jump power, and works nicely for default humanoids with a jump power of 50.
				-- copy this down if you want this behavior in custom implementations, otherwise computing path is paused while the agent is in air by default.
			end)
		end

		A = Humanoid:GetPropertyChangedSignal("FloorMaterial"):Connect(function()
			if Humanoid.FloorMaterial ~= AIR then
				Humanoid:ChangeState(JUMPING)
				C:Disconnect()
				A:Disconnect()
				B:Disconnect()
			end
		end)
		B = self.OnDestroy:Connect(function()
			C:Disconnect()
			A:Disconnect()
			B:Disconnect()
		end)
	end

	local C = Humanoid.StateChanged:Connect(function(Old, New)
		if (Old == JUMPING or Old == FREEFALL) and (New ~= JUMPING and New ~= FREEFALL) then
			JumpFinished:Fire()
		end
	end)

	local A
	A = self.OnDestroy:Connect(function()
		A:Disconnect()
		C:Disconnect()
	end)

	self.Jump = Jump

	return self
end

function module.View(AgentParams : Path.AgentParams)
	local self = setmetatable({
		View = true,

	}, NoobPath)

	Default(AgentParams, self)

	return self
end

function NoobPath:Compute(PointA : Vector3, PointB : Vector3)
	local Route, Partial = self.Path:Generate(PointA, PointB)

	if self.Destroying ~= false then
		return
	end

	if not self:ValidateRoute(Route, Partial) then
		return
	end

	self.Route = Route
	self.Partial = Partial

	if self.Timeout then
		if not self.Speed then
			error("No Speed Provided")
		end
		self:Predict()
	end

	if self.Visualize then
		self:Show()
	end

	return true
end

function NoobPath:Show()
	self.Path:Show(self.Route)
end

function NoobPath:Hide()
	self.Path:Hide()
end

function NoobPath:Predict()
	self.Estimate = self.Path:Estimate(self.Route, self.Speed)
end

function NoobPath:GetEstimateTotal()
	local Estimate = self.Estimate

	local Sum = 0
	for i = 1, #Estimate do
		Sum += Estimate[i]
	end

	return Sum
end

function NoobPath:Activate()
	if Server then
		self.DescendantAddedC = self.Character.DescendantAdded:Connect(function(Item)
			if Item:IsA("BasePart") and Item:CanSetNetworkOwnership() then
				Item:SetNetworkOwner(nil)
			end
		end)
	end

	self.MoveFinishedC = self.MoveFinished:Connect(function(Success) -- if you are using custom MoveFinished, Fire(true) if successfully reached
		if self.Idle then
			return
		end
		if Success then
			local NextWaypoint = self:GetNextWaypoint()
			if NextWaypoint then
				self:TravelNextWaypoint()
				self.WaypointReached:Fire(self:GetWaypoint(), NextWaypoint)

			else
				self:Arrive()
			end
		else
			self:Stop()
			self.Trapped:Fire("ReachFailed")
		end
	end)
	self.JumpFinishedC = self.JumpFinished:Connect(function()
		self.InAir = false
	end)
end

function NoobPath:PauseUntilLanded()
	local C :RBXScriptConnection
	local A : RBXScriptConnection
	local B :RBXScriptConnection

	C = self.Overide:Connect(function()
		C:Disconnect()
		A:Disconnect()
		B:Disconnect()
	end)

	A = self.JumpFinished:Connect(function()
		C:Disconnect()
		A:Disconnect()
		B:Disconnect()
		self:Run()
	end)

	B = self.OnDestroy:Connect(function()
		C:Disconnect()
		A:Disconnect()
		B:Disconnect()
	end)
end

function NoobPath:ValidateRoute(Route : Path.Route, Partial : boolean)
	if not Route then
		self.Error:Fire("ComputationError") -- No Route Generated/Error when generating
		return
	end
	if #Route == 0 then
		self.Error:Fire("TargetUnreachable") -- Can't Find Path
		return
	end
	if #Route < 2 then -- Route too short, likely already arrived
		self:Arrive(Route, Partial)
		return
	end
	return true
end

-- Caculate Route To Location & Move The Character There
function NoobPath:Run(Location : Location)
	if self.Destroying ~= false then
		return
	end

	self.Overide:Fire()

	if Location then
		self.Goal = Location
	else
		Location = self.Goal or error("No Destination Provided")
	end

	if self.InAir then
		self:PauseUntilLanded()
		return
	end

	if not self:Compute(GetPrimaryPivot(self.Character).Position, ToVector3(Location)) then
		return
	end

	self.Index = 1
	self.Idle = false

	self:TravelNextWaypoint()
end

-- Stop The Character From Moving
function NoobPath:Stop()
	if self.Destroying ~= false then
		return
	end
	self.Idle = true

	self.Overide:Fire()
	self.Move(GetPrimaryPivot(self.Character).Position)
	self.Stopped:Fire()
end

function NoobPath:GetLastWaypoint()
	local Route = self.Route
	return Route[#Route]
end

function NoobPath:GetWaypoint()
	return self.Route[self.Index]
end

function NoobPath:GetNextWaypoint()
	return self.Route[self.Index + 1]
end

function NoobPath:GetEstimateTime()
	return self.Estimate[self.Index]
end

function NoobPath:CheckTimeout()
	local Time = self:GetEstimateTime()
	if not Time then
		return
	end

	local Route = self.Route
	local Index = self.Index

	task.delay(Time * 2, function() -- usually double time work best, not too sensitive
		if not self.Idle and self.Route == Route and self.Index == Index then
			self.Trapped:Fire("ReachTimeout")
		end
	end)
end

function NoobPath:TravelNextWaypoint()
	self.Index += 1
	self:TravelWaypoint()
end

function NoobPath:TravelWaypoint()
	local Waypoint = self:GetWaypoint()
	if self.Idle or not Waypoint then
		return
	end
	self.Move(Waypoint.Position)
	if self.Timeout then
		self:CheckTimeout()
	end
	if Waypoint.Action == JUMP then
		self.InAir = true
		self.Jump()
	end
end

function NoobPath:Arrive(Route, Partial)
	Route = Route or self.Route
	local Waypoint = Route[#Route]

	self.Idle = true
	self.Overide:Fire()
	self.Reached:Fire(Waypoint, Partial or self.Partial)
end

local function Terminate(self)
	self.Path:Destroy()
	self.Reached:DisconnectAll()
	self.WaypointReached:DisconnectAll()
	self.Trapped:DisconnectAll()
	self.Error:DisconnectAll()
	self.Overide:DisconnectAll()
	self.OnDestroy:DisconnectAll()

	table.clear(self)
	setmetatable(self, nil)
	self = nil
end

function NoobPath:Dump()
	self.Destroying = true
	self.OnDestroy:Fire()

	task.defer(Terminate, self) -- defer for things to settle down
end

-- Destroy The NoobPath Object
function NoobPath:Destroy()
	self:Stop()
	self.Destroying = true

	self.MoveFinishedC:Disconnect()
	self.JumpFinishedC:Disconnect()

	if Server then
		self.DescendantAddedC:Disconnect()
	end

	self:Dump()
end

return module


--------------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- API:                                                                       --
--   local Signal = require(THIS MODULE)                                      --
--   local sig = Signal.new()                                                 --
--   local connection = sig:Connect(function(arg1, arg2, ...) ... end)        --
--   sig:Fire(arg1, arg2, ...)                                                --
--   connection:Disconnect()                                                  --
--   sig:DisconnectAll()                                                      --
--   local arg1, arg2, ... = sig:Wait()                                       --
--                                                                            --
-- Licence:                                                                   --
--   Licenced under the MIT licence.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--------------------------------------------------------------------------------

export type Connection = {
	--- Disconnects this connection.
	Disconnect: (self: Connection) -> (),
}

export type Signal<T...> = {
	--- Connects a function to this signal so that, when it's fired, it executes.
	Connect: (self: Signal<T...>, funcToConnect: (T...) -> ()) -> Connection,
	--- Disconnects all of the connections to this signal.
	DisconnectAll: (self: Signal<T...>) -> (),
	--- Fires this signal and executes all connections.
	Fire: (self: Signal<T...>, T...) -> (),
	--- Same as `Fire()`, but uses `task.defer()` internally & doesn't take advantage of thread reuse.
	FireDeferred: (self: Signal<T...>, T...) -> (),
	--- Yields the current thread until this connection is fired.
	Wait: (self: Signal<T...>) -> T...,
	--- Connects a function to this signal so that, when it's fired, it executes.
	---
	--- The difference with `Signal:Connect()` is that, when it executes once, it disconnects itself.
	Once: (self: Signal<T...>, funcToConnect: (T...) -> ()) -> Connection,
}

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread()
	-- We cannot use the initial set of arguments passed to
	-- runEventHandlerInFreeThread for a call to the handler, because those
	-- arguments would stay on the stack for the duration of the thread's
	-- existence, temporarily leaking references. Without access to raw bytecode
	-- there's no way for us to clear the "..." references from the stack.
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

-- Connection class
local Connection = {}
Connection.__index = Connection

function Connection.new<T...>(signal: Signal<T...>, fn: (T...) -> ())
	return setmetatable({
		_connected = true,
		_signal = signal,
		_fn = fn,
		_next = false,
	}, Connection)
end

function Connection:Disconnect()
	self._connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end
Connection.Destroy = Connection.Disconnect

-- Make Connection strict
setmetatable(Connection, {
	__index = function(tb, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(tb, key, value)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end
})

-- Signal class
local Signal = {}
Signal.__index = Signal

function Signal.new()
	return setmetatable({
		_handlerListHead = false,
	}, Signal)
end

function Signal:Connect<T...>(fn: (T...) -> ())
	local connection = Connection.new(self, fn)
	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end
	return connection
end

-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
function Signal:DisconnectAll()
	self._handlerListHead = false
end
Signal.Destroy = Signal.DisconnectAll

-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item._connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
				-- Get the freeRunnerThread to the first yield
				coroutine.resume(freeRunnerThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

-- Implement Signal:Wait() in terms of a temporary connection using
-- a Signal:Connect() which disconnects itself.
function Signal:Wait()
	local waitingCoroutine = coroutine.running()
	local cn
	cn = self:Connect(function(...)
		cn:Disconnect()
		task.spawn(waitingCoroutine, ...)
	end)
	return coroutine.yield()
end

-- Implement Signal:Once() in terms of a connection which disconnects
-- itself before running the handler.
function Signal:Once<T...>(fn: (T...) -> ())
	local cn
	cn = self:Connect(function(...)
		if cn._connected then
			cn:Disconnect()
		end
		fn(...)
	end)
	return cn
end

-- Make signal strict
setmetatable(Signal, {
	__index = function(tb, key)
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(tb, key, value)
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end
})

return Signal


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CanBenchmark = RunService:IsStudio() and workspace:GetAttribute("DebugAllowed")

local SSSDone = false
local RSDone = false

local totalModules = 0
local loadTimes = {}

local function InitModule(Module: ModuleScript)
    debug.setmemorycategory(Module.Name)
    local Scr = require(Module)
    if typeof(Scr) == "table" and typeof(Scr.Init) == "function" then
        Scr:Init()
    end
end

task.spawn(function()
    for _, Module in script.Parent:GetDescendants() do
        if not Module:IsA("ModuleScript") then
            continue
        end

        totalModules += 1

        if CanBenchmark then
            local start = os.clock()
            pcall(InitModule, Module)
            local duration = os.clock() - start

            table.insert(loadTimes, {
                name = Module:GetFullName(),
                time = duration
            })
        else
            pcall(InitModule, Module)
        end
    end
    debug.setmemorycategory("Default")
    SSSDone = true
end)

task.spawn(function()
    require(ReplicatedStorage.Initter):Init()
    RSDone = true
end)

while not (SSSDone and RSDone) do
    task.wait()
end

if CanBenchmark then
    table.sort(loadTimes, function(a, b)
        return a.time > b.time
    end)

    print("Total modules loaded:", totalModules)
    print("Top 5 slowest modules:")

    for index = 1, math.min(5, #loadTimes) do
        local info = loadTimes[index]
        print(index .. ".", info.name, string.format("(%.5f seconds)", info.time))
    end
elseif not RunService:IsStudio() then
    print(require(ReplicatedStorage.Modules.Utils).Credits)
end

workspace:SetAttribute("ServerLoaded", true)


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")

local Lighting = require(ReplicatedStorage.Modules.Lighting)
local Sounds = require(ReplicatedStorage.Modules.Sounds)
local Utils = require(ReplicatedStorage.Modules.Utils)

local MapManager = {
    --- Allows the map to not be the same to the last one.
    CanRepeatMap = true, --might want to disable on release
    CurrentMap = nil,
    LastMap = nil,
}

local Rand = Random.new()

function MapManager:Init()
    MapManager.MapPrefabs = ServerStorage.Maps:GetChildren()

    -- iterating backwards to not have to do `table.find()` every single time i want to remove a map from the table
	for i = #MapManager.MapPrefabs, 1, -1 do
		local Map = MapManager.MapPrefabs[i]
		if not Map:IsA("Folder") and not Map:IsA("Model") then
			table.remove(MapManager.MapPrefabs, i)
		end
	end

    -- this was fucking useless -dys
    -- MapManager.CurrentMap = nil
    -- MapManager.LastMap = nil
end

--- Picks a random map from `ServerStorage.Assets.Maps` and inits everything related to it, setting it as `Map` in `workspace`.
--- If `MapManager.CanRepeatMap` is true, the map will never be the same to the one chosen the round before for variety.
function MapManager.GetRandomMap(): Model | Folder
    --chooses a random map
    local SelectedMap = MapManager.MapPrefabs[Rand:NextInteger(1, #MapManager.MapPrefabs)]
    --if it's the same as the one before it chooses a different one
    if not MapManager.CanRepeatMap and MapManager.LastMap and #MapManager.MapPrefabs > 1 then
        while SelectedMap == MapManager.LastMap do
            SelectedMap = MapManager.MapPrefabs[Rand:NextInteger(1, #MapManager.MapPrefabs)]
        end
    end
    --sets the "last map" as the current map for not repeating it next time
    MapManager.LastMap = SelectedMap

    --clones the map for usage
    local MapInstance = SelectedMap:Clone()

    --creates the InGame folder for ability instances
    if not MapInstance:FindFirstChild("InGame") then
        local InGameFolder = Instance.new("Folder")
        InGameFolder.Name = "InGame"
        InGameFolder.Parent = MapInstance
    end

    --creates the Items folder for spawned `PickableItem`s
    if not MapInstance:FindFirstChild("Items") then
        local ItemsFolder = Instance.new("Folder")
        ItemsFolder.Name = "Items"
        ItemsFolder.Parent = MapInstance
    end

    --TODO: make random PickableItem spawns

    local ActualMap = MapInstance:FindFirstChild("Map")

    task.spawn(function()
        --checks if there are item spawn spots around
        local ItemSpawns = ActualMap:FindFirstChild("ItemSpawns")
        if not ItemSpawns then
            return
        end

        --every item should have its own folder with possible spawn spots
        for _, child in ItemSpawns:GetChildren() do
            if not child:IsA("Folder") then
                continue
            end

            --each folder should be named after its PickableItem
            local ItemEquivalent = ReplicatedStorage.Assets.PickableItems:FindFirstChild(child.Name)
            if not ItemEquivalent then
                return
            end
            ItemEquivalent = require(ItemEquivalent)

            for _, spawnPoint in child:GetChildren() do
                --every spawn point should be an anchored, invisible part
                if not spawnPoint:IsA("BasePart") then
                    continue
                end

                --you can add a Chance attribute to every spawn to have a 1 every X chance for the item to spawn there
                --by default it'll be a 1/10 chance
                local Chance = spawnPoint:GetAttribute("Chance") or 10
                local Result = Rand:NextInteger(1, Chance)
                if Result ~= 1 then
                    continue
                end

                --spawning the object
                local ItemInstance = Utils.Type.CopyTable(ItemEquivalent)
                ItemInstance:Init()

                --applies the exact same cframe as the spawn point
                local TargetCFrame = spawnPoint.CFrame

                --if the axis is supposed to be random then do so
                local RandomizedAxis = ItemEquivalent.RandomRotation
                if RandomizedAxis then
                    local RandomRotation = Vector3.new()
                    local AxisTable = RandomizedAxis == "All" and {"X", "Y", "Z"} or RandomizedAxis

                    for _, Axis in AxisTable do
                        RandomRotation[Axis] = Rand:NextInteger(0, 359)
                    end

                    TargetCFrame *= CFrame.fromEulerAnglesXYZ(math.rad(RandomRotation.X), math.rad(RandomRotation.Y), math.rad(RandomRotation.Z))
                end

                --if any offset is present, apply it
                if ItemEquivalent.CFrameOffset then
                    TargetCFrame *= ItemEquivalent.CFrameOffset
                end

                --place it where it's supposed to be
                ItemInstance.ModelInstance:PivotTo(TargetCFrame)
            end
        end
    end)

    --checks if there's more than 1 layout available
    local Spawnpoints = ActualMap:FindFirstChild("SpawnPoints")
    local AvailableLayouts = Spawnpoints:GetChildren()
    if #AvailableLayouts > 2 then
        --chooses a random layout from the spawnpoint pool
        local ChosenLayout = AvailableLayouts[Rand:NextInteger(1, #AvailableLayouts)]
        --destroys the rest to ensure that the chosen one is the only one available
        for _, Layout in AvailableLayouts do
            if Layout == ChosenLayout then
                continue
            end

            Layout:Destroy()
        end
    end

    --i think this is used idk lol but it's useful
    MapManager.CurrentMap = MapInstance

    --tries to find the map's config module
    local ConfigModule = MapInstance:FindFirstChild("Config")
    if ConfigModule then
        local Config = require(ConfigModule)
        --will apply the ambience music if it's there
        if Config.Ambience then
            local Props = Config.AmbienceProperties or {}
            Props.Priority = Props.Priority or 0.5
            Props.Name = "MapAmbience"
            Sounds.PlayTheme(Config.Ambience, Props)
        end
    end

    --renaming to generalize it
    MapInstance.Name = "Map"
    --creates PermAbilities folder if it isn't there
    if not MapInstance:FindFirstChild("PermAbilities") then
        local P = Instance.new("Folder")
        P.Name = "PermAbilities"
        P.Parent = MapInstance
    end
    --parented to workspace
    MapInstance.Parent = workspace
    --if it gets destroyed then the theme stops
    MapInstance.AncestryChanged:Connect(function()
        Sounds.StopTheme("MapAmbience")
    end)

    --will apply the lighting module if it's there
    local LightingModule = MapInstance:FindFirstChild("Lighting")
    if LightingModule then
        Lighting.SetCustomLighting(LightingModule)
    end

    --will execute a behaviour module if it exists for either custom map events or sfx that need to be initialized
    local BehaviourModule = MapInstance:FindFirstChild("Behaviour")
    if BehaviourModule then
        local Behaviour = require(MapInstance.Behaviour)
        if Behaviour.Init and typeof(Behaviour.Init) == "function" then
            Behaviour:Init()
        end
    end

    --returns the map instance for usage
    return MapInstance
end

function MapManager.DestroyCurrentMap()
    if MapManager.CurrentMap then
        MapManager.CurrentMap:Destroy()
        MapManager.CurrentMap = nil
        Lighting.SetDefaultLighting()
    end
end

return MapManager


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local PlayerSpeedManager = require(script.Parent.PlayerSpeedManager)
local Types = require(ReplicatedStorage.Classes.Types)
local Utils = require(ReplicatedStorage.Modules.Utils)

local FactorName = "Backpedaling"
local SpeedMult = 0.75

return {
    Init = function(_)
        Utils.Player.ObservePlayers(function(Player: Player)
            Utils.Character.ObserveCharacter(Player, function(Character: Model, CharacterJanitor: Types.Janitor)
                local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
                local HumanoidRootPart = Utils.Character.GetRootPart(Character)

                PlayerSpeedManager.AddSpeedFactor(Player, FactorName, 1)

                Utils.Instance.ObserveProperty(Humanoid, "MoveDirection", function(value: Vector3)
                    PlayerSpeedManager.ManagedPlayers[Player].Factors[FactorName] = HumanoidRootPart.CFrame:VectorToObjectSpace(value).Z > 0.1 and SpeedMult or 1
                end)
            end)
        end)
    end,
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")

local PlayerSpeedManager = require(script.Parent.PlayerSpeedManager)
local Utils = require(ReplicatedStorage.Modules.Utils)

return {
    Init = function(_)
        Utils.Player.ObservePlayers(function(Player: Player)
            Utils.Character.ObserveCharacter(Player, function(Character: Model)
                local Role = Character:FindFirstChild("Role")
                if Role.Value ~= "Survivor" then
                    return
                end

                PlayerSpeedManager.AddSpeedFactor(Player, "FallSlowness", 1)

                local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
                local HRP = Utils.Character.GetRootPart(Character)
                local thread
                Humanoid.StateChanged:Connect(function(_old: Enum.HumanoidStateType, new: Enum.HumanoidStateType)
                    if new == Enum.HumanoidStateType.Landed then
                        if thread then
                            return
                        end
                        local Vel = math.abs(HRP.AssemblyLinearVelocity.Y)
                        local Mult = 40 / Vel / 1.5
                        local Recovery = Vel / 40 * 0.25
                        PlayerSpeedManager.ManagedPlayers[Player].Factors.FallSlowness = Mult
                        thread = task.defer(function()
                            local elapsed = 0
                            local alpha = 0
                            repeat
                                alpha = math.clamp(elapsed / Recovery, 0, 1)
                                PlayerSpeedManager.ManagedPlayers[Player].Factors.FallSlowness = math.lerp(Mult, 1, TweenService:GetValue(alpha, Enum.EasingStyle.Circular, Enum.EasingDirection.In))
                                elapsed += task.wait()
                            until alpha == 1 or not PlayerSpeedManager.ManagedPlayers[Player].Factors.FallSlowness
                        end)
                    end
                end)
            end)
        end)
    end,
}


local BadgeService = game:GetService("BadgeService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Utils = require(ReplicatedStorage.Modules.Utils)

type BadgeID = number
type BadgeItemList = {
    [BadgeID]: {
        {
            Type: "Character" | "Skin" | "Emote",
            Item: {
                Role: ("Killer" | "Survivor")?,
                Name: string,
                RootCharacterName: string?,
            },
        }
    }
}

--[=[
    ADD YOUR BADGE ITEMS HERE
]=]--
local BadgeItemGrantingManager = {}
BadgeItemGrantingManager.BadgesWithItems = {
    -- Here's an example of how a badge in the list could look like:
    -- [0000000000] = {
    --     {
    --         Type = "Character",
    --         Item = {
    --             Role = "Killer",
    --             Name = "NullexVoyd",
    --         },
    --     },
    -- },
} :: BadgeItemList

function BadgeItemGrantingManager:Init()
end

function BadgeItemGrantingManager.CheckPlayer(Player: Player, PlayerData: Folder)
    local PurchasedFolder = PlayerData:FindFirstChild("Purchased")

    local BadgesToCheck = {}
    for ID, _ in BadgeItemGrantingManager.BadgesWithItems do
        table.insert(BadgesToCheck, ID)
    end

    for _, Badges in Utils.Type.SplitTableIntoChunks(BadgesToCheck, 10) do
        local success, ownedBadgeIDs = pcall(function()
            return BadgeService:CheckUserBadgesAsync(Player.UserId, Badges)
        end)

        if not success then
            continue
        end

        for _, ownedID in ownedBadgeIDs do
            local ItemTable = BadgeItemGrantingManager.BadgesWithItems[ownedID]
            if ItemTable.Type == "Character" then
                if not Utils.Instance.GetCharacterModule(ItemTable.Item.Role, ItemTable.Item.Name) then
                    continue
                end

                local Purchased = PurchasedFolder:FindFirstChild(ItemTable.Item.Role.."s")
                if Purchased:FindFirstChild(ItemTable.Item.Name) then
                    continue
                end

                local CharValue = Instance.new("NumberValue")
                CharValue.Name = ItemTable.Item.Name
                CharValue.Value = 0
                CharValue:AddTag("PreventSave")
                CharValue.Parent = Purchased
            elseif ItemTable.Type == "Skin" then
                if not Utils.Instance.GetCharacterModule(ItemTable.Item.Role, ItemTable.Item.RootCharacterName, ItemTable.Item.Name) then
                    continue
                end

                local Purchased = Utils.Instance.FindFirstChild(PurchasedFolder, "Skins."..ItemTable.Item.RootCharacterName, 0) or Utils.PlayerData.CreateMissingPurchasedSkinValue(Player, ItemTable.Item.RootCharacterName)
                if Purchased:FindFirstChild(ItemTable.Item.Name) then
                    continue
                end

                local SkinValue = Instance.new("NumberValue")
                SkinValue.Name = ItemTable.Item.Name
                SkinValue.Value = 0
                SkinValue:AddTag("PreventSave")
                SkinValue.Parent = Purchased
            elseif ItemTable.Type == "Emote" then
                if not Utils.Instance.GetEmoteModule(ItemTable.Item.Name) then
                    continue
                end

                local Purchased = PurchasedFolder:FindFirstChild("Emotes")
                if Purchased:FindFirstChild(ItemTable.Item.Name) then
                    continue
                end

                local EmoteValue = Instance.new("NumberValue")
                EmoteValue.Name = ItemTable.Item.Name
                EmoteValue.Value = 0
                EmoteValue:AddTag("PreventSave")
                EmoteValue.Parent = Purchased
            end
        end

        task.wait(0.1)
    end
end

return BadgeItemGrantingManager


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Utils = require(ReplicatedStorage.Modules.Utils)
type DevID = number
type DevItemList = {
    [DevID]: {
        {
            Type: "Character" | "Skin" | "Emote",
            Item: {
                Role: ("Killer" | "Survivor")?,
                Name: string,
                RootCharacterName: string?,
            },
        }
    }
}

--[=[
    ADD YOUR DEV ITEMS HERE
]=]--
local DevItemGrantingManager = {}
DevItemGrantingManager.UsersWithDevItems = {
    -- Here's an example of how a dev in the list could look like:
    -- [432073982] = { --Dyscarn
    --     {
    --         Type = "Character",
    --         Item = {
    --             Role = "Killer",
    --             Name = "NullexVoyd",
    --         },
    --     },
    -- },
} :: DevItemList

function DevItemGrantingManager:Init()
end

function DevItemGrantingManager.CheckPlayer(Player: Player, PlayerData: Folder)
    if not DevItemGrantingManager.UsersWithDevItems[Player.UserId] then
        return
    end

    local PurchasedFolder = PlayerData:FindFirstChild("Purchased")

    for _, ItemTable in DevItemGrantingManager.UsersWithDevItems[Player.UserId] do
        if ItemTable.Type == "Character" then
            if not Utils.Instance.GetCharacterModule(ItemTable.Item.Role, ItemTable.Item.Name) then
                continue
            end

            local Purchased = PurchasedFolder:FindFirstChild(ItemTable.Item.Role.."s")
            if Purchased:FindFirstChild(ItemTable.Item.Name) then
                continue
            end

            local CharValue = Instance.new("NumberValue")
            CharValue.Name = ItemTable.Item.Name
            CharValue.Value = 0
            CharValue:AddTag("PreventSave")
            CharValue.Parent = Purchased
        elseif ItemTable.Type == "Skin" then
            if not Utils.Instance.GetCharacterModule(ItemTable.Item.Role, ItemTable.Item.RootCharacterName, ItemTable.Item.Name) then
                continue
            end

            local Purchased = Utils.Instance.FindFirstChild(PurchasedFolder, "Skins."..ItemTable.Item.RootCharacterName, 0) or Utils.PlayerData.CreateMissingPurchasedSkinValue(Player, ItemTable.Item.RootCharacterName)
            if Purchased:FindFirstChild(ItemTable.Item.Name) then
                continue
            end

            local SkinValue = Instance.new("NumberValue")
            SkinValue.Name = ItemTable.Item.Name
            SkinValue.Value = 0
            SkinValue:AddTag("PreventSave")
            SkinValue.Parent = Purchased
        elseif ItemTable.Type == "Emote" then
            if not Utils.Instance.GetEmoteModule(ItemTable.Item.Name) then
                continue
            end

            local Purchased = PurchasedFolder:FindFirstChild("Emotes")
            if Purchased:FindFirstChild(ItemTable.Item.Name) then
                continue
            end

            local EmoteValue = Instance.new("NumberValue")
            EmoteValue.Name = ItemTable.Item.Name
            EmoteValue.Value = 0
            EmoteValue:AddTag("PreventSave")
            EmoteValue.Parent = Purchased
        end
    end
end

return DevItemGrantingManager


local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local BadgeItemGrantingManager = require(script.BadgeItemGrantingManager)
local DevItemGrantingManager = require(script.DevItemGrantingManager)
local Achievements = require(ReplicatedStorage.Assets.Achievements)
local Utils = require(ReplicatedStorage.Modules.Utils)

-- The module has two tables inherently:
--	PlayerDatas holds the unmodified tables that are grabbed from the datastore and turned into a tree of valueobjects.
-- 	PlayerLeaderstatConns holds references to the event listeners watching over certain stats that are tracked by the leaderboard, so they can be disconnected when the player leaves, to avoid a memory leak.
local SaveManager = {
	PlayerDatas = {},
	PlayerLeaderstatConns = {},
}

local Storage
local Attempt = 0

-- An attempt to fix constantly having to go back to this script and re-set your datastore name and key whenever the SaveManager updates.
-- These objects are parented to the SaveManager script. Place your datastore name and key in them, and it should never require you to go back and set them again, since those objects will not be updated as frequently as this script here.
-- If Rojo is being used, the values for these objects MUST be changed by going into their .model.json files adjacent to this script.
-- If not, then they can just be changed through Studio in this script's children.
local DataStoreHolder = script:FindFirstChild("DataStoreRef")
local DataStoreKeyHolder = script:FindFirstChild("DataStoreKey")

local DataStoreRef = DataStoreHolder.Value
if not DataStoreRef or DataStoreRef == "DATA_STORE_NAME_HERE" then
	warn("\n[SaveManager] – WARNING: Datastore name not set!"
		.."\nPlace the name of your datastore in ServerScriptService/Managers/SaveManager/DataStoreRef in Roblox Studio, or in src/ServerScriptService/Managers/SaveManager/DataStoreRef.model.json in Visual Studio Code."
		.."\nYou should only need to do this once – if this happens frequently, contact a Dysymmetrical developer.")
	DataStoreRef = "Dysymmetrical" -- Dysymmetrical default datastore for testing purposes.
end

local DataStoreKey = DataStoreKeyHolder.Value
if not DataStoreKey or DataStoreKey == "DATA_STORE_KEY_HERE" then
	warn("\n[SaveManager] – WARNING: Key for datastore '"..DataStoreRef.."' not set!"
		.."\nMake sure you have the key of your datastore in ServerScriptService/Managers/SaveManager/DataStoreKey in Roblox Studio, or in src/ServerScriptService/Managers/SaveManager/DataStoreKey.model.json in Visual Studio Code."
		.."\nHopefully, you should only need to do this once – if this message appears frequently, contact a Dysymmetrical developer.")
	DataStoreKey = "dysymmetrical" -- Dysymmetrical default datastore for testing purposes.
end

-- Will retry retrieving data again and again until it finally succeeds. Datastores are too unreliable to be one-and-done, and may otherwise corrupt data.
-- We don't want to defer this– if files are attempted to be grabbed from the datastore before the datastore loads... That'll be, ah, not good.
repeat
	Attempt += 1

	_, Storage = pcall(function()
		return DataStoreService:GetDataStore(DataStoreRef)
	end)
until Storage or Attempt > 50

--[[
	Function for converting a ValueObject tree into a table.

	Used to turn the valueobjects used to store playerdata into a format we can save to the datastore.

	Eventually, this will probably be made two-way, and more functionality may be added to it.
]]--
local function ConvertInstanceTreeToTable(TargetInstance: Instance, Depth: number?): ({[string]: {any}}) -- Can return a table of string-value pairs which can be arbitrarily deep, dont quite know how to define that for the typechecker, so just do your best (as always!).

	local tree = {}

	-- A cap for maximum allowed depth– leaving it arbitrary feels like a recipe for disaster
	Depth = (Depth or -1) + 1

	-- Iterate through all of the instance's children
	for _index, value in ipairs(TargetInstance:GetChildren()) do

		-- If the child is a folder, run this function again on it to create another nest.
		if value:IsA("Folder") and Depth < 50 then -- Customize the depth cap as you please. Realistically, nothing will ever go this deep, but to avoid a permanant hang or an infinite loop, you can never be too sure...

			tree[value.Name] = ConvertInstanceTreeToTable(value, Depth)

			-- If the child is a valueobject, grab its value and add it to the table.
		elseif value:IsA("ValueBase") then
			tree[value.Name] = value.Value
		end

	end

	-- Return the completed table to whatever called the function.
	return tree
end

--- Sets up save data handlers, listens for players joining and leaving to read, write, retrieve, and store save data.
function SaveManager:Init()

	-- Validate the default values for playerdata, and make sure they point to killers/survivors that exist. If killers/survivors are moved and this isn't updated, new players could otherwise get an invalid killer/survivor, and become softlocked.
	-- We *want* the game to hang for a bit to check these, since new players wont be able to join the game if it's malformed.

	-- First, check to make sure the valueobjects which these would be stored in exist.

	local Playerdata = script:FindFirstChild("PlayerData")

	-- Quickly flip through and make sure the valueobjects we need exist. Eventually may create them on its own, to make things easier.
	if not (
			Playerdata
			and Playerdata:FindFirstChild("Equipped")
			and Playerdata.Equipped:FindFirstChild("Killer")
			and Playerdata.Equipped:FindFirstChild("Survivor")
			and Playerdata:FindFirstChild("Purchased") and
			Playerdata.Purchased:FindFirstChild("Killers")
			and Playerdata.Purchased:FindFirstChild("Skins")
			and Playerdata.Purchased:FindFirstChild("Survivors")
			and Playerdata:FindFirstChild("Achievements")
		) then

		error("[SaveManager:Init()] – ERROR: Default playerdata may be missing important pieces, or entirely absent! Check for a folder in ServerScriptService/Managers/SaveManager/PlayerData. If you know what you're doing, you can silence this message in ServerScriptService/Managers/SaveManager.")
	end

	for AchievementGroup, _ in Achievements do
		local GroupFolder = Instance.new("Folder")
		GroupFolder.Name = AchievementGroup
		GroupFolder.Parent = Playerdata.Achievements
	end

	local EquippedFolder = Playerdata.Equipped
	local PurchasedFolder = Playerdata.Purchased

	-- Check if the listed default killer still exists
	local DefaultKiller = EquippedFolder.Killer.Value
	local KillerModule = Utils.Instance.GetCharacterModule("Killer", DefaultKiller, nil, false)

	-- If the default killer points to a killer who doesn't exist anymore, grab the first one the game can find and use that as the default instead.
	if not KillerModule then

		local AllKillers = ReplicatedStorage:FindFirstChild("Characters"):FindFirstChild("Killers"):GetChildren()

		-- Set the default equipped killer to the first killer found in this list as a temporary, non-error-prone solution. This can be changed by the user to another killer, and the game wont try to do this again unless the killer this value points to doesn't exist again.
		EquippedFolder.Killer.Value = AllKillers[1].Name

		-- "This could technically error if no killers are found, but if such is the case, I think the developer has bigger errors to worry about, so I think its fine to leave it as-is" – Itred

		warn("[SaveManager:Init()] – INFO: Default (template) playerdata points to a killer which no longer exists. Substituting with '"..AllKillers[1].Name.."' for now. \nModify the value in 'ServerScriptService/Managers/SaveManager/PlayerData/Equipped/Killer' if you wish to set a different default killer.")

	end

	-- Check if the listed default survivors still exists
	local DefaultSurvivor = EquippedFolder.Survivor.Value
	local SurvivorModule = Utils.Instance.GetCharacterModule("Survivor", DefaultSurvivor, nil, false)

	-- Same deal for survivors– If the default survivor points to one whom doesn't exist anymore, grab the first one the game can find and use that as the default instead.
	if not SurvivorModule then

		local AllSurvivors = ReplicatedStorage:FindFirstChild("Characters"):FindFirstChild("Survivors"):GetChildren()

		-- Similarly, set the default equipped survivor to the first survivor found in this list as a temporary, non-error-prone solution. This can be changed by the user to another survivor, and the game wont try to do this again unless the survivor this value points to doesn't exist again.
		EquippedFolder.Survivor.Value = AllSurvivors[1].Name

		-- "This, too, could technically error if no survivors are found, but if such is the case, what are you even doing." – Itred

		warn("[SaveManager:Init()] – INFO: Default (template) playerdata points to a survivor which no longer exists. Substituting with '"..AllSurvivors[1].Name.."' for now. \nModify the value in 'ServerScriptService/Managers/SaveManager/PlayerData/Equipped/Survivor' if you wish to set a different default survivor.")

	end

	-- Also check for the default purchased killer/survivor. Note, however, that this will only apply to the template data, so bought killers by players that are to be replaced later will also be unaffected.

	-- Default purchased killers:
	for index, purchasedkiller in PurchasedFolder.Killers:GetChildren() do

		-- Find its module.
		local PurchasedKillerModule = Utils.Instance.GetCharacterModule("Killer", purchasedkiller.Name, nil, false)

		-- If it doesn't exist, set its name to the default killer instead after just one more check.
		if not PurchasedKillerModule then

			-- Check to see if theres already a valueobject tied to the default killer. If there isn't, then assume that the sole default purchased killer should match the default killer.
			if not PurchasedFolder.Killers:FindFirstChild(DefaultKiller) then

				purchasedkiller.Name = EquippedFolder.Killer.Value -- Re-read from the newly-updated killer value
				warn("[SaveManager:Init()] – INFO: Default (template) purchased killer doesn't exist. Modifying to match default equipped killer... Change this in 'ServerScriptService/Managers/SaveManager/PlayerData/Purchased/Killer' if you wish to set your own default purchased killer.")

			else

				warn("[SaveManager:Init()] – WARNING: More than one default purchased killer pointed to a character which didn't exist! For stability reasons, these will be temporarily destroyed. Check 'ServerScriptService/Managers/SaveManager/PlayerData/Purchased/Killer' to diagonose any issues.")
				purchasedkiller:Destroy()

			end
		end
	end

	-- Default purchased survivors:
	for index, purchasedsurvivor in PurchasedFolder.Survivors:GetChildren() do

		-- Find its module.
		local PurchasedSurvivorModule = Utils.Instance.GetCharacterModule("Survivor", purchasedsurvivor.Name, nil, false)

		-- If it doesn't exist, set its name to the default survivor instead after just one more check.
		if not PurchasedSurvivorModule then

			-- Same deal as with the killer– Check to see if theres already a valueobject tied to the default survivor. If there isn't, then assume that the sole default purchased survivor should match the default survivor.
			if not PurchasedFolder.Survivors:FindFirstChild(DefaultSurvivor) then

				purchasedsurvivor.Name = EquippedFolder.Survivor.Value -- Re-read from the newly-updated survivor value
				warn("[SaveManager:Init()] – INFO: Default purchased killer doesn't exist. Modifying to match default equipped killer... Change this in 'ServerScriptService/Managers/SaveManager/PlayerData/Purchased/Killer' if you wish to set your own default purchased killer.")

			else

				warn("[SaveManager:Init()] – WARNING: More than one default purchased killer pointed to a character which didn't exist! For stability reasons, these will be temporarily destroyed. Check 'ServerScriptService/Managers/SaveManager/PlayerData/Purchased/Killer' to diagonose any issues.")
				purchasedsurvivor:Destroy()

			end
		end
	end

	-- Watches for players joining the game through a custom version of Players.PlayerAdded. See in Utils.Player for more information.
	Utils.Player.ObservePlayers(function(Player: Player)

		-- Go through players in parallel, rather than one-by-one.
		task.defer(function()

			-- Grab the player's data from the datastore, place it into a table for later use
			SaveManager.PlayerDatas[Player.UserId] = SaveManager.Load(Player)
			local ThisPlayerData = SaveManager.PlayerDatas[Player.UserId] -- Gets its own variable to make things easier to read. Done in this way so this variable points to the entry in the table and not to the data itself.

			-- Go through all achievements, set them up in folders for the player in question.
			for AchievementGroupCodeName, AchievementGroup in Achievements do

				local Group = ThisPlayerData.Achievements:FindFirstChild(AchievementGroupCodeName)

				-- If the specific achievement group doesnt exist, make it.
				if not Group then

					Group = Instance.new("Folder")
					Group.Name = AchievementGroupCodeName
					Group.Parent = ThisPlayerData.Achievements

				end

				-- Go by achievement group, check for achievement-tracking valueobjects.
				for AchievementCodeName, Achievement in AchievementGroup.Achievements do

					if Group:FindFirstChild(AchievementCodeName) then
						continue
					end

					-- If the valueobject for tracking this achievement's progress doesn't exist, make it.
					local AchievementProgress

					if Achievement.Requirement then
						AchievementProgress = Instance.new("NumberValue")
						AchievementProgress.Value = 0
					else
						AchievementProgress = Instance.new("BoolValue")
						AchievementProgress.Value = false
					end

					AchievementProgress.Name = AchievementCodeName
					AchievementProgress.Parent = Group

				end

			end

		end)

	end)

	-- Save a player's data when they leave the game.
	Players.PlayerRemoving:Connect(function(Player: Player)
		-- Deferred so it'll save playerdata parallel, instead of doing so one-at-a-time.
		task.defer(function()

			if SaveManager.PlayerLeaderstatConns[Player.UserId] then
				SaveManager.PlayerLeaderstatConns[Player.UserId]:Disconnect()
			end

		end)

		SaveManager.Save(Player, SaveManager.PlayerDatas[Player.UserId])
	end)

	-- Mark some global saving code to be run if the server gets manually closed for one reason or another.
	game:BindToClose(function()
		for _, Player in Players:GetPlayers() do

			SaveManager.Save(Player, SaveManager.PlayerDatas[Player.UserId])

		end
	end)

end

--- Sets up folder for any data that has the `DisplayInLeaderstats` attribute, to display them on the leaderboard.
function SaveManager._SetupLeaderStats(DataFolder, parent) -- Uses its own function, so it doesn't save.

	for _, data in DataFolder:GetDescendants() do

		if data:IsA("ValueBase") and data:GetAttribute("DisplayInLeaderstats") == true then

			local LeaderStat = Instance.new("IntValue")
			LeaderStat.Name = data.Name
			LeaderStat.Value = data.Value
			LeaderStat.Parent = parent

			SaveManager.PlayerLeaderstatConns[DataFolder.Parent.UserId] = data.Changed:Connect(function()
				if LeaderStat then
					LeaderStat.Value = data.Value
				end
			end)
		end

	end
end

--- Loads a player's savedata and generates a heirarchy of folders and valuebase objects for storing, reading, and writing to and from it in-game.
---
--- ### DEFER ANY CALLS TO THIS FUNCTION, IT MAY TAKE A WHILE.
function SaveManager.Load(Player: Player): Folder

	-- Retrieve playerdata
	local PlayerData
	if Storage then
		-- Try retrieving data again and again until something is sent back, as datastores arent always reliable.

		local Attempt = 0
		repeat
			Attempt += 1

			pcall(function()
				PlayerData = Storage:GetAsync(`{DataStoreKey}_{Player.UserId}`)
			end)

			task.wait()
		-- do few attempts just in case it's a new player
		until PlayerData or Attempt > 10

	else
		warn("[SaveManager.Load()]: FATAL ERROR – ENTIRE DATASTORE failed to load. Handling playerdata from here is extremely hazardous. Here be dragons!")
	end

	local ClonedData = script.PlayerData:Clone()

	local Leaderstats = Instance.new("Folder")
	Leaderstats.Name = "leaderstats"

	-- If we're able to properly retrieve the data, parse through it and make valueobjects for expressing it in-game.
	if PlayerData then

		if not PlayerData.Achievements then
			DevItemGrantingManager.CheckPlayer(Player, ClonedData)
			BadgeItemGrantingManager.CheckPlayer(Player, ClonedData)

			ClonedData.Parent = Player
			SaveManager._SetupLeaderStats(ClonedData, Leaderstats)
			Leaderstats.Parent = Player

			return ClonedData

		end

		-- Iterate through achievementgroups and achievements
		for index, achievementgroup in PlayerData.Achievements do
			for subindex, achievement in achievementgroup do
				if not ClonedData.Achievements:FindFirstChild(subindex, true) then
					continue
				end
				ClonedData.Achievements:FindFirstChild(subindex, true).Value = achievement
			end
		end

		-- Iterate through equipped emotes
		for emotename, emote in PlayerData.Equipped.Emotes do
			if Utils.Instance.GetEmoteModule(emote) == nil then
				continue
			end

			local Emote = Instance.new("StringValue")
			Emote.Name = emotename
			Emote.Value = emote
			Emote.Parent = ClonedData.Equipped.Emotes
		end

		-- Iterate through equipped skins
		for skinname, skin in PlayerData.Equipped.Skins do
			if
				Utils.Instance.GetCharacterModule("Killer", skinname, skin, false) == nil
				and Utils.Instance.GetCharacterModule("Survivor", skinname, skin, false) == nil
			then
				continue
			end

			local Value = Instance.new("StringValue")
			Value.Name = skinname
			Value.Value = skin
			Value.Parent = ClonedData.Equipped.Skins
		end

		-- Find their equipped killer and survivor
		ClonedData.Equipped.Killer.Value = Utils.Instance.GetCharacterModule("Killer", PlayerData.Equipped.Killer, nil, false) ~= nil and PlayerData.Equipped.Killer or ""
		ClonedData.Equipped.Survivor.Value = Utils.Instance.GetCharacterModule("Survivor", PlayerData.Equipped.Survivor, nil, false) ~= nil and PlayerData.Equipped.Survivor or ""

		-- Not sure what this does, likely to do with the update log appearing.
		ClonedData.Misc.LastSeenLog.Value = PlayerData.Misc.LastSeenLog

		-- Finally, make valueobjects for all purchased emotes,
		for emotename, emotedata in PlayerData.Purchased.Emotes do

			-- If the emote doesn't exist, skip it for now. It'll likely cleaned up later.
			if not Utils.Instance.GetEmoteModule(emotename) then
				continue
			end

			local int = Instance.new("IntValue")
			int.Parent = ClonedData.Purchased.Emotes
			int.Name = emotename
		end

		-- killers,
		for killer, level in PlayerData.Purchased.Killers do

			-- If the killer doesn't exist, skip it for now. It'll also likely be cleaned up later.
			if Utils.Instance.GetCharacterModule("Killer", killer, nil, false) == nil then
				continue
			end

			local int = Instance.new("IntValue")
			int.Parent = ClonedData.Purchased.Killers
			-- If theres a stored level for the killer, set the object to that value. Otherwise, keep it at 0.
			if level then
				int.Value = level
			end
			int.Name = killer

		end

		-- survivors,
		for survivor, level in PlayerData.Purchased.Survivors do

			-- If the survivor doesn't exist, skip it as well. It'll likely be cleaned up later, too.
			if Utils.Instance.GetCharacterModule("Survivor", survivor, nil, false) == nil then
				continue
			end

			local int = Instance.new("IntValue")
			int.Parent = ClonedData.Purchased.Survivors
			-- If theres a stored level for the survivor, set the object to that value. Otherwise, keep it at 0.
			if level then
				int.Value = level
			end
			int.Name = survivor
		end

		-- skins,
		for rootname, skindata in PlayerData.Purchased.Skins do

			-- Check whether its a survivor or killer skin
			local IsSurvivor = Utils.Instance.GetCharacterModule("Survivor", rootname, nil, false) ~= nil

			if not IsSurvivor then
				if Utils.Instance.GetCharacterModule("Killer", rootname, nil, false) == nil then
					continue
				end
			end

			local Folder = Instance.new("Folder")
			Folder.Name = rootname
			Folder.Parent = ClonedData.Purchased.Skins

			for skinname, value in skindata do

				if Utils.Instance.GetCharacterModule(if IsSurvivor then "Survivor" else "Killer", rootname, skinname, false) == nil then
					continue
				end

				local int = Instance.new("IntValue")
				int.Parent = Folder
				int.Name = skinname
				int.Value = value
			end

		end

		-- applied settings,
		for _index, setting in PlayerData.Settings do

			for settingname, settingdata in setting do
				if not ClonedData.Settings:FindFirstChild(settingname, true) then
					continue --break
				end
				ClonedData.Settings:FindFirstChild(settingname, true).Value = settingdata
			end

		end

		-- and tracked stats.
		for _index, stat in PlayerData.Stats do

			for statname, statvalue in stat do
				if not ClonedData.Stats:FindFirstChild(statname, true) then
					continue --break
				end
				ClonedData.Stats:FindFirstChild(statname, true).Value = statvalue
			end

		end

	end
	-- SaveManager._CheckCheckSaveIntegrity(ClonedData)

	DevItemGrantingManager.CheckPlayer(Player, ClonedData)
	BadgeItemGrantingManager.CheckPlayer(Player, ClonedData)

	-- Finally, nest all these instances neatly in the player for easy reference.
	ClonedData.Parent = Player
	SaveManager._SetupLeaderStats(ClonedData, Leaderstats)
	Leaderstats.Parent = Player

	return ClonedData
end

--okay so somehow deferring it fucks up the whole thing -dys

--- Saves a given player's data.
---
--- ### SPAWN ANY CALLS TO THIS FUNCTION, IT MAY TAKE A WHILE.
function SaveManager.Save(Player: Player, Data: Folder)
	task.spawn(function()
		local SaveFile = ConvertInstanceTreeToTable(Data)

		--i just figured out pairs and ipairs are fucking useless as generalized iteration had been implemented fairly long ago.
		--i want to die. -dys

		-- "Oh, thats good to know, actually." – Itred

		-- fun fact: ipairs is still useful because of it being done in order for numeric tables! yippee!!!
		-- seriorusly though, remove pairs. -dys

		-- Grab the currently equipped killer and survivor.
		for target, equipped in { Killer = SaveFile.Equipped.Killer, Survivor = SaveFile.Equipped.Survivor } do

			local Module = Utils.Instance.GetCharacterModule(target, equipped, nil, false)

			if not Module or Module:HasTag("PreventSave") then
				SaveFile["Equipped"][target] = script:FindFirstChild("Equipped"):FindFirstChild(target).Value --if it's smth like a dev item it doesn't save in publics
			end

		end

		-- Give a readout if needed.
		if workspace:GetAttribute("DebugAllowed") == true then
			print("Saving data for: " .. Player.Name, SaveFile)
		end

		-- Give storing data a few tries until it goes through, as datastores are too unreliable to be one-and-done.
		local success
		local Attempt = 0
		repeat
			Attempt += 1

			success, _ = pcall(function()
				Storage:UpdateAsync(`{DataStoreKey}_{Player.UserId}`, function(_old)
					return SaveFile
				end)
			end)

		until success or Attempt > 99

		-- Some kind of handling for if the player's data cant be saved would be epic, to prevent total corruption of savedata. Attempt cap is high to try and mitigate this, but it'd still be good practice.
		-- i believe that if it fails, the previous data remains so it doesn't get corrupted; i'll be unmarking this as a todo but correct me if i'm wrong
	end)
end

return SaveManager


--- Class used to manage the game's time in its current state, let it be Lobby or Round or any custom state you implement.
local TimeManager = {
    CurrentTime = 0,
    Config = {
        LobbyTime = 45, --0:45 / 45
        RoundTime = 240, --4:00 / 240
        LastManStandingTime = 90, --1:30 / 90

        --- If true, `TimeManager` will wait until `TimeManager.Config.MinimumPlayerAmount` players have loaded their game to tick down.
        WaitForEnoughPlayers = true,
        MinimumPlayerAmount = 2, --change this to 1 to test rounds alone instead of using `WaitForEnoughPlayers`
    },
    --- Function that'll execute whenever the timer reaches 0.
    OnTimerEnd = function() end, --func
    --differentiate these two
    CanDecreaseTime = true, --used internally
    FreezeTime = false, --used with the workspace attribute
}
--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Utils = require(ReplicatedStorage.Modules.Utils)
--
local TimeValue: NumberValue = ReplicatedStorage.RoundInfo.TimeRemaining
local State: StringValue = ReplicatedStorage.RoundInfo.CurrentState
local EnoughPlayers: BoolValue = ReplicatedStorage.RoundInfo.EnoughPlayers
--
function TimeManager:Init()
    --print("FINISH TIMEMANAGER LATER >:(") --gotta save this line for memories lol

    TimeManager.FreezeTime = workspace:GetAttribute("FreezeTime")
    Utils.Instance.ObserveAttribute(workspace, "FreezeTime", function(value: boolean)
        TimeManager.FreezeTime = value
    end)

    RunService.Heartbeat:Connect(function(delta: number)
        if TimeManager.Config.WaitForEnoughPlayers and State.Value == "Lobby" and not TimeManager._EnoughPlayers() then
            EnoughPlayers.Value = false
            if TimeManager.CurrentTime ~= TimeManager.Config.LobbyTime then
                TimeManager.SetTime(TimeManager.Config.LobbyTime)
            end
            return
        end

        EnoughPlayers.Value = true
        if TimeManager.CanDecreaseTime and not TimeManager.FreezeTime then
            TimeManager.SetTime(TimeManager.CurrentTime - delta)
            if TimeManager.CurrentTime <= 0 then
                TimeManager.CanDecreaseTime = false
                TimeManager.SetTime(0)
                TimeManager.OnTimerEnd()
            end
        end
    end)

    TimeManager.SetTime(TimeManager.Config.LobbyTime) --starting with lobby since `Init()` is called when the server initializes
end

function TimeManager._EnoughPlayers()
    return #Utils.Player.GetLoadedPlayers(false) >= TimeManager.Config.MinimumPlayerAmount
end

--- You can use this instead of changing the value directly. It works better.
function TimeManager.SetTime(value: number)
    TimeManager.CurrentTime = value
    TimeValue.Value = value
end

return TimeManager


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Utils = require(ReplicatedStorage.Modules.Utils)
local Network = require(ReplicatedStorage.Modules.Network)

return function()
    Network:SetConnection("BuyItem", "REMOTE_FUNCTION", function(SourcePlayer: Player, Module: ModuleScript, Quotation: boolean?)
        if Quotation == nil then
            Quotation = true
        end

        local ItemType: string = Module.Parent
        if ItemType.Parent.Parent.Name == "Skins" then
            ItemType = ItemType.Parent.Parent
        end
        ItemType = ItemType.Name
        local Money = Utils.Instance.FindFirstChild(SourcePlayer, "PlayerData.Stats.Currency.Money")
        local NetWorth = Utils.Instance.FindFirstChild(SourcePlayer, "PlayerData.Stats.Currency.NetWorth")
        local Price = require(Module).Config.Price

        if Price > Money.Value then
            return false
        else
            Money.Value -= Price
            NetWorth.Value += Price

            local Product = Instance.new("IntValue")
            Product.Name = Module.Name
            Product.Value = 0

            local Parent = SourcePlayer.PlayerData.Purchased[ItemType]
            if ItemType == "Skins" then
                Parent = Parent:FindFirstChild(Module.Parent.Name) or Utils.PlayerData.CreateMissingPurchasedSkinValue(SourcePlayer, Module.Parent.Name)
            end

            Product.Parent = Parent

            Network:FireClientConnection(SourcePlayer, "ShowRewardNotification", "REMOTE_EVENT", Module, "Quoted"..ItemType:sub(1, -2))

            return true
        end
    end)
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Network = require(ReplicatedStorage.Modules.Network)
local Utils = require(ReplicatedStorage.Modules.Utils)

return function()
    Network:SetConnection("DropItem", "REMOTE_EVENT", function(Player: Player, Tool: Tool)
        if not Player.Character or not Tool or (Tool.Parent ~= Player.Character and Tool.Parent ~= Player.Backpack) then
            return
        end

        local PickableItemEquivalent = ReplicatedStorage.Assets.PickableItems:FindFirstChild(Tool.Name)
        if not PickableItemEquivalent then
            return
        end

        Tool:Destroy()

        PickableItemEquivalent = Utils.Type.CopyTable(require(PickableItemEquivalent))
        PickableItemEquivalent:Init()

        local HRP = Utils.Character.GetRootPart(Player.Character)
        PickableItemEquivalent.ModelInstance:PivotTo(HRP.CFrame * CFrame.new(0, -3, 0))
    end)
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Utils = require(ReplicatedStorage.Modules.Utils)
local Network = require(ReplicatedStorage.Modules.Network)

return function()
    Network:SetConnection("EquipEmote", "REMOTE_FUNCTION", function(Player: Player, Slot: number, Value: string)
        if #Value <= 0 then
            Utils.PlayerData.GetPlayerEquipped(Player, "Emotes.Emote"..tostring(Slot), false).Value = ""
            return
        end

        if Utils.PlayerData.GetPlayerOwned(Player, "Emotes."..Value) then
            Utils.PlayerData.GetPlayerEquipped(Player, "Emotes.Emote"..tostring(Slot), false).Value = Value
        end
    end)
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Utils = require(ReplicatedStorage.Modules.Utils)
local Network = require(ReplicatedStorage.Modules.Network)

return function()
    Network:SetConnection("EquipItem", "REMOTE_EVENT", function(plr: Player, name: string, Type: string, SkinPath: string?)
        if Type == "Skin" and SkinPath then
            Utils.PlayerData.GetPlayerEquipped(plr, SkinPath, false).Value = name
            return
        end

        if not Utils.PlayerData.GetPlayerOwned(plr, Type.."s."..name, false) then
            return
        end

        Utils.PlayerData.GetPlayerEquipped(plr, Type, false).Value = name
    end)
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Network = require(ReplicatedStorage.Modules.Network)

local EmoteFolder = ReplicatedStorage.Assets.Emotes

return function()
    Network:SetConnection("InitEmote", "REMOTE_EVENT", function(plr: Player, emoteName: string)
        if not plr.Character or not plr.Character:FindFirstChild("Humanoid") or not plr.Character.Humanoid:FindFirstChild("Animator") then
            return
        end

        if EmoteFolder:FindFirstChild(emoteName) then
            require(EmoteFolder[emoteName]):Init(plr)
        end
    end)
end


local LogService = game:GetService("LogService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local Network = require(ReplicatedStorage.Modules.Network)

return function()
    LogService.MessageOut:Connect(function(message, messageType)
        if workspace:GetAttribute("DebugAllowed") ~= true or RunService:IsStudio() then
            return
        end
        if messageType == Enum.MessageType.MessageError or messageType == Enum.MessageType.MessageWarning then
            Network:FireAllClientConnection("Print", "UREMOTE_EVENT", messageType, message)
        end
    end)
end


--- Make a script in here (use another one as an example) to handle an independant Remote instance.

return {
	Init = function(_self)
		for _, func in script:GetChildren() do
			if not func:IsA("ModuleScript") then
				continue
			end
			require(func)()
		end
	end
}


local Debris = game:GetService("Debris")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local Utils = require(ReplicatedStorage.Modules.Utils)
local CommonFunctions = require(ServerScriptService.System.CommonFunctions)
local AntiFly = {
    TimeBetweenChecks = 0.5,
    MaximumFlyTime = 10,
    VisualizeRays = false,
}

function AntiFly:Init()
    Utils.Player.ObservePlayers(function(plr: Player)
        Utils.Character.ObserveCharacter(plr, function(_char: Model)
            AntiFly._SetupCharacter(plr)
        end)
    end)
end

function AntiFly._SetupCharacter(player: Player)
    local char = player.Character
    local root = Utils.Character.GetRootPart(player)

    local Params = RaycastParams.new()
    Params.FilterType = Enum.RaycastFilterType.Exclude
    Params.FilterDescendantsInstances = {char}

    local lastTouched = time()

    task.spawn(function()
        while root and root.Parent and char and char.Parent do
            task.wait(AntiFly.TimeBetweenChecks)

            if AntiFly._IsCharacterNotFlying(char, root, Params) then
                lastTouched = time()
                continue --used to not check anything else during this check
            end

            local timeSinceFloor = time() - lastTouched
            if timeSinceFloor >= AntiFly.MaximumFlyTime then
                CommonFunctions.KickPlayer(player, CommonFunctions.KickIDs.Flying)
            end
        end
    end)
end

function AntiFly._IsCharacterNotFlying(model: Model, root: BasePart, Params: RaycastParams): boolean
    if not (model and root and root.Parent and not root.Anchored) then
        return true
    end
    if math.abs(root.AssemblyLinearVelocity.Y) > 45 then --if the player is flung it's obv gonna be false positive (DAMN YOU SHITTY ROBLOX PHYSICS!!!)
        return true
    end

    for frontIndex = -1, 1 do
        for sideIndex = -1, 1 do
            local Origin = root.Position + (root.CFrame.RightVector * sideIndex) + (root.CFrame.LookVector * frontIndex)
            local _, BoundSize = model:GetBoundingBox()
            local RayHeight = BoundSize.Y / 2 + 1.25

            local Cast = workspace:Raycast(Origin, Vector3.new(0, -RayHeight, 0), Params)

            if AntiFly.VisualizeRays then
                local Part = Instance.new("Part")
                Part.Anchored = true
                Part.CanCollide = false
                Part.CanTouch = false
                Part.CanQuery = false
                Part.Position = Origin - Vector3.new(0, 2.125, 0)
                Part.Size = Vector3.new(0.1, 4.25, 0.1)
                Part.Material = Enum.Material.Neon
                Part.Color = Cast and Cast.Instance and Color3.fromRGB(255, 145, 0) or Color3.fromRGB(255, 72, 0)
                Part.Parent = workspace.TempObjectFolders
                Debris:AddItem(Part, 3)
            end

            if Cast and Cast.Instance then
                return true
            end
        end
    end

    return false
end

return AntiFly


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

local CommonFunctions = require(ServerScriptService.System.CommonFunctions)
local Utils = require(ReplicatedStorage.Modules.Utils)

local AntiTP = {
    KickAllowed = false,
    PlayerThreads = {},
    PositionMagnitudeThreshold = 500,
}

function AntiTP:Init()
    Utils.Player.ObservePlayers(function(Player: Player)
        Utils.Character.ObserveCharacter(Player, function(Character: Model)
            AntiTP._SetupCharacter(Player)
        end)
        Player.CharacterRemoving:Connect(function(_Character: Model)
            if not AntiTP.PlayerThreads[Player.UserId] then
                return
            end

            AntiTP.PlayerThreads[Player.UserId]:Disconnect()
            AntiTP.PlayerThreads[Player.UserId] = nil
        end)
    end)

    Players.PlayerRemoving:Connect(function(Player: Player)
        if AntiTP.PlayerThreads[Player.UserId] then
            AntiTP.PlayerThreads[Player.UserId]:Disconnect()
            AntiTP.PlayerThreads[Player.UserId] = nil
        end
    end)
end

function AntiTP._SetupCharacter(Player: Player)
    local Character = Player.Character
    local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
    local HRP = Utils.Character.GetRootPart(Player)

    local LastPos = HRP.Position

    AntiTP.PlayerThreads[Player.UserId] = RunService.PostSimulation:Connect(function()
        local PosDiff = HRP.Position - LastPos

        if PosDiff.Magnitude > AntiTP.PositionMagnitudeThreshold then
            if AntiTP.KickAllowed then
                CommonFunctions.KickPlayer(Player, CommonFunctions.KickIDs.TPing)
                return
            end

            Utils.Misc.Print("Killing "..Player.Name..": TP detected")
            Humanoid.Health = 0
        end

        LastPos = HRP.Position
    end)
end

return AntiTP


local Figure = script.Parent
local Humanoid: Humanoid = Figure:WaitForChild("Humanoid")
if Humanoid.RigType ~= Enum.HumanoidRigType.R6 then
	return
end
local Torso = Figure:WaitForChild("Torso")
local RightShoulder = Torso:WaitForChild("Right Shoulder")
local LeftShoulder = Torso:WaitForChild("Left Shoulder")
local RightHip = Torso:WaitForChild("Right Hip")
local LeftHip = Torso:WaitForChild("Left Hip")
local TransitionTime = Figure:WaitForChild("PlayerAttributes"):WaitForChild("AnimationTransitionTime")
local pose = "Standing"

local EMOTE_TRANSITION_TIME = 0.1

local userAnimateScaleRunSuccess, userAnimateScaleRunValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserAnimateScaleRun") end)
local userAnimateScaleRun = userAnimateScaleRunSuccess and userAnimateScaleRunValue

local function getRigScale()
	if userAnimateScaleRun then
		return Figure:GetScale()
	else
		return 1
	end
end

local currentAnim = ""
local currentAnimInstance = nil
local currentAnimTrack = nil
local currentAnimKeyframeHandler = nil
local currentAnimSpeed = 1.0
local animTable = {}
local animNames = {
	idle = 	{
		{ id = "http://www.roblox.com/asset/?id=180435571", weight = 9 },
		{ id = "http://www.roblox.com/asset/?id=180435792", weight = 1 }
	},
	walk = 	{
		{ id = "http://www.roblox.com/asset/?id=180426354", weight = 10 }
	},
	run = 	{
		{ id = "run.xml", weight = 10 }
	},
	jump = 	{
		{ id = "http://www.roblox.com/asset/?id=125750702", weight = 10 }
	},
	fall = 	{
		{ id = "http://www.roblox.com/asset/?id=180436148", weight = 10 }
	},
	climb = {
		{ id = "http://www.roblox.com/asset/?id=180436334", weight = 10 }
	},
	sit = 	{
		{ id = "http://www.roblox.com/asset/?id=178130996", weight = 10 }
	},
	toolnone = {
		{ id = "http://www.roblox.com/asset/?id=182393478", weight = 10 }
	},
	toolslash = {
		{ id = "http://www.roblox.com/asset/?id=129967390", weight = 10 }
		--				{ id = "slash.xml", weight = 10 }
	},
	toollunge = {
		{ id = "http://www.roblox.com/asset/?id=129967478", weight = 10 }
	},
	wave = {
		{ id = "http://www.roblox.com/asset/?id=128777973", weight = 10 }
	},
	point = {
		{ id = "http://www.roblox.com/asset/?id=128853357", weight = 10 }
	},
	dance1 = {
		{ id = "http://www.roblox.com/asset/?id=182435998", weight = 10 },
		{ id = "http://www.roblox.com/asset/?id=182491037", weight = 10 },
		{ id = "http://www.roblox.com/asset/?id=182491065", weight = 10 }
	},
	dance2 = {
		{ id = "http://www.roblox.com/asset/?id=182436842", weight = 10 },
		{ id = "http://www.roblox.com/asset/?id=182491248", weight = 10 },
		{ id = "http://www.roblox.com/asset/?id=182491277", weight = 10 }
	},
	dance3 = {
		{ id = "http://www.roblox.com/asset/?id=182436935", weight = 10 },
		{ id = "http://www.roblox.com/asset/?id=182491368", weight = 10 },
		{ id = "http://www.roblox.com/asset/?id=182491423", weight = 10 }
	},
	laugh = {
		{ id = "http://www.roblox.com/asset/?id=129423131", weight = 10 }
	},
	cheer = {
		{ id = "http://www.roblox.com/asset/?id=129423030", weight = 10 }
	},
}
local dances = {"dance1", "dance2", "dance3"}

-- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote
local emoteNames = { wave = false, point = false, dance1 = true, dance2 = true, dance3 = true, laugh = false, cheer = false}

function configureAnimationSet(name, fileList)
	if (animTable[name] ~= nil) then
		for _, connection in pairs(animTable[name].connections) do
			connection:Disconnect()
		end
	end
	animTable[name] = {}
	animTable[name].count = 0
	animTable[name].totalWeight = 0
	animTable[name].connections = {}

	-- check for config values
	local config = script:FindFirstChild(name)
	if (config ~= nil) then
		--		print("Loading anims " .. name)
		table.insert(animTable[name].connections, config.ChildAdded:Connect(function(child)
            configureAnimationSet(name, fileList)
        end))
		table.insert(animTable[name].connections, config.ChildRemoved:Connect(function(child)
            configureAnimationSet(name, fileList)
        end))
		local idx = 1
		for _, childPart in pairs(config:GetChildren()) do
			if (childPart:IsA("Animation")) then
				table.insert(animTable[name].connections, childPart.Changed:Connect(function(property)
                    configureAnimationSet(name, fileList)
                end))
				animTable[name][idx] = {}
				animTable[name][idx].anim = childPart
				local weightObject = childPart:FindFirstChild("Weight")
				if (weightObject == nil) then
					animTable[name][idx].weight = 1
				else
					animTable[name][idx].weight = weightObject.Value
				end
				animTable[name].count = animTable[name].count + 1
				animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight
				--			print(name .. " [" .. idx .. "] " .. animTable[name][idx].anim.AnimationId .. " (" .. animTable[name][idx].weight .. ")")
				idx = idx + 1
			end
		end
	end

	-- fallback to defaults
	if (animTable[name].count <= 0) then
		for idx, anim in pairs(fileList) do
			animTable[name][idx] = {}
			animTable[name][idx].anim = Instance.new("Animation")
			animTable[name][idx].anim.Name = name
			animTable[name][idx].anim.AnimationId = anim.id
			animTable[name][idx].weight = anim.weight
			animTable[name].count = animTable[name].count + 1
			animTable[name].totalWeight = animTable[name].totalWeight + anim.weight
			--			print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")")
		end
	end
end

-- Setup animation objects
function scriptChildModified(child)
	local fileList = animNames[child.Name]
	if (fileList ~= nil) then
		configureAnimationSet(child.Name, fileList)
	end
end

script.ChildAdded:Connect(scriptChildModified)
script.ChildRemoved:Connect(scriptChildModified)

-- Clear any existing animation tracks
-- Fixes issue with characters that are moved in and out of the Workspace accumulating tracks
local animator = if Humanoid then Humanoid:FindFirstChildWhichIsA("Animator") else nil
if animator then
	local animTracks = animator:GetPlayingAnimationTracks()
	for _, track in ipairs(animTracks) do
		track:Stop(0)
		track:Destroy()
	end
end

for name, fileList in pairs(animNames) do
	configureAnimationSet(name, fileList)
end

-- ANIMATION

-- declarations
local toolAnim = "None"
local toolAnimTime = 0

local jumpAnimTime = 0
local jumpAnimDuration = TransitionTime.Value * 3

local toolTransitionTime = TransitionTime.Value
local fallTransitionTime = TransitionTime.Value * 3

-- functions

function stopAllAnimations()
	local oldAnim = currentAnim

	-- return to idle if finishing an emote
	if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then
		oldAnim = "idle"
	end

	currentAnim = ""
	currentAnimInstance = nil
	if (currentAnimKeyframeHandler ~= nil) then
		currentAnimKeyframeHandler:Disconnect()
	end

	if (currentAnimTrack ~= nil) then
		currentAnimTrack:Stop()
		currentAnimTrack:Destroy()
		currentAnimTrack = nil
	end
	return oldAnim
end

function setAnimationSpeed(speed)
	if speed ~= currentAnimSpeed then
		currentAnimSpeed = speed
		currentAnimTrack:AdjustSpeed(currentAnimSpeed)
	end
end

function keyFrameReachedFunc(frameName)
	if (frameName == "End") then

		local repeatAnim = currentAnim
		-- return to idle if finishing an emote
		if (emoteNames[repeatAnim] ~= nil and emoteNames[repeatAnim] == false) then
			repeatAnim = "idle"
		end

		local animSpeed = currentAnimSpeed
		playAnimation(repeatAnim, 0.0, Humanoid)
		setAnimationSpeed(animSpeed)
	end
end

-- Preload animations
function playAnimation(animName, transitionTime, humanoid)

	local roll = math.random(1, animTable[animName].totalWeight)
	local idx = 1
	while (roll > animTable[animName][idx].weight) do
		roll = roll - animTable[animName][idx].weight
		idx = idx + 1
	end
	--		print(animName .. " " .. idx .. " [" .. origRoll .. "]")
	local anim = animTable[animName][idx].anim

	-- switch animation
	if (anim ~= currentAnimInstance) then

		if (currentAnimTrack ~= nil) then
			currentAnimTrack:Stop(transitionTime)
			currentAnimTrack:Destroy()
		end

		currentAnimSpeed = 1.0

		-- load it to the humanoid; get AnimationTrack
		currentAnimTrack = animator:LoadAnimation(anim)
		currentAnimTrack.Priority = Enum.AnimationPriority.Core

		-- play the animation
		currentAnimTrack:Play(transitionTime)
		currentAnim = animName
		currentAnimInstance = anim

		-- set up keyframe name triggers
		if (currentAnimKeyframeHandler ~= nil) then
			currentAnimKeyframeHandler:Disconnect()
		end
		currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:Connect(keyFrameReachedFunc)

	end

end

-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------

local toolAnimName = ""
local toolAnimTrack = nil
local toolAnimInstance = nil
local currentToolAnimKeyframeHandler = nil

function toolKeyFrameReachedFunc(frameName)
	if (frameName == "End") then
		--		print("Keyframe : ".. frameName)
		playToolAnimation(toolAnimName, 0.0, Humanoid)
	end
end

function playToolAnimation(animName, transitionTime, humanoid, priority)

	local roll = math.random(1, animTable[animName].totalWeight)
	local idx = 1
	while (roll > animTable[animName][idx].weight) do
		roll = roll - animTable[animName][idx].weight
		idx = idx + 1
	end
	--		print(animName .. " * " .. idx .. " [" .. origRoll .. "]")
	local anim = animTable[animName][idx].anim

	if (toolAnimInstance ~= anim) then

		if (toolAnimTrack ~= nil) then
			toolAnimTrack:Stop()
			toolAnimTrack:Destroy()
			transitionTime = 0
		end

		-- load it to the humanoid; get AnimationTrack
		toolAnimTrack = animator:LoadAnimation(anim)
		if priority then
			toolAnimTrack.Priority = priority
		end

		-- play the animation
		toolAnimTrack:Play(transitionTime)
		toolAnimName = animName
		toolAnimInstance = anim

		currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:Connect(toolKeyFrameReachedFunc)
	end
end

function stopToolAnimations()
	local oldAnim = toolAnimName

	if (currentToolAnimKeyframeHandler ~= nil) then
		currentToolAnimKeyframeHandler:Disconnect()
	end

	toolAnimName = ""
	toolAnimInstance = nil
	if (toolAnimTrack ~= nil) then
		toolAnimTrack:Stop()
		toolAnimTrack:Destroy()
		toolAnimTrack = nil
	end

	return oldAnim
end

-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------

local Speed = 0
game:GetService("RunService").RenderStepped:Connect(function(_delta: number)
	if currentAnimInstance and pose == "Running" then
		local HRP = Figure:FindFirstChild("HumanoidRootPart") or Figure:FindFirstChildWhichIsA("Humanoid").RootPart or Figure.PrimaryPart
		local DirectionOfMovement = HRP.CFrame:VectorToObjectSpace(HRP.AssemblyLinearVelocity)
		DirectionOfMovement = Vector3.new( DirectionOfMovement.X / Humanoid.WalkSpeed, 0, DirectionOfMovement.Z / Humanoid.WalkSpeed )

		if DirectionOfMovement.Z < 0.1 then
			setAnimationSpeed(Speed / 14.5)
		else
			setAnimationSpeed(-Speed / 14.5)
		end
	end
end)

function onRunning(speed)
	speed /= getRigScale()
	Speed = speed

	if speed > 0.01 then
		playAnimation("walk", TransitionTime.Value, Humanoid)
		if currentAnimInstance and currentAnimInstance.AnimationId == "http://www.roblox.com/asset/?id=180426354" then
			setAnimationSpeed(speed / 14.5)
		end
		pose = "Running"
	else
		if emoteNames[currentAnim] == nil then
			playAnimation("idle", TransitionTime.Value, Humanoid)
			pose = "Standing"
		end
	end
end

function onDied()
	pose = "Dead"
end

function onJumping()
	playAnimation("jump", TransitionTime.Value, Humanoid)
	jumpAnimTime = jumpAnimDuration
	pose = "Jumping"
end

function onClimbing(speed)
	speed /= getRigScale()

	playAnimation("climb", TransitionTime.Value, Humanoid)
	setAnimationSpeed(speed / 12.0)
	pose = "Climbing"
end

function onGettingUp()
	pose = "GettingUp"
end

function onFreeFall()
	if (jumpAnimTime <= 0) then
		playAnimation("fall", fallTransitionTime, Humanoid)
	end
	pose = "FreeFall"
end

function onFallingDown()
	pose = "FallingDown"
end

function onSeated()
	pose = "Seated"
end

function onPlatformStanding()
	pose = "PlatformStanding"
end

function onSwimming(speed)
	if speed > 0 then
		pose = "Running"
	else
		pose = "Standing"
	end
end

function getTool()
	for _, kid in ipairs(Figure:GetChildren()) do
		if kid.className == "Tool" then return kid end
	end
	return nil
end

function getToolAnim(tool)
	for _, c in ipairs(tool:GetChildren()) do
		if c.Name == "toolanim" and c.className == "StringValue" then
			return c
		end
	end
	return nil
end

function animateTool()

	if (toolAnim == "None") then
		playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle)
		return
	end

	if (toolAnim == "Slash") then
		playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action)
		return
	end

	if (toolAnim == "Lunge") then
		playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action)
		return
	end
end

local lastTick = 0

function move(time)
	local amplitude = 1
	local frequency = 1
	local deltaTime = time - lastTick
	lastTick = time

	local climbFudge = 0
	local setAngles = false

	if (jumpAnimTime > 0) then
		jumpAnimTime = jumpAnimTime - deltaTime
	end

	if (pose == "FreeFall" and jumpAnimTime <= 0) then
		playAnimation("fall", fallTransitionTime, Humanoid)
	elseif (pose == "Seated") then
		playAnimation("sit", 0.5, Humanoid)
		return
	elseif (pose == "Running") then
		playAnimation("walk", 0.1, Humanoid)
	elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then
		--		print("Wha " .. pose)
		stopAllAnimations()
		amplitude = 0.1
		frequency = 1
		setAngles = true
	end

	if (setAngles) then
		local desiredAngle = amplitude * math.sin(time * frequency)

		RightShoulder:SetDesiredAngle(desiredAngle + climbFudge)
		LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge)
		RightHip:SetDesiredAngle(-desiredAngle)
		LeftHip:SetDesiredAngle(-desiredAngle)
	end

	-- Tool Animation handling
	local tool = getTool()
	if tool and tool:FindFirstChild("Handle") then

		local animStringValueObject = getToolAnim(tool)

		if animStringValueObject then
			toolAnim = animStringValueObject.Value
			-- message recieved, delete StringValue
			animStringValueObject.Parent = nil
			toolAnimTime = time + .3
		end

		if time > toolAnimTime then
			toolAnimTime = 0
			toolAnim = "None"
		end

		animateTool()
	else
		stopToolAnimations()
		toolAnim = "None"
		toolAnimInstance = nil
		toolAnimTime = 0
	end
end

-- connect events
Humanoid.Died:Connect(onDied)
Humanoid.Running:Connect(onRunning)
Humanoid.Jumping:Connect(onJumping)
Humanoid.Climbing:Connect(onClimbing)
Humanoid.GettingUp:Connect(onGettingUp)
Humanoid.FreeFalling:Connect(onFreeFall)
Humanoid.FallingDown:Connect(onFallingDown)
Humanoid.Seated:Connect(onSeated)
Humanoid.PlatformStanding:Connect(onPlatformStanding)
Humanoid.Swimming:Connect(onSwimming)

---- setup emote chat hook
game:GetService("Players").LocalPlayer.Chatted:Connect(function(msg)
	local emote = ""
	if msg == "/e dance" then
		emote = dances[math.random(1, #dances)]
	elseif (string.sub(msg, 1, 3) == "/e ") then
		emote = string.sub(msg, 4)
	elseif (string.sub(msg, 1, 7) == "/emote ") then
		emote = string.sub(msg, 8)
	end

	if (pose == "Standing" and emoteNames[emote] ~= nil) then
		playAnimation(emote, 0.1, Humanoid)
	end

end)

-- emote bindable hook
script:WaitForChild("PlayEmote").OnInvoke = function(emote)
	-- Only play emotes when idling
	if pose ~= "Standing" then
		return
	end
	if emoteNames[emote] ~= nil then
		-- Default emotes
		playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid)

		return true, currentAnimTrack
	end

	-- Return false to indicate that the emote could not be played
	return false
end
-- main program

-- initialize to idle
playAnimation("idle", 0.1, Humanoid)
pose = "Standing"

while Figure.Parent ~= nil do
	move(task.wait(0.1))
end


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

local Utils = require(ReplicatedStorage.Modules.Utils)
local Janitor = require(ReplicatedStorage.Packages.Janitor)

local AnimationManager = {
    AnimationTracks = {},
	CurrentCoreAnimation = nil,
	Character = nil,
	Janitor = nil,
	AnimationTransitionTime = nil,
	Initted = false,
}

local DefaultAnimations: {[string]: string} = {
	Climb = "rbxassetid://180436334",
	Fall = "rbxassetid://180436148",
	Idle = "rbxassetid://180435571",
	Walk = "rbxassetid://180426354",
	Sit = "rbxassetid://178130996",
}

local SpectatorOnly = {
	"Fall",
	"Climb",
	"Sit",
}

function AnimationManager:Init()
	if self.Initted then
		return
	end
	self.Initted = true

	self.Character = self.Character or Players.LocalPlayer.Character
	local Humanoid = Utils.Instance.WaitForChildWhichIsA(self.Character, "Humanoid") :: Humanoid
	local HumanoidRootPart = Utils.Character.GetRootPart(self.Character)
	local Role = self.Character:FindFirstChild("Role")

	local Sprinting = self.Character.PlayerAttributes.Sprinting

	self.AnimationTransitionTime = Utils.Instance.FindFirstChild(self.Character, "PlayerAttributes.AnimationTransitionTime")

	for Name, ID in DefaultAnimations do
		if self.AnimationTracks[Name] then
			continue
		end

		if Role and Role.Value ~= "Spectator" and table.find(SpectatorOnly, Name) then
			continue
		end

		self:LoadAnimation(Name, ID, false, Enum.AnimationPriority.Core)
	end

	local function onClimbing(speed: number)
		self:PlayAnimation("Climb")

		self:AdjustAnimationSpeed("Climb", speed / 11)
	end

	local function onFreeFalling(active: boolean)
		if active then
			self:PlayAnimation("Fall", self.AnimationTransitionTime.Value * 5)
		end
	end

	local Speed = 0
	local function onRunning(speed: number?)
		Speed = (speed ~= nil and typeof(speed) == "number") and speed or Speed

		if Speed > 0.01 then
			self:PlayAnimation(if (self.AnimationTracks.Sprint and Sprinting.Value) then "Sprint" else "Walk")
		else
			self:PlayAnimation("Idle")
		end
	end

	local function onSeated(active: boolean)
		if active then
			self:PlayAnimation("Sit")
		end
	end

	self.Janitor = Janitor.new()
	self.Janitor:LinkToInstance(self.Character)

	self.Janitor:Add(Humanoid.Climbing:Connect(onClimbing))
	self.Janitor:Add(Humanoid.FreeFalling:Connect(onFreeFalling))
	self.Janitor:Add(Humanoid.Running:Connect(onRunning))
	self.Janitor:Add(Humanoid.Seated:Connect(onSeated))
	self.Janitor:Add(Sprinting.Changed:Connect(onRunning))

	self.Janitor:Add(RunService.PreAnimation:Connect(function()
		if (self.AnimationTracks.Walk and self.AnimationTracks.Walk.IsPlaying) or (self.AnimationTracks.Sprint and self.AnimationTracks.Sprint.IsPlaying) then
			local DirectionOfMovement = HumanoidRootPart.CFrame:VectorToObjectSpace(HumanoidRootPart.AssemblyLinearVelocity)
			DirectionOfMovement = Vector3.new(DirectionOfMovement.X / Humanoid.WalkSpeed, 0, DirectionOfMovement.Z / Humanoid.WalkSpeed)
			if DirectionOfMovement.Z > 0.1 then
				self:AdjustAnimationSpeed("Walk", -(Humanoid.WalkSpeed / 16))
				self:AdjustAnimationSpeed("Sprint", -(Humanoid.WalkSpeed / 30))
			else
				self:AdjustAnimationSpeed("Walk", Humanoid.WalkSpeed / 16)
				self:AdjustAnimationSpeed("Sprint", Humanoid.WalkSpeed / 30)
			end
		end
	end))

	self:PlayAnimation("Idle")
end

function AnimationManager:LoadAnimation(Name: string, ID: string, YieldUntilLoad: boolean?, PriorityOverride: Enum.AnimationPriority?): AnimationTrack
	--removes the animation if it exists
	self:RemoveAnimation(Name)

	--init the module if not initted yet
	if not self.Initted then
		self:Init()
	end

	--loads the new one
	local AnimationTrack = Utils.Character.LoadAnimationFromID(self.Character, ID, YieldUntilLoad)

	if PriorityOverride then
		AnimationTrack.Priority = PriorityOverride
	end

	self.AnimationTracks[Name] = AnimationTrack

	return AnimationTrack
end

function AnimationManager:RemoveAnimation(Name: string)
	local AnimationTrack = self.AnimationTracks[Name]
	if AnimationTrack then
		AnimationTrack:Stop(0)
		AnimationTrack:Destroy()
	end
end

function AnimationManager:PlayAnimation(Name: string, fadeTime: number?)
	local AnimationTrack: AnimationTrack = self.AnimationTracks[Name]
	if not AnimationTrack or AnimationTrack.IsPlaying then
		return
	end

	if AnimationTrack.Priority == Enum.AnimationPriority.Core or DefaultAnimations[Name] or Name == "Sprint" then
		if self.CurrentCoreAnimation then
			self.CurrentCoreAnimation:Stop(fadeTime or self.AnimationTransitionTime.Value)
		end
		AnimationTrack:Play(fadeTime or self.AnimationTransitionTime.Value)
		self.CurrentCoreAnimation = AnimationTrack

		return
	end

	AnimationTrack:Play(fadeTime or self.AnimationTransitionTime.Value)
end

function AnimationManager:StopAnimation(Name: string, fadeTime: number?)
	local AnimationTrack: AnimationTrack = self.AnimationTracks[Name]
	if not AnimationTrack or not AnimationTrack.IsPlaying then
		return
	end

	if self.CurrentCoreAnimation == AnimationTrack then
		self.CurrentCoreAnimation = nil
	end

	AnimationTrack:Stop(fadeTime or self.AnimationTransitionTime.Value)
end

function AnimationManager:AdjustAnimationSpeed(Name: string, Speed: number)
	local AnimationTrack = self.AnimationTracks[Name]
	if not AnimationTrack then
		return
	end

	AnimationTrack:AdjustSpeed(Speed)
end

--this is useless but it's done for readability's sake
function AnimationManager:GetAnimationTrack(Name: string): AnimationTrack
	return self.AnimationTracks[Name]
end

return AnimationManager


-- Gradually regenerates the Humanoid's Health over time.

--[[local REGEN_RATE = 1/100 -- Regenerate this fraction of MaxHealth per second.
local REGEN_STEP = 1 -- Wait this long between each regeneration step.

--------------------------------------------------------------------------------

local Character = script.Parent
local Humanoid = Character:WaitForChild('Humanoid')

--------------------------------------------------------------------------------

while true do
	while Humanoid.Health < Humanoid.MaxHealth do
		local dt = task.wait(REGEN_STEP)
		local dh = dt*REGEN_RATE*Humanoid.MaxHealth
		Humanoid.Health = math.min(Humanoid.Health + dh, Humanoid.MaxHealth)
	end
	Humanoid.HealthChanged:Wait()
end]]

local Config = {
    RegenRate = 0, --1/100
    RegenStepInSeconds = 1,
}

local char = script.Parent
local Humanoid = char:FindFirstChildWhichIsA("Humanoid")

if Config.RegenRate > 0 and Config.RegenStepInSeconds > 0 then
    while Humanoid.HealthChanged:Wait() do
        while Humanoid.Health < Humanoid.MaxHealth do
            local Delta = task.wait(Config.RegenStepInSeconds)
            local DH = Delta * Config.RegenRate * Humanoid.MaxHealth
            Humanoid.Health = math.min(Humanoid.Health + DH, Humanoid.MaxHealth)
        end
    end
end