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

local EmoteManager = {
	EmotesAvailable = {},
	CurrentlyPlayingEmote = nil,
	Janitor = Janitor.new(),
}

function EmoteManager:Init()
	local LocalPlayer = game:GetService("Players").LocalPlayer
	local EquippedEmotes = Utils.Instance.FindFirstChild(LocalPlayer, "PlayerData.Equipped.Emotes")

	self.Char = Players.LocalPlayer.Character
	self.Char:SetAttribute("Emoting", false)
	self.Hum = self.Char:FindFirstChildWhichIsA("Humanoid")
	self.Head = self.Char:FindFirstChild("Head")

	self.Animator = self.Char:FindFirstChildWhichIsA("Humanoid"):FindFirstChildWhichIsA("Animator")

	self.Janitor:LinkToInstance(self.Char)

	self.Janitor:Add(EquippedEmotes.DescendantAdded:Connect(function(_descendant: ObjectValue)
		self:ReloadAvailableContent(EquippedEmotes)
	end))

	self.Janitor:Add(EquippedEmotes.DescendantRemoving:Connect(function(_descendant: ObjectValue)
		self:ReloadAvailableContent(EquippedEmotes)
	end))

	for _, Emote in EquippedEmotes:GetChildren() do
		self.Janitor:Add(Emote.Changed:Connect(function()
			self:ReloadAvailableContent(EquippedEmotes)
		end))
	end

	self.Janitor:Add(require(LocalPlayer.PlayerScripts.InputManager):GetInputAction("Miscellaneous.StopEmote").Pressed:Connect(function()
		if self.CurrentlyPlayingEmote then
			self:StopEmote(self.CurrentlyPlayingEmote)
		end
	end), "Disconnect")

	self.Janitor:Add(self.Hum.Died:Connect(function()
		if self.CurrentlyPlayingEmote then
			self:StopEmote(self.CurrentlyPlayingEmote)
		end
	end))

	self.Janitor:Add(function()
		for _, Emote in self.EmotesAvailable do
			self:StopEmote(Emote.Config.Name)
		end
		if self.Char then
			self.Char:SetAttribute("Emoting", false)
		end
	end, true)

	self:ReloadAvailableContent(EquippedEmotes)
end

function EmoteManager:PlayEmote(name: string)
	if not self.EmotesAvailable[name] or not self.Hum or self.Hum.Health <= 0 then return end

	for _, Emote in self.EmotesAvailable do
		self:StopEmote(Emote.Config.Name or Emote)
	end

	self.CurrentlyPlayingEmote = self.EmotesAvailable[name]
	workspace.CurrentCamera.CameraSubject = self.Head
	self.CurrentlyPlayingEmote:BaseBehaviour()
end

function EmoteManager:StopEmote(name: string | {})
	if not self.Hum then
		return
	end

	if typeof(name) == "string" then
		if not self.EmotesAvailable[name] or not self.EmotesAvailable[name].TrackPlaying then
			return
		end

		Network:FireServerConnection("StopEmote", "REMOTE_EVENT", name)
		self.CurrentlyPlayingEmote = nil

		return
	end

	if self.Hum.Health > 0 then
		workspace.CurrentCamera.CameraSubject = self.Hum
	end

	Network:FireServerConnection("StopEmote", "REMOTE_EVENT", name.Config.Name)
	self.Char:SetAttribute("Emoting", false)
	self.CurrentlyPlayingEmote = nil
end

function EmoteManager:ReloadAvailableContent(EquippedEmotes: Folder)
	for _, emote: StringValue in EquippedEmotes:GetChildren() do
		if self.EmotesAvailable[emote.Value] then
			return
		end
		if not emote:IsA("StringValue") or #emote.Value <= 0 then
			continue
		end

		local Module = Utils.Instance.GetEmoteModule(emote.Value)
		if not Module then
			return
		end

		self.EmotesAvailable[emote.Value] = Utils.Type.CopyTable(require(Module))
		Network:FireServerConnection("InitEmote", "REMOTE_EVENT", Module.Name)
		self.EmotesAvailable[emote.Value]:Init(Players.LocalPlayer)
	end
end

return EmoteManager


return {
    Init = function(_self)
        local Players = game:GetService("Players")
        local ReplicatedStorage = game:GetService("ReplicatedStorage")

        local Sounds = require(ReplicatedStorage.Modules.Sounds)

        local Character = Players.LocalPlayer.Character

        local function ToggleTheme(role: string)
            if role == "Spectator" then
                Sounds.PlayTheme("rbxassetid://75034945737494", {Priority = 1, Volume = 0.2})
            else
                Sounds.StopTheme("rbxassetid://75034945737494")
            end
        end

        local Role = Character:FindFirstChild("Role")
        ToggleTheme(Role.Value)
        Role.Changed:Connect(function(Value: string)
            ToggleTheme(Value)
        end)
    end
}


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

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

local LocalPlayer = Players.LocalPlayer
local RoundState: StringValue = ReplicatedStorage.RoundInfo.CurrentState

local Radiuses = {
	["L4Upcoming"] = -1e10, ["L4"] = 15,
	["L3Upcoming"] = 15, ["L3"] = 30,
	["L2Upcoming"] = 30, ["L2"] = 45,
	["L1Upcoming"] = 45, ["L1"] = 60,
}

local TerrorRadiusManager = {
    AbleToInit = true,
    LayerMagnitudes = {},
    InChase = false,
    Playing = false,
    ThemeFolder = nil,
    LocalPlayerProps = {
        Role = "",
        Char = nil,
        Humanoid = nil,
        HRP = nil,
    },
    MapAmbience = nil,
    MapAmbienceVolume = 0,
    MapAmbienceTweenState = "Play",
    Janitor = nil,
}

local function CalculateSound(Distance)
	return 4.96 / (Distance + 10.26)
end

function TerrorRadiusManager:Init()
    local ThemeFolder = SoundService:FindFirstChild("ChaseThemes")
    if ThemeFolder then
        ThemeFolder:Destroy()
    end

    if not self.AbleToInit then
        return
    end
    local Char = LocalPlayer.Character
    local Role = Char:FindFirstChild("Role").Value
    if Role == "Spectator" then
        return
    end
    local Humanoid = Char:FindFirstChildWhichIsA("Humanoid")
    self.LocalPlayerProps = {
        Role = Role,
        Char = Char,
        Humanoid = Humanoid,
        HRP = Utils.Character.GetRootPart(Char),
    }

    self.MapAmbience = workspace.Themes:FindFirstChild("MapAmbience")
    self.MapAmbienceVolume = self.MapAmbience and self.MapAmbience.Volume or 0

    ThemeFolder = Instance.new("Folder")
    ThemeFolder.Name = "ChaseThemes"
    ThemeFolder.Parent = SoundService
    self.ThemeFolder = ThemeFolder

    while RoundState.Value ~= "InRound" do
        task.wait()
    end

    for _, PlayerChar in workspace.Players:GetChildren() do
        if not PlayerChar:FindFirstChild("Role") or PlayerChar.Role.Value ~= "Killer" then
            continue
        end

        local PlayerLayerFolder = Instance.new("Folder")
        PlayerLayerFolder.Name = PlayerChar.Name
        PlayerLayerFolder.Parent = self.ThemeFolder

        local ChaseThemes = require(Utils.Instance.GetCharacterModule("Killer", PlayerChar:GetAttribute("CharacterName"), PlayerChar:GetAttribute("CharacterSkinName"))).Config.ChaseThemes
        for layerName, ID in ChaseThemes do
            local Sound = Instance.new("Sound")
            Sound.Name = layerName
            Sound.SoundId = ID
            Sound.Volume = 0
            Sound.Looped = true
            Sound.SoundGroup = SoundService.SoundGroups.Master.Music
            Sound.Parent = PlayerLayerFolder

	    	if not Sound.IsLoaded then
	    		Sound.Loaded:Wait()
	    	end

            self:SetLayerMagnitude(Sound, PlayerChar.Name, Radiuses[layerName])
        end
    end

    for _, Theme in self.ThemeFolder:GetDescendants() do
        if Theme:IsA("Sound") and Theme.Name ~= "L4" then
            Theme:Play()
        end
    end

    local TerrorRadiusJanitor = Janitor.new()

    --slow down on death fx
    TerrorRadiusJanitor:Add(self.LocalPlayerProps.Humanoid.Died:Connect(function()
        for _, s in self.ThemeFolder:GetDescendants() do
            if not s:IsA("Sound") then
                continue
            end
            TweenService:Create(s, TweenInfo.new(0.4), {PlaybackSpeed = 0}):Play()
        end
    end))

    TerrorRadiusJanitor:Add(RunService.PreRender:Connect(function(delta: number)
        self:CheckInRadius(delta)
    end))

    TerrorRadiusJanitor:Add(function()
        if self.ThemeFolder then
            self.ThemeFolder:Destroy()
        end
    end, true)

    TerrorRadiusJanitor:LinkToInstance(Char)

    self.Janitor = TerrorRadiusJanitor
end

function TerrorRadiusManager:SetLayerMagnitude(SoundInstance: Sound, Parent: string, Radius: number)
    if not self.LayerMagnitudes[Parent] then
        self.LayerMagnitudes[Parent] = {}
    end
    self.LayerMagnitudes[Parent][Radius] = SoundInstance
end

function TerrorRadiusManager:CheckInRadius(delta: number)
    if self.LocalPlayerProps.Role == "Survivor" then
        self:_CheckAsSurvivor(delta)
    elseif self.LocalPlayerProps.Role == "Killer" then
        self:_CheckAsKiller(delta)
    end
end

function TerrorRadiusManager:TweenVolume(Sound: Sound, TargetVolume: number, delta: number)
    if math.abs(Sound.Volume - TargetVolume) > 0.01 then
        Sound.Volume = math.lerp(Sound.Volume, TargetVolume, delta * 14)
    end
end

function TerrorRadiusManager:PlayLayer4(Subfolder: string, delta: number)
    if self.Playing then
        return
    end
    self.Playing = true

    for _, s: Sound in self.LayerMagnitudes[Subfolder] do
        if s.Name == "L4" then
            s:Play()
            self:TweenVolume(s, 0.5, delta)
            local conn
            conn = self.Janitor:Add(s.Changed:Connect(function()
                if s.Volume <= 0 then
                    conn:Disconnect()
                end
            end))
        else
            self:TweenVolume(s, 0, delta)
        end
    end
end

function TerrorRadiusManager:_CheckAsSurvivor(delta: number)
    local PlayersFound = {}
    for _, PlayerChar in workspace.Players:GetChildren() do
        if not self.ThemeFolder:FindFirstChild(PlayerChar.Name) and PlayerChar:FindFirstChild("Role") and PlayerChar.Role.Value == "Killer" then
            local PlayerThemeFolder = Instance.new("Folder")
            PlayerThemeFolder.Name = PlayerChar.Name
            PlayerThemeFolder.Parent = self.ThemeFolder

            local ChaseThemes = require(Utils.Instance.GetCharacterModule("Killer", PlayerChar:GetAttribute("CharacterName"), PlayerChar:GetAttribute("CharacterSkinName"))).Config.ChaseThemes
            for layerName, ID in ChaseThemes do
                local Sound = Instance.new("Sound")
                Sound.Name = layerName
                Sound.SoundId = ID
                Sound.Volume = 0
                Sound.Looped = true
                Sound.SoundGroup = SoundService.SoundGroups.Master.Music
                Sound.Parent = PlayerThemeFolder

	        	if not Sound.IsLoaded then
	        		Sound.Loaded:Wait()
	        	end

                self:SetLayerMagnitude(Sound, PlayerChar.Name, Radiuses[layerName])
            end
        end
        table.insert(PlayersFound, PlayerChar.Name)
    end
    for _, Theme in self.ThemeFolder:GetChildren() do
        if not table.find(PlayersFound, Theme.Name) then
            Theme:Destroy()
        end
    end

    local CurrentKillerChosen
    for _, s in self.ThemeFolder:GetDescendants() do
        if s:IsA("Sound") and s.Volume > 0 then
            CurrentKillerChosen = s.Parent.Name
            break
        end
    end
    if not CurrentKillerChosen then
        for _, PlayerChar in workspace.Players:GetChildren() do
            if not PlayerChar:FindFirstChild("Role") or PlayerChar.Role.Value ~= "Killer" then
                continue
            end

            local Distance = self.LocalPlayerProps.Humanoid and self.LocalPlayerProps.Humanoid.Health > 0 and (self.LocalPlayerProps.HRP.Position - Utils.Character.GetRootPart(PlayerChar).Position).Magnitude or 50000
            if Distance <= Radiuses["L1"] and (not CurrentKillerChosen or Distance <= CurrentKillerChosen.Distance) then
                CurrentKillerChosen = {
                    Player = PlayerChar,
                    Distance = Distance,
                }
            end
        end

        if not CurrentKillerChosen then
            return
        end
        CurrentKillerChosen = CurrentKillerChosen.Player
    else
        CurrentKillerChosen = workspace.Players:FindFirstChild(CurrentKillerChosen)
    end

    local KillerHRP = Utils.Character.GetRootPart(CurrentKillerChosen)

    local Distance = self.LocalPlayerProps.Humanoid and self.LocalPlayerProps.Humanoid.Health > 0 and (self.LocalPlayerProps.HRP.Position - KillerHRP.Position).Magnitude or 50000

    local Playing = false
    for Killer, Layers in self.LayerMagnitudes do
        if tostring(Killer) == tostring(CurrentKillerChosen) then
            local HasUndetectable = Utils.Instance.FindFirstChild(CurrentKillerChosen, "Effects.Undetectable", 0)
            for radius, sound in Layers do
                local Upcoming = Radiuses[sound.Name.."Upcoming"] or -1e10

                if self.InChase then
                    if Distance <= Radiuses["L1"] and not HasUndetectable then
                        if sound.Name == "L4" then
                            Playing = true
                            self:TweenVolume(sound, Distance < 55 and 0.5 or CalculateSound(Distance), delta)
                        else
                            self:TweenVolume(sound, 0, delta)
                        end
                    else
                        self.InChase = false
                        if sound.Name == "L4" then
                            self.Playing = false
                            self:TweenVolume(sound, 0, delta)
                        end
                    end
                else
                    if Distance <= radius and Distance >= Upcoming and not HasUndetectable then
                        if sound.Name == "L4" then
                            Playing = true
                            self.InChase = true
                            self:PlayLayer4(tostring(CurrentKillerChosen), delta)
                            self:TweenVolume(sound, 0.5, delta)
                        else
                            Playing = true
                            self:TweenVolume(sound, CalculateSound(Distance), delta)
                        end
                    else
                        self:TweenVolume(sound, 0, delta)
                    end
                end
            end
            continue
        end

        for _, sound in Killer do
            self:TweenVolume(sound, 0, delta)
        end
    end

    if self.MapAmbience then
        if Playing then
            if self.MapAmbienceTweenState ~= "Stop" then
                self.MapAmbienceTweenState = "Stop"
                TweenService:Create(self.MapAmbience, TweenInfo.new(0.4), {Volume = 0}):Play()
            end
        else
            if self.MapAmbienceTweenState ~= "Play" then
                self.MapAmbienceTweenState = "Play"
                TweenService:Create(self.MapAmbience, TweenInfo.new(0.4), {Volume = self.MapAmbienceVolume}):Play()
            end
        end
    end

    return
end

function TerrorRadiusManager:_CheckAsKiller(delta: number)
    local ClosestDistance = 50000

    for _, char in workspace.Players:GetChildren() do
        if char == self.LocalPlayerProps.Char then
            continue
        end

        local Hum = char:FindFirstChildWhichIsA("Humanoid")
        if not Hum or Hum.Health <= 0 then
            continue
        end

        local Role = char:FindFirstChild("Role")
        if not Role or Role.Value == "Survivor" then
            continue
        end

        if not Utils.Instance.FindFirstChild(char, "Effects.Undetectable", 0) then
            continue
        end

        local Root = Utils.Character.GetRootPart(char)
        if not Root then
            continue
        end

        local Distance = (Root.Position - self.LocalPlayerProps.HRP.Position).Magnitude
        if ClosestDistance > Distance then
            ClosestDistance = Distance
        end
    end

    self.InChase = if self.InChase then ClosestDistance <= Radiuses.L1 else ClosestDistance <= Radiuses.L4

    for r: number, s: Sound in self.LayerMagnitudes[self.LocalPlayerProps.Char.Name] do
        if s.Name == "L4" then
            self:TweenVolume(s, self.InChase and 0.5 or 0, delta)
        end
    end

    if self.InChase then
        self:PlayLayer4(self.LocalPlayerProps.Char.Name, delta)
    else
        self.Playing = false
    end
end

return TerrorRadiusManager


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Janitor = require(ReplicatedStorage.Packages.Janitor)

local CharacterAbilities = {
    CharacterModule = {}, --this is here as a utility local for possible usage
    Abilities = {},
}

function CharacterAbilities:Init()
    local janitorInstance = Janitor.new()
    janitorInstance:LinkToInstance(game:GetService("Players").LocalPlayer.Character)
    janitorInstance:Add(function()
        table.clear(CharacterAbilities.Abilities)
    end, true)
end

return CharacterAbilities


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

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

local EffectManager = {
    Effects = {},
    Janitor = nil,
}

function EffectManager:Init()
    local Char = Players.LocalPlayer.Character
    self.Janitor = Janitor.new()
    self.Janitor:LinkToInstance(Char)

    --visual
    self.Janitor:Add(RunService.PreSimulation:Connect(function(delta: number)
        for _, effect: Types.Effect in self.Effects do
            effect.TimeLeft -= delta
        end
    end))

    local Effects = Char:FindFirstChild("Effects")

    self.Janitor:Add(Effects.ChildAdded:Connect(function(EffectValue: Instance)
        if EffectValue:IsA("NumberValue") then
            while EffectValue:GetAttribute("Duration") == nil do
                task.wait()
            end

            self:AddEffect(Utils.Instance.GetEffectModule(EffectValue.Name, EffectValue:GetAttribute("Subfolder"), true), EffectValue.Value, EffectValue:GetAttribute("Duration"))
        end
    end))

    self.Janitor:Add(Effects.ChildRemoved:Connect(function(EffectValue: Instance)
        if EffectValue:IsA("NumberValue") then
            self:RemoveEffect(EffectValue.Name)
        end
    end))
end

function EffectManager:AddEffect(Effect: ModuleScript, level: number?, duration: number?)
    if self.Effects[Effect.Name] ~= nil then
        --too lazy to implement this
        -- if effect.ReplaceExisting and effect.Level >= self.Effects[effect.Name].Level then
        --     self.Effects[effect.Name].TimeLeft = self.Effects[effect.Name].Duration
        -- end
        return
    end

    local effect = Utils.Type.CopyTable(require(Effect))
    self.Effects[Effect.Name] = effect

    effect.TimeLeft = duration or effect.Duration
    effect.Level = level or effect.Level

    effect:Apply(level, Players.LocalPlayer.Character, duration or effect.Duration)
end

function EffectManager:RemoveEffect(effect: string)
    if self.Effects[effect] == nil then return end

    self.Effects[effect]:Remove(Players.LocalPlayer.Character)
    self.Effects[effect] = nil
end

return EffectManager


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Janitor = require(ReplicatedStorage.Packages.Janitor)

local Camera = workspace.CurrentCamera
local LocalPlayer = game:GetService("Players").LocalPlayer

local FOVManager = {
	FOVFactors = {},
	DeltaMultiplier = 4, --6.5
	CanUpdateFOV = true,
	Janitor = nil,
}

function FOVManager:Init()
	self.Janitor = Janitor.new()
	self.Janitor:LinkToInstance(LocalPlayer.Character)

	self.Janitor:Add(RunService.PreRender:Connect(function(delta)
		if self.CanUpdateFOV then
			local FOV = LocalPlayer:GetAttribute("BaseFOV") or 70

			for _, factor in self.FOVFactors do
				FOV *= factor
			end

			Camera.FieldOfView = math.lerp(
				Camera.FieldOfView,
				FOV,
				delta * self.DeltaMultiplier
			)
		end
	end))
end

function FOVManager:AddFOVFactor(factorName: string, factorMultiplier: number)
	self.FOVFactors[factorName] = factorMultiplier
end

function FOVManager:RemoveFOVFactor(factorName: string)
	self.FOVFactors[factorName] = nil
end

return FOVManager


return {
	Init = function(_)
		for _, Module in script.Parent:GetDescendants() do
			if Module == script then
				continue
			end
			if not Module:IsA("ModuleScript") then
				continue
			end

			local Scr = require(Module)
			if type(Scr) == "table" and Scr.Init and type(Scr.Init) == "function" then
				Scr:Init()
			end
		end
	end,
}


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

StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Health, false)
StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false)
StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.PlayerList, false)

local Utils = require(ReplicatedStorage.Modules.Utils)

--ORDER GUI HERE INSTEAD OF USING "DisplayOrder" SINCE IT'S AUTOMATICALLY APPLIED HERE
StarterGui = script.Parent

local GUIs = {
	"TemporaryUI",
	"EmotePanel",
	"Time",
	"PlayerStats",
	"Effects",
	"RoundPlayerList",
	"CharacterGUI",
	"VideoPlayer",
	"PlayerList",
	"Menus",
	"SideBar",
	"KillerIntros",
	"MoneyXPRewards",
	"RewardNotifications",
	"AFKLabel",
}

for index, name in GUIs do
	Utils.Instance.FindFirstChild(StarterGui, name).DisplayOrder = index
end


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

local Utils = require(ReplicatedStorage.Modules.Utils)

return {
    Init = function(_)
        Utils.Character.ObserveCharacter(Players.LocalPlayer, function(Char: Model)
            local Server = Char:FindFirstChild("Server")

            local AnimationManager = Char:FindFirstChild("AnimationManager")
            require(AnimationManager):Init()

            for _, Module in Char:GetDescendants() do
            	if not Module:IsA("ModuleScript") or Module == AnimationManager or Module:IsDescendantOf(Server) then
            		continue
            	end

            	debug.setmemorycategory(Module.Name)
            	local Scr = require(Module)
            	if type(Scr) == "table" and Scr["Init"] and type(Scr["Init"]) == "function" then
            		Scr:Init()
            	end
            end

            debug.setmemorycategory("Default")
        end)
    end,
}


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

local CharManager = {}

local LocalPlayer = Players.LocalPlayer

local AbilityGUI = require(ReplicatedStorage.Classes.Ability.AbilityGUI)
local Character = require(ReplicatedStorage.Classes.Character)
local Utils = require(ReplicatedStorage.Modules.Utils)

local AnimLoadPrevented = {
    "IdleAnimation",
    "WalkAnimation",
    "RunAnimation",
    "Execution",
}

function CharManager:Init()
    Utils.Character.ObserveCharacter(LocalPlayer, function(Char: Model)
        if Char:FindFirstChild("Role").Value ~= "Spectator" then
            CharManager.LoadCharacter(Char:GetAttribute("CharacterName"), Char.Role.Value, Char:GetAttribute("CharacterSkinName"))
        end
    end)
end

--- Initializes a character's client abilities.
function CharManager._InitCharacterAbilities(Module)
    local CharacterAbilities = require(LocalPlayer.Character.PlayerAbilities.CharacterAbilities)

    CharacterAbilities.CharacterModule = Module
    for _, Ability in Module.GameplayConfig.Abilities do
        CharacterAbilities.Abilities[Ability.Name or Ability] = Ability
        if Ability.Init then
            Ability:Init(Module, LocalPlayer)
        end
    end

    local function LoadAnim(name, anim)
        if typeof(anim) == "table" then
            for Name, Anim in anim do
                LoadAnim(Name, Anim)
            end
            return
        end

        if table.find(AnimLoadPrevented, name) then
            return
        end

        Module.GameplayConfig.Cache.Animations[name] = Utils.Character.LoadAnimationFromID(LocalPlayer.Character, anim, false)
    end

    for name, anim in Module.Config.AnimationIDs do
        LoadAnim(name, anim)
    end

    return CharacterAbilities
end

--- Loads a specific character's client stuff. The character itself is spawned in in `ServerScriptService.Managers.ServerCharacterManager`.
function CharManager.LoadCharacter(charName: string, charType: "Survivor" | "Killer", skinName: string?)
    task.delay(5, function()
        StarterGui:SetCore("ResetButtonCallback", true)
    end)

    local Char: Model = LocalPlayer.Character

    local CharModule = Utils.Type.CopyTable(require(Utils.Instance.GetCharacterModule(charType, charName, skinName)))
    CharModule.Owner = LocalPlayer

    Utils.Misc.PreloadAssets(CharModule.Config)

    --animation loading
    task.defer(function()
        local AnimationManager = require(Char:FindFirstChild("AnimationManager"))
        AnimationManager:LoadAnimation("Idle", CharModule.Config.AnimationIDs.IdleAnimation)
        AnimationManager:LoadAnimation("Walk", CharModule.Config.AnimationIDs.WalkAnimation)
        AnimationManager:LoadAnimation("Sprint", CharModule.Config.AnimationIDs.RunAnimation)

        for name, anim in CharModule.Config.AnimationIDs do
            local animCodeName = name:gsub("Animation", "")
            if table.find(Character.AutoLoadedAnims, animCodeName) or animCodeName:lower():find("execution") then
                continue
            end

            CharModule.GameplayConfig.Cache.Animations[name] = AnimationManager:LoadAnimation(name, anim)
        end
    end)

    Utils.Instance.FindFirstChild(Char, "PlayerAttributes.AnimationTransitionTime", 0).Value = CharModule.Config.AnimationTransitionTime

    local CharacterAbilities = CharManager._InitCharacterAbilities(CharModule)

    Char.Effects.ChildAdded:Connect(function(newChild)
        if newChild.Name == "Stunned" then
            if CharacterAbilities.Animations.Stunned then
                CharacterAbilities.Animations.Stunned:Play(0)
            end

            if CharacterAbilities.Animations.StunnedLoop then
                task.delay(CharacterAbilities.Animations.Stunned.Length - 0.1, function()
                    if CharacterAbilities.Animations.Stunned.IsPlaying then
                        CharacterAbilities.Animations.Stunned:Stop(0)
                    end

                    if not CharacterAbilities.Animations.StunnedEnd.IsPlaying then
                        CharacterAbilities.Animations.StunnedLoop:Play(0)
                    end
                end)
            end

            if CharacterAbilities.Animations.StunnedEnd then
                task.delay((newChild:GetAttribute("Duration") < 0.2 and 0.2 or newChild:GetAttribute("Duration")) - 0.2 - CharacterAbilities.Animations.StunnedEnd.Length, function()
                    if CharacterAbilities.Animations.Stunned then
                        CharacterAbilities.Animations.Stunned:Stop(0)
                    end

                    if CharacterAbilities.Animations.StunnedLoop then
                        CharacterAbilities.Animations.StunnedLoop:Stop(0)
                    end

                    CharacterAbilities.Animations.StunnedEnd:Play(0)
                end)
            end
        end
    end)

    AbilityGUI.InitGUI()

    if CharModule.OnInit then
        CharModule:OnInit(Char)
    end

    return Char
end

return CharManager


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

local Signal = require(ReplicatedStorage.Utils.Signal)

local InputManager = {
    --- WARNING: this won't change in server! Use `UIS.PreferredInput` instead!
    CurrentControlScheme = "Keyboard",
    --- WARNING: this won't fire in server! Use `UIS.PreferredInput` instead!
    SchemeChanged = Signal.new(),
}

function InputManager:Init()
    if RunService:IsServer() then
        return
    end

    --SETTING UP--
    local function ChangePreferred(value: Enum.UserInputType)
        local LastControlScheme = InputManager.CurrentControlScheme
        if value.Name:find("Gamepad") then
            InputManager.CurrentControlScheme = "Gamepad"
        elseif value == Enum.UserInputType.Touch then
            InputManager.CurrentControlScheme = "Touch"
        else
            InputManager.CurrentControlScheme = "Keyboard"
        end
        if InputManager.CurrentControlScheme ~= LastControlScheme then
            InputManager.SchemeChanged:Fire(InputManager.CurrentControlScheme)
        end
    end
    UserInputService.InputBegan:Connect(function(input: InputObject, gpe: boolean)
        if gpe then return end
        ChangePreferred(input.UserInputType)
    end)

    ChangePreferred(UserInputService:GetLastInputType())

    Players.LocalPlayer.CharacterAdded:Connect(function(_char: Model)
        for _, context in script.InputActions:GetChildren() do
            if not context:IsA("InputContext") then
                continue
            end
            context.Enabled = true

            for _, action in context:GetChildren() do
                if not action:IsA("InputAction") then
                    continue
                end

                action.Enabled = true
            end
        end
    end)
end

--making these functions to provide safe autocompletion, nothing else
function InputManager:GetInputContext(name: string): InputContext
    local Folder = script.InputActions:FindFirstChild(name)

    if not Folder:IsA("InputContext") then
        warn("[InputManager:GetInputAction()] Child \""..name.."\" isn't an input context!", debug.traceback())
        return
    end

    return Folder
end

function InputManager:GetInputAction(path: string): InputAction
    local Folder = script.InputActions
    local Steps = path:split(".")

    for _, Step in ipairs(Steps) do
        Folder = Folder:FindFirstChild(Step)
        if not Folder then
            break
        end
    end

    if not Folder:IsA("InputAction") then
        warn("[InputManager:GetInputAction()] Path \""..path.."\" isn't an input action!", debug.traceback())
        return
    end

    return Folder
end

function InputManager:GetInputBinding(path: string): InputBinding
    local Folder = script.InputActions
    local Steps = path:split(".")

    for _, Step in ipairs(Steps) do
        Folder = Folder:FindFirstChild(Step)
        if not Folder then
            break
        end
    end

    if not Folder:IsA("InputBinding") then
        warn("[InputManager:GetInputAction()] Path \""..path.."\" isn't an input binding!", debug.traceback())
        return
    end

    return Folder
end

return InputManager


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

return {
    Init = function(_)
        workspace.TempObjectFolders.Hitboxes.ChildAdded:Connect(function(Child)
            if Child:IsA("BasePart") and Utils.PlayerData.GetPlayerSetting(Players.LocalPlayer, "Advanced.ShowHitboxes") then
                Child.Material = Enum.Material.ForceField
                Child.Transparency = 0.65
            end
        end)
    end,
}


local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- local StarterGui = game:GetService("StarterGui")

local ItemGUI = require(ReplicatedStorage.Classes.Item.ItemGUI)
local Utils = require(ReplicatedStorage.Modules.Utils)

return {
    Init = function(_)
        Utils.Character.ObserveCharacter(Players.LocalPlayer, function(char: Model)
            local Role = char:FindFirstChild("Role")

            if Role.Value ~= "Spectator" then
                -- StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false)

                ItemGUI.InitGUI()

                -- return
            end

            -- StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, true)
        end)
    end,
}


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

local Utils = require(ReplicatedStorage.Modules.Utils)

return {
    Init = function(_self)
        Utils.Character.ObserveCharacter(Players.LocalPlayer, function(Character: Model)
            if Character:FindFirstChild("Role").Value == "Killer" then
                return
            end
            local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")

            local LastValue = Humanoid.Health
            Utils.Instance.ObserveProperty(Humanoid, "Health", function(value: number)
                if value >= LastValue or LastValue - value < 10 then
                    LastValue = value
                    return
                end

                Utils.Player.ShakeCamera(1.56 * (LastValue - value) / 100, 0.4)

                LastValue = value
            end)
        end)
    end,
}


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

local Types = require(ReplicatedStorage.Classes.Types)
local Utils = require(ReplicatedStorage.Modules.Utils)

return {
    Init = function(_self)
        Utils.Character.ObserveCharacter(Players.LocalPlayer, function(Character: Model, JanitorInstance: Types.Janitor)
            local Vignette = Utils.Instance.FindFirstChild(script, "DamageVignette"):Clone()
            Vignette.Parent = Utils.Instance.FindFirstChild(Players.LocalPlayer.PlayerGui, "TemporaryUI")

            local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")

            JanitorInstance:Add(RunService.PreRender:Connect(function(delta: number)
                local Target = Humanoid.Health < Humanoid.MaxHealth * 0.45 and
                    math.clamp(
                        Humanoid.Health / Humanoid.MaxHealth / 2 + 0.65,
                        0.5,
                        0.999
                    ) or 0.999
                Vignette.ImageTransparency = math.lerp(
                    Vignette.ImageTransparency,
                    Target,
                    delta * 9.8
                )
            end))

            --heartbeat
            JanitorInstance:Add(task.defer(function()
                while not JanitorInstance.CurrentlyCleaning and Humanoid and Humanoid.Parent do
                    if Humanoid.Health <= 0 or Humanoid.Health >= Humanoid.MaxHealth * 0.45 then
                        task.wait(0.05)
                        continue
                    end

                    local WaitTime = 0.15 + Humanoid.Health / Humanoid.MaxHealth / 2

                    Vignette.ImageTransparency *= 0.8
                    task.wait(WaitTime)
                    Vignette.ImageTransparency *= 0.8
                    task.wait(WaitTime * 2)
                end
            end))

            JanitorInstance:Add(function()
                if Vignette then
                    Vignette:Destroy()
                end
            end, true)
        end)
    end,
}


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

local Utils = require(ReplicatedStorage.Modules.Utils)

return {
    Init = function(_)
        Utils.Character.ObserveCharacter(Players.LocalPlayer, function(Character: Model)
            local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
            local DesaturationSetting = Utils.Instance.FindFirstChild(Players.LocalPlayer, "PlayerData.Settings.Customization.ScreenDesaturation")

            local Desat = Instance.new("ColorCorrectionEffect")
            Desat.Name = "HealthDesat"
            Desat.Saturation = 0
            Desat.Parent = Lighting

            local function GetExistingDesat(): number
                local Amount = 0

                for _, FX in Lighting:GetChildren() do
                    if FX:IsA("ColorCorrectionEffect") and FX.Name ~= "HealthDesat" and not FX:GetAttribute("IgnoredByHealthDesat") then
                        Amount += FX.Saturation
                    end
                end

                return Amount
            end
            local function Update(value: number)
                print(value)
                local Magnitude = Humanoid.Health / Humanoid.MaxHealth
                local Base = GetExistingDesat()
                local Mult = math.min(0, -1 - Base)
                if Magnitude <= 0.5 and DesaturationSetting.Value then
                    Desat.Saturation = Mult * (1 - Magnitude * 2)
                else
                    Desat.Saturation = 0
                end
            end

            Utils.Instance.ObserveProperty(Humanoid, "Health", Update)
            Update(Humanoid.Health)
        end)
    end,
}


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

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

local FootstepPlayer = {
	DefaultSFXID = "rbxassetid://78754179999047",
	ControlledCharacters = {},
}

function FootstepPlayer.PlayFootstep(player: Player, Anchor: BasePart)
	if Anchor:FindFirstChildWhichIsA("LinearVelocity") or Anchor.Parent:GetAttribute("Emoting") then return end

	Sounds.PlaySound(FootstepPlayer.ControlledCharacters[player.UserId] or FootstepPlayer.DefaultSFXID, {
		MinPitch = 0.9,
		MaxPitch = 1.05,
		Position = Anchor.Position - Vector3.new(0, 3, 0),
	})
end

function FootstepPlayer._SetupChar(player: Player, character: Model)
	local Humanoid = character:FindFirstChildWhichIsA("Humanoid")
	if not Humanoid then
		return
	end
	local HRP = Utils.Character.GetRootPart(character)
	if not HRP then
		return
	end

	if character:GetAttribute("CharacterName") then
		local CharModule = require(Utils.Instance.GetCharacterModule(character.Role.Value, character:GetAttribute("CharacterName"), character:GetAttribute("CharacterSkinName")))
		if CharModule.Config.Sounds.FootstepSounds then
			FootstepPlayer.ControlledCharacters[player.UserId] = CharModule.Config.Sounds.FootstepSounds
		end
	elseif FootstepPlayer.ControlledCharacters[player.UserId] then
		FootstepPlayer.ControlledCharacters[player.UserId] = nil
	end

	local ConnectedTracks = {}
	Humanoid:FindFirstChildWhichIsA("Animator").AnimationPlayed:Connect(function(track: AnimationTrack)
		if table.find(ConnectedTracks, track) then
			return
		end
		table.insert(ConnectedTracks, track)

		track:GetMarkerReachedSignal("Footstep"):Connect(function()
			if Humanoid.FloorMaterial == Enum.Material.Air or HRP.Anchored then
				return
			end
			FootstepPlayer.PlayFootstep(player, HRP)
		end)
	end)
end

function FootstepPlayer:Init()
	Utils.Player.ObservePlayers(function(plr: Player)
		Utils.Character.ObserveCharacter(plr, function(newChar: Model)
			FootstepPlayer._SetupChar(plr, newChar)
		end)
	end)
	Players.PlayerRemoving:Connect(function(plr: Player)
		if FootstepPlayer.ControlledCharacters[plr.UserId] then
			FootstepPlayer.ControlledCharacters[plr.UserId] = nil
		end
	end)
end

return FootstepPlayer


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

local Utils = require(ReplicatedStorage.Modules.Utils)

return {
    Init = function(_)
        Utils.Character.ObserveCharacter(Players.LocalPlayer, function()
            if Lighting:FindFirstChild("HealthDesat") then
                Lighting.HealthDesat:Destroy()
            end
        end)
    end,
}


local Debris = game:GetService("Debris")
local Lighting = game:GetService("Lighting")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")

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

return {
    Init = function(_)
        Network:SetConnection("KilledPlayer", "REMOTE_EVENT", function()
            local FX = Instance.new("ColorCorrectionEffect")
            FX.Name = "KillFX"

            local Epilepsy = Utils.PlayerData.GetPlayerSetting(Players.LocalPlayer, "Accessibility")

            FX.TintColor = Epilepsy and Color3.fromRGB(255, 100, 100) or Color3.fromRGB(255, 20, 20)
            FX.Contrast = Epilepsy and 0.5 or 2

            FX.Parent = Lighting

            TweenService:Create(FX, TweenInfo.new(1), {
                Contrast = 0,
                TintColor = Color3.fromRGB(255, 255, 255),
            }):Play()
            Debris:AddItem(FX, 1)
        end)
    end,
}


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

local Utils = require(ReplicatedStorage.Modules.Utils)

local LocalPlayer = Players.LocalPlayer

local QueryHitboxVisibility = {
    ShowOtherPlayersHitbox = false,
}

function QueryHitboxVisibility:Init()
    Utils.Player.ObservePlayers(function(Player: Player)
        if not self.ShowOtherPlayersHitbox and Player ~= Players.LocalPlayer then
            return
        end
        Utils.Character.ObserveCharacter(Player, function(char: Model)
            self._CharSetup(char)
        end)
    end)
end

function QueryHitboxVisibility._CharSetup(char: Model)
    task.defer(function()
        if not Utils.PlayerData.GetPlayerSetting(LocalPlayer, "Advanced.ShowPlayerHitboxes") then
            return
        end

        local Hitboxes = char:FindFirstChild("Hitboxes")
        if Hitboxes then
            for _, Hitbox in Hitboxes:GetChildren() do
                if not Hitbox:IsA("BasePart") then
                    continue
                end

                Hitbox.Transparency = Hitbox:GetAttribute("VisibleTransparency") or 0.75
            end
        end
    end)
end

return QueryHitboxVisibility


return {
    Init = function()
        -- ChildAdded is optimized as everything that isn't maps and players is added in folders inside it so it doesn't get called. -dys
        workspace.ChildAdded:Connect(function(newChild: Instance)
            if newChild.Name ~= "Map" then
                return
            end

            local Behaviour = newChild:FindFirstChild("Behaviour")
            if not Behaviour then
                return
            end

            require(workspace.Map.Behaviour):Init()
        end)
    end,
}


local SoundServiceResetter = {}

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

function SoundServiceResetter:Init()
    Utils.Character.ObserveCharacter(game:GetService("Players").LocalPlayer, function(_Character: Model)
        for _, Descendant in SoundService.SoundGroups:GetDescendants() do
            if not Descendant:IsA("SoundGroup") then
                Descendant:Destroy()
            end
        end
    end)
end

return SoundServiceResetter


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

local Utils = require(ReplicatedStorage.Modules.Utils)

local LERP_ALPHA = 0.25
local FOWARD_LEAN_ANGLE = math.rad(10)
local SIDE_LEAN_ANGLE = math.rad(35)

local TARGET_FPS = 60
local TARGET_DELTA = 1 / TARGET_FPS

local camera = workspace.CurrentCamera

return {
	HeadPreventionFactors = {},
	Init = function(self)
		Utils.Player.ObservePlayers(function(player: Player, playerJanitor)
			playerJanitor:Add(Utils.Character.ObserveHumanoid(player, function(humanoid: Humanoid, humanoidJanitor)
				local character = humanoid.Parent
				--TODO: support for r15 and customs
				if character:GetAttribute("CustomRig") or humanoid.RigType ~= Enum.HumanoidRigType.R6 then
					return
				end

				local rootPart = humanoid.RootPart
				local torso: BasePart = character:FindFirstChild("Torso") or character:FindFirstChild("LowerTorso")
				local head: BasePart = character:FindFirstChild("Head")

				local joints: {[string]: Motor6D} = {
					RootJoint = rootPart:FindFirstChild("RootJoint") or torso:FindFirstChild("Root"),
					Neck = torso:FindFirstChild("Neck") or head:FindFirstChild("Neck"),
					RightShoulder = torso:FindFirstChild("Right Shoulder") or Utils.Instance.FindFirstChild(character, "RightUpperArm.RightShoulder", 0),
					LeftShoulder = torso:FindFirstChild("Left Shoulder") or Utils.Instance.FindFirstChild(character, "LeftUpperArm.LeftShoulder", 0),
					RightHip = torso:FindFirstChild("Right Hip") or Utils.Instance.FindFirstChild(character, "RightUpperLeg.RightHip", 0),
					LeftHip = torso:FindFirstChild("Left Hip") or Utils.Instance.FindFirstChild(character, "LeftUpperLeg.LeftHip", 0),
				}

				local defaultC0s = {}
				local offsets = {}

				for name, joint in joints do
					defaultC0s[name] = joint.C0
					offsets[name] = CFrame.identity
				end

				local function lerpTilt(name: string, angles: {number}, deltaTimeSim: number)
					offsets[name] = offsets[name]:Lerp(CFrame.Angles(table.unpack(angles)), LERP_ALPHA * deltaTimeSim)
				end

				local Accumulator = 0
				humanoidJanitor:Add(RunService.PreSimulation:Connect(function(deltaTimeSim: number)
					Accumulator += deltaTimeSim
					if Accumulator < TARGET_DELTA then
						return
					end

					deltaTimeSim = Accumulator * 60
					Accumulator = 0

					-- NOTE: disabled this because of smooth shift lock, might find a workaround later -dys

					-- if not humanoid.AutoRotate then
					-- 	for name, joint in joints do
					-- 		if name == "Neck" then
					-- 			continue
					-- 		end
					-- 		joint.C0 = joint.C0:Lerp(defaultC0s[name], 0.1 * deltaTimeSim)
					-- 	end
					-- 	return
					-- end

					local moveDirection = rootPart.CFrame:VectorToObjectSpace(humanoid.MoveDirection)

					local x = math.clamp(moveDirection.X, -1, 1)
					local z = math.clamp(moveDirection.Z, -1, 1)

					local forwardLean = -z * FOWARD_LEAN_ANGLE
					local sideLean = -x * SIDE_LEAN_ANGLE
					-- isBackward = z > 0.5
					local hipTiltFactor = z > 0.5 and 1 or -1

					for name, angles in {
						RootJoint = {forwardLean, 0, sideLean * -hipTiltFactor},
						Neck = {-forwardLean * 0.5, 0, -sideLean * 0.5},
						Shoulder = {0, x * math.rad(10), 0},
						Hip = {0, hipTiltFactor * x * math.rad(20), 0},
					} do

						if name:lower() == "hip" or name:lower() == "shoulder" then
							for _, side in {"Left", "Right"} do
								lerpTilt(side..name, angles, deltaTimeSim)
							end

							continue
						end

						lerpTilt(name, angles, deltaTimeSim)
					end

					for name, joint in joints do
						if name == "Neck" then
							continue
						end
						joint.C0 = defaultC0s[name] * offsets[name]
					end

					if player ~= Players.LocalPlayer then
						return
					end

					local cameraLook = camera.CFrame.LookVector
					local torsoLook = torso.CFrame.LookVector
					local headPos = head.Position
					local targetPos = headPos + cameraLook * 10

					local dist = (headPos - targetPos).Magnitude
					local diff = head.Position.Y - targetPos.Y

					local finalx = (((headPos - targetPos).Unit):Cross(torsoLook)).Y * -1
					local y = (math.atan(diff / dist) / 2)

					joints.Neck.C0 = Utils.Type.GetCountOfDict(self.HeadPreventionFactors) <= 0 and joints.Neck.C0:Lerp(defaultC0s["Neck"] * CFrame.Angles(y, 0, -finalx), LERP_ALPHA * deltaTimeSim) or defaultC0s["Neck"]
				end))
			end))
		end)
	end,
	AddHeadPreventionFactor = function(self, name: string)
		self.HeadPreventionFactors[name] = true
	end,
	RemoveHeadPreventionFactor = function(self, name: string)
		self.HeadPreventionFactors[name] = nil
	end,
}


local PlayerModule = script.Parent.PlayerModule

local Player = game:GetService("Players").LocalPlayer

local PlayerInitter = {
    Loaded = false,
    CurrentlyInitting = nil,
    Initting = false,
}

function PlayerInitter:Init()
    if game:GetService("RunService"):IsServer() or self.Initting then
        return
    end

    self.Initting = true

    Player:SetAttribute("BaseFOV", 70)

    for _, Module in script.Parent:GetDescendants() do
        if not (Module:IsA("ModuleScript") and Module ~= script and Module ~= PlayerModule and not Module:IsDescendantOf(PlayerModule)) then
            continue
    	end

        if Module.Name ~= "ModPanel" then
            self.CurrentlyInitting = Module.Name
        end
        debug.setmemorycategory(Module.Name)
    	local Scr = require(Module)
    	if type(Scr) == "table" and Scr.Init and type(Scr.Init) == "function" then
    		Scr:Init()
    	end
    end

    debug.setmemorycategory("Default")

    self.Loaded = true
end

return PlayerInitter


--[[
	BaseOcclusion - Abstract base class for character occlusion control modules
	2018 Camera Update - AllYourBlox
--]]

--[[ The Module ]]--
local BaseOcclusion: any = {}
BaseOcclusion.__index = BaseOcclusion
setmetatable(BaseOcclusion, {
	__call = function(_, ...)
		return BaseOcclusion.new(...)
	end
})

function BaseOcclusion.new()
	local self = setmetatable({}, BaseOcclusion)
	return self
end

-- Called when character is added
function BaseOcclusion:CharacterAdded(char: Model, player: Player)
end

-- Called when character is about to be removed
function BaseOcclusion:CharacterRemoving(char: Model, player: Player)
end

function BaseOcclusion:OnCameraSubjectChanged(newSubject)
end

--[[ Derived classes are required to override and implement all of the following functions ]]--
function BaseOcclusion:GetOcclusionMode(): Enum.DevCameraOcclusionMode?
	-- Must be overridden in derived classes to return an Enum.DevCameraOcclusionMode value
	warn("BaseOcclusion GetOcclusionMode must be overridden by derived classes")
	return nil
end

function BaseOcclusion:Enable(enabled: boolean)
	warn("BaseOcclusion Enable must be overridden by derived classes")
end

function BaseOcclusion:Update(dt: number, desiredCameraCFrame: CFrame, desiredCameraFocus: CFrame)
	warn("BaseOcclusion Update must be overridden by derived classes")
	return desiredCameraCFrame, desiredCameraFocus
end

return BaseOcclusion


--!nonstrict
local ContextActionService = game:GetService("ContextActionService")
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local VRService = game:GetService("VRService")

local player = Players.LocalPlayer

local CAMERA_INPUT_PRIORITY = Enum.ContextActionPriority.Medium.Value
local MB_TAP_LENGTH = 0.3 -- (s) length of time for a short mouse button tap to be registered

local ROTATION_SPEED_KEYS = math.rad(120) -- (rad/s)
local ROTATION_SPEED_MOUSE = Vector2.new(1, 0.77)*math.rad(0.5) -- (rad/s)
local ROTATION_SPEED_POINTERACTION = Vector2.new(1, 0.77)*math.rad(7) -- (rad/s)
local ROTATION_SPEED_TOUCH = Vector2.new(1, 0.66)*math.rad(1) -- (rad/s)
local ROTATION_SPEED_GAMEPAD = Vector2.new(1, 0.77)*math.rad(4) -- (rad/s)

local ZOOM_SPEED_MOUSE = 1 -- (scaled studs/wheel click)
local ZOOM_SPEED_KEYS = 0.1 -- (studs/s)
local ZOOM_SPEED_TOUCH = 0.04 -- (scaled studs/DIP %)

local MIN_TOUCH_SENSITIVITY_FRACTION = 0.25 -- 25% sensitivity at 90°

local FFlagUserResetTouchStateOnMenuOpen
do
	local success, result = pcall(function()
		return UserSettings():IsUserFeatureEnabled("UserResetTouchStateOnMenuOpen")
	end)
	FFlagUserResetTouchStateOnMenuOpen = success and result
end

-- right mouse button up & down events
local rmbDown, rmbUp do
	local rmbDownBindable = Instance.new("BindableEvent")
	local rmbUpBindable = Instance.new("BindableEvent")

	rmbDown = rmbDownBindable.Event
	rmbUp = rmbUpBindable.Event

	UserInputService.InputBegan:Connect(function(input, gpe)
		if not gpe and input.UserInputType == Enum.UserInputType.MouseButton2 then
			rmbDownBindable:Fire()
		end
	end)

	UserInputService.InputEnded:Connect(function(input, gpe)
		if input.UserInputType == Enum.UserInputType.MouseButton2 then
			rmbUpBindable:Fire()
		end
	end)
end

local thumbstickCurve do
	local K_CURVATURE = 2 -- amount of upwards curvature (0 is flat)
	local K_DEADZONE = 0.1 -- deadzone

	function thumbstickCurve(x)
		-- remove sign, apply linear deadzone
		local fDeadzone = (math.abs(x) - K_DEADZONE)/(1 - K_DEADZONE)

		-- apply exponential curve and scale to fit in [0, 1]
		local fCurve = (math.exp(K_CURVATURE*fDeadzone) - 1)/(math.exp(K_CURVATURE) - 1)

		-- reapply sign and clamp
		return math.sign(x)*math.clamp(fCurve, 0, 1)
	end
end

-- Adjust the touch sensitivity so that sensitivity is reduced when swiping up
-- or down, but stays the same when swiping towards the middle of the screen
local function adjustTouchPitchSensitivity(delta: Vector2): Vector2
	local camera = workspace.CurrentCamera

	if not camera then
		return delta
	end

	-- get the camera pitch in world space
	local pitch = camera.CFrame:ToEulerAnglesYXZ()

	if delta.Y*pitch >= 0 then
		-- do not reduce sensitivity when pitching towards the horizon
		return delta
	end

	-- set up a line to fit:
	-- 1 = f(0)
	-- 0 = f(±pi/2)
	local curveY = 1 - (2*math.abs(pitch)/math.pi)^0.75

	-- remap curveY from [0, 1] -> [MIN_TOUCH_SENSITIVITY_FRACTION, 1]
	local sensitivity = curveY*(1 - MIN_TOUCH_SENSITIVITY_FRACTION) + MIN_TOUCH_SENSITIVITY_FRACTION

	return Vector2.new(1, sensitivity)*delta
end

local function isInDynamicThumbstickArea(pos: Vector3): boolean
	local playerGui = player:FindFirstChildWhichIsA("PlayerGui")
	local touchGui = playerGui and playerGui:FindFirstChild("TouchGui")
	local touchFrame = touchGui and touchGui:FindFirstChild("TouchControlFrame")
	local thumbstickFrame = touchFrame and touchFrame:FindFirstChild("DynamicThumbstickFrame")

	if not thumbstickFrame then
		return false
	end

	if not touchGui.Enabled then
		return false
	end

	local posTopLeft = thumbstickFrame.AbsolutePosition
	local posBottomRight = posTopLeft + thumbstickFrame.AbsoluteSize

	return
		pos.X >= posTopLeft.X and
		pos.Y >= posTopLeft.Y and
		pos.X <= posBottomRight.X and
		pos.Y <= posBottomRight.Y
end

local worldDt = 1/60
RunService.Stepped:Connect(function(_, _worldDt)
	worldDt = _worldDt
end)

local CameraInput = {}

do
	local connectionList = {}
	local panInputCount = 0

	local function incPanInputCount()
		panInputCount = math.max(0, panInputCount + 1)
	end

	local function decPanInputCount()
		panInputCount = math.max(0, panInputCount - 1)
	end

	local function resetPanInputCount()
		panInputCount = 0
	end

	local gamepadState = {
		Thumbstick2 = Vector2.new(),
	}
	local keyboardState = {
		Left = 0,
		Right = 0,
		I = 0,
		O = 0
	}
	local mouseState = {
		Movement = Vector2.new(),
		Wheel = 0, -- PointerAction
		Pan = Vector2.new(), -- PointerAction
		Pinch = 0, -- PointerAction
	}
	local touchState = {
		Move = Vector2.new(),
		Pinch = 0,
	}

	local gamepadZoomPressBindable = Instance.new("BindableEvent")
	CameraInput.gamepadZoomPress = gamepadZoomPressBindable.Event

	local gamepadResetBindable = VRService.VREnabled and Instance.new("BindableEvent") or nil
	if VRService.VREnabled then
		CameraInput.gamepadReset = gamepadResetBindable.Event
	end

	function CameraInput.getRotationActivated(): boolean
		return panInputCount > 0 or gamepadState.Thumbstick2.Magnitude > 0
	end

	function CameraInput.getRotation(disableKeyboardRotation: boolean?): Vector2
		local inversionVector = Vector2.new(1, UserGameSettings:GetCameraYInvertValue())

		-- keyboard input is non-coalesced, so must account for time delta
		local kKeyboard = Vector2.new(keyboardState.Right - keyboardState.Left, 0)*worldDt
		local kGamepad = gamepadState.Thumbstick2
		local kMouse = mouseState.Movement
		local kPointerAction = mouseState.Pan
		local kTouch = adjustTouchPitchSensitivity(touchState.Move)

		if disableKeyboardRotation then
			kKeyboard = Vector2.new()
		end

		local result =
			kKeyboard*ROTATION_SPEED_KEYS +
			kGamepad*ROTATION_SPEED_GAMEPAD +
			kMouse*ROTATION_SPEED_MOUSE +
			kPointerAction*ROTATION_SPEED_POINTERACTION +
			kTouch*ROTATION_SPEED_TOUCH

		return result*inversionVector
	end

	function CameraInput.getZoomDelta(): number
		local kKeyboard = keyboardState.O - keyboardState.I
		local kMouse = -mouseState.Wheel + mouseState.Pinch
		local kTouch = -touchState.Pinch
		return kKeyboard*ZOOM_SPEED_KEYS + kMouse*ZOOM_SPEED_MOUSE + kTouch*ZOOM_SPEED_TOUCH
	end

	do
		local function thumbstick(action, state, input)
			local position = input.Position
			gamepadState[input.KeyCode.Name] = Vector2.new(thumbstickCurve(position.X), -thumbstickCurve(position.Y))
			return Enum.ContextActionResult.Pass
		end

		local function mouseMovement(input)
			local delta = input.Delta
			mouseState.Movement = Vector2.new(delta.X, delta.Y)
		end

		local function keypress(action, state, input)
			keyboardState[input.KeyCode.Name] = state == Enum.UserInputState.Begin and 1 or 0
		end

		local function gamepadZoomPress(action, state, input)
			if state == Enum.UserInputState.Begin then
				gamepadZoomPressBindable:Fire()
			end
		end

		local function gamepadReset(action, state, input)
			if state == Enum.UserInputState.Begin then
				gamepadResetBindable:Fire()
			end
		end

		local function resetInputDevices()
			for _, device in pairs({
				gamepadState,
				keyboardState,
				mouseState,
				touchState,
			}) do
				for k, v in pairs(device) do
					if type(v) == "boolean" then
						device[k] = false
					else
						device[k] *= 0 -- Mul by zero to preserve vector types
					end
				end
			end
		end

		local touchBegan, touchChanged, touchEnded, resetTouchState do
			-- Use TouchPan & TouchPinch when they work in the Studio emulator

			local touches: {[InputObject]: boolean?} = {} -- {[InputObject] = sunk}
			local dynamicThumbstickInput: InputObject? -- Special-cased
			local lastPinchDiameter: number?

			function touchBegan(input: InputObject, sunk: boolean)
				assert(input.UserInputType == Enum.UserInputType.Touch)
				assert(input.UserInputState == Enum.UserInputState.Begin)

				if dynamicThumbstickInput == nil and isInDynamicThumbstickArea(input.Position) and not sunk then
					-- any finger down starting in the dynamic thumbstick area should always be
					-- ignored for camera purposes. these must be handled specially from all other
					-- inputs, as the DT does not sink inputs by itself
					dynamicThumbstickInput = input
					return
				end

				if not sunk then
					incPanInputCount()
				end

				-- register the finger
				touches[input] = sunk
			end

			function touchEnded(input: InputObject, sunk: boolean)
				assert(input.UserInputType == Enum.UserInputType.Touch)
				assert(input.UserInputState == Enum.UserInputState.End)

				-- reset the DT input
				if input == dynamicThumbstickInput then
					dynamicThumbstickInput = nil
				end

				-- reset pinch state if one unsunk finger lifts
				if touches[input] == false then
					lastPinchDiameter = nil
					decPanInputCount()
				end

				-- unregister input
				touches[input] = nil
			end

			function touchChanged(input, sunk)
				assert(input.UserInputType == Enum.UserInputType.Touch)
				assert(input.UserInputState == Enum.UserInputState.Change)

				-- ignore movement from the DT finger
				if input == dynamicThumbstickInput then
					return
				end

				-- fixup unknown touches
				if touches[input] == nil then
					touches[input] = sunk
				end

				-- collect unsunk touches
				local unsunkTouches = {}
				for touch, sunk in pairs(touches) do
					if not sunk then
						table.insert(unsunkTouches, touch)
					end
				end

				-- 1 finger: pan
				if #unsunkTouches == 1 then
					if touches[input] == false then
						local delta = input.Delta
						touchState.Move += Vector2.new(delta.X, delta.Y) -- total touch pan movement (reset at end of frame)
					end
				end

				-- 2 fingers: pinch
				if #unsunkTouches == 2 then
					local pinchDiameter = (unsunkTouches[1].Position - unsunkTouches[2].Position).Magnitude

					if lastPinchDiameter then
						touchState.Pinch += pinchDiameter - lastPinchDiameter
					end

					lastPinchDiameter = pinchDiameter
				else
					lastPinchDiameter = nil
				end
			end

			function resetTouchState()
				touches = {}
				dynamicThumbstickInput = nil
				lastPinchDiameter = nil
				if FFlagUserResetTouchStateOnMenuOpen then
					resetPanInputCount()
				end
			end
		end

		local function pointerAction(wheel, pan, pinch, gpe)
			if not gpe then
				mouseState.Wheel = wheel
				mouseState.Pan = pan
				mouseState.Pinch = -pinch
			end
		end

		local function inputBegan(input, sunk)
			if input.UserInputType == Enum.UserInputType.Touch then
				touchBegan(input, sunk)

			elseif input.UserInputType == Enum.UserInputType.MouseButton2 and not sunk then
				incPanInputCount()
			end
		end

		local function inputChanged(input, sunk)
			if input.UserInputType == Enum.UserInputType.Touch then
				touchChanged(input, sunk)

			elseif input.UserInputType == Enum.UserInputType.MouseMovement then
				mouseMovement(input)
			end
		end

		local function inputEnded(input, sunk)
			if input.UserInputType == Enum.UserInputType.Touch then
				touchEnded(input, sunk)

			elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
				decPanInputCount()
			end
		end

		local inputEnabled = false

		function CameraInput.setInputEnabled(_inputEnabled)
			if inputEnabled == _inputEnabled then
				return
			end
			inputEnabled = _inputEnabled

			resetInputDevices()
			resetTouchState()

			if inputEnabled then -- enable
				ContextActionService:BindActionAtPriority(
					"RbxCameraThumbstick",
					thumbstick,
					false,
					CAMERA_INPUT_PRIORITY,
					Enum.KeyCode.Thumbstick2
				)

				ContextActionService:BindActionAtPriority(
					"RbxCameraKeypress",
					keypress,
					false,
					CAMERA_INPUT_PRIORITY,
					Enum.KeyCode.Left,
					Enum.KeyCode.Right,
					Enum.KeyCode.I,
					Enum.KeyCode.O
				)

				if VRService.VREnabled then
					ContextActionService:BindAction(
						"RbxCameraGamepadReset",
						gamepadReset,
						false,
						Enum.KeyCode.ButtonL3
					)
				end

				ContextActionService:BindAction(
					"RbxCameraGamepadZoom",
					gamepadZoomPress,
					false,
					Enum.KeyCode.ButtonR3
				)

				table.insert(connectionList, UserInputService.InputBegan:Connect(inputBegan))
				table.insert(connectionList, UserInputService.InputChanged:Connect(inputChanged))
				table.insert(connectionList, UserInputService.InputEnded:Connect(inputEnded))
				table.insert(connectionList, UserInputService.PointerAction:Connect(pointerAction))
				if FFlagUserResetTouchStateOnMenuOpen then
					local GuiService = game:GetService("GuiService")
					table.insert(connectionList, GuiService.MenuOpened:connect(resetTouchState))
				end

			else -- disable
				ContextActionService:UnbindAction("RbxCameraThumbstick")
				ContextActionService:UnbindAction("RbxCameraMouseMove")
				ContextActionService:UnbindAction("RbxCameraMouseWheel")
				ContextActionService:UnbindAction("RbxCameraKeypress")

				ContextActionService:UnbindAction("RbxCameraGamepadZoom")
				if VRService.VREnabled then
					ContextActionService:UnbindAction("RbxCameraGamepadReset")
				end

				for _, conn in pairs(connectionList) do
					conn:Disconnect()
				end
				connectionList = {}
			end
		end

		function CameraInput.getInputEnabled()
			return inputEnabled
		end

		function CameraInput.resetInputForFrameEnd()
			mouseState.Movement = Vector2.new()
			touchState.Move = Vector2.new()
			touchState.Pinch = 0

			mouseState.Wheel = 0 -- PointerAction
			mouseState.Pan = Vector2.new() -- PointerAction
			mouseState.Pinch = 0 -- PointerAction
		end

		UserInputService.WindowFocused:Connect(resetInputDevices)
		UserInputService.WindowFocusReleased:Connect(resetInputDevices)
	end
end

-- Toggle pan
do
	local holdPan = false
	local togglePan = false
	local lastRmbDown = 0 -- tick() timestamp of the last right mouse button down event

	function CameraInput.getHoldPan(): boolean
		return holdPan
	end

	function CameraInput.getTogglePan(): boolean
		return togglePan
	end

	function CameraInput.getPanning(): boolean
		return togglePan or holdPan
	end

	function CameraInput.setTogglePan(value: boolean)
		togglePan = value
	end

	local cameraToggleInputEnabled = false
	local rmbDownConnection
	local rmbUpConnection

	function CameraInput.enableCameraToggleInput()
		if cameraToggleInputEnabled then
			return
		end
		cameraToggleInputEnabled = true

		holdPan = false
		togglePan = false

		if rmbDownConnection then
			rmbDownConnection:Disconnect()
		end

		if rmbUpConnection then
			rmbUpConnection:Disconnect()
		end

		rmbDownConnection = rmbDown:Connect(function()
			holdPan = true
			lastRmbDown = tick()
		end)

		rmbUpConnection = rmbUp:Connect(function()
			holdPan = false
			if tick() - lastRmbDown < MB_TAP_LENGTH and (togglePan or UserInputService:GetMouseDelta().Magnitude < 2) then
				togglePan = not togglePan
			end
		end)
	end

	function CameraInput.disableCameraToggleInput()
		if not cameraToggleInputEnabled then
			return
		end
		cameraToggleInputEnabled = false

		if rmbDownConnection then
			rmbDownConnection:Disconnect()
			rmbDownConnection = nil
		end

		if rmbUpConnection then
			rmbUpConnection:Disconnect()
			rmbUpConnection = nil
		end
	end
end

return CameraInput


local Input = require(script.Parent:WaitForChild("CameraInput"))
local CameraUI = require(script.Parent:WaitForChild("CameraUI"))
local CameraUtils = require(script.Parent:WaitForChild("CameraUtils"))

local lastTogglePan = false
local lastTogglePanChange = tick()

local CROSS_MOUSE_ICON = "rbxasset://textures/Cursors/CrossMouseIcon.png"

local lockStateDirty = false
local wasTogglePanOnTheLastTimeYouWentIntoFirstPerson = false
local lastFirstPerson = false

CameraUI.setCameraModeToastEnabled(false)

return function(isFirstPerson: boolean)
	local togglePan = Input.getTogglePan()
	local toastTimeout = 3

	if isFirstPerson and togglePan ~= lastTogglePan then
		lockStateDirty = true
	end

	if lastTogglePan ~= togglePan or tick() - lastTogglePanChange > toastTimeout then
		local doShow = togglePan and tick() - lastTogglePanChange < toastTimeout

		CameraUI.setCameraModeToastOpen(doShow)

		if togglePan then
			lockStateDirty = false
		end
		lastTogglePanChange = tick()
		lastTogglePan = togglePan
	end

	if isFirstPerson ~= lastFirstPerson then
		if isFirstPerson then
			wasTogglePanOnTheLastTimeYouWentIntoFirstPerson = Input.getTogglePan()
			Input.setTogglePan(true)
		elseif not lockStateDirty then
			Input.setTogglePan(wasTogglePanOnTheLastTimeYouWentIntoFirstPerson)
		end
	end

	if isFirstPerson then
		if Input.getTogglePan() then
			CameraUtils.setMouseIconOverride(CROSS_MOUSE_ICON)
			CameraUtils.setMouseBehaviorOverride(Enum.MouseBehavior.LockCenter)
			CameraUtils.setRotationTypeOverride(Enum.RotationType.CameraRelative)
		else
			CameraUtils.restoreMouseIcon()
			CameraUtils.restoreMouseBehavior()
			CameraUtils.setRotationTypeOverride(Enum.RotationType.CameraRelative)
		end

	elseif Input.getTogglePan() then
		CameraUtils.setMouseIconOverride(CROSS_MOUSE_ICON)
		CameraUtils.setMouseBehaviorOverride(Enum.MouseBehavior.LockCenter)
		CameraUtils.setRotationTypeOverride(Enum.RotationType.MovementRelative)

	elseif Input.getHoldPan() then
		CameraUtils.restoreMouseIcon()
		CameraUtils.setMouseBehaviorOverride(Enum.MouseBehavior.LockCurrentPosition)
		CameraUtils.setRotationTypeOverride(Enum.RotationType.MovementRelative)

	else
		CameraUtils.restoreMouseIcon()
		CameraUtils.restoreMouseBehavior()
		CameraUtils.restoreRotationType()
	end

	lastFirstPerson = isFirstPerson
end


--!nonstrict
local StarterGui = game:GetService("StarterGui")

local initialized = false

local CameraUI: any = {}

--- Instantaneously disable the toast or enable for opening later on. Used when switching camera modes.
function CameraUI.setCameraModeToastEnabled(enabled: boolean)
	if not enabled and not initialized then
		return
	end

	if not initialized then
		initialized = true
	end

	if not enabled then
		CameraUI.setCameraModeToastOpen(false)
	end
end

function CameraUI.setCameraModeToastOpen(open: boolean)
	assert(initialized)

	if open then
		StarterGui:SetCore("SendNotification", {
			Title = "Camera Control Enabled",
			Text = "Right click to toggle",
			Duration = 3,
		})
	end
end

return CameraUI


--!nonstrict
--[[
	Invisicam - Occlusion module that makes objects occluding character view semi-transparent
	2018 Camera Update - AllYourBlox
--]]

--[[ Top Level Roblox Services ]]--
local PlayersService = game:GetService("Players")

--[[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local USE_STACKING_TRANSPARENCY = true	-- Multiple items between the subject and camera get transparency values that add up to TARGET_TRANSPARENCY
local TARGET_TRANSPARENCY = 0.75 -- Classic Invisicam's Value, also used by new invisicam for parts hit by head and torso rays
local TARGET_TRANSPARENCY_PERIPHERAL = 0.5 -- Used by new SMART_CIRCLE mode for items not hit by head and torso rays

local MODE = {
	--CUSTOM = 1, 		-- Retired, unused
	LIMBS = 2, 			-- Track limbs
	MOVEMENT = 3, 		-- Track movement
	CORNERS = 4, 		-- Char model corners
	CIRCLE1 = 5, 		-- Circle of casts around character
	CIRCLE2 = 6, 		-- Circle of casts around character, camera relative
	LIMBMOVE = 7, 		-- LIMBS mode + MOVEMENT mode
	SMART_CIRCLE = 8, 	-- More sample points on and around character
	CHAR_OUTLINE = 9,	-- Dynamic outline around the character
}

local LIMB_TRACKING_SET = {
	-- Body parts common to R15 and R6
	['Head'] = true,

	-- Body parts unique to R6
	['Left Arm'] = true,
	['Right Arm'] = true,
	['Left Leg'] = true,
	['Right Leg'] = true,

	-- Body parts unique to R15
	['LeftLowerArm'] = true,
	['RightLowerArm'] = true,
	['LeftUpperLeg'] = true,
	['RightUpperLeg'] = true
}

local CORNER_FACTORS = {
	Vector3.new(1,1,-1),
	Vector3.new(1,-1,-1),
	Vector3.new(-1,-1,-1),
	Vector3.new(-1,1,-1)
}

local CIRCLE_CASTS = 10
local MOVE_CASTS = 3
local SMART_CIRCLE_CASTS = 24
local SMART_CIRCLE_INCREMENT = 2.0 * math.pi / SMART_CIRCLE_CASTS
local CHAR_OUTLINE_CASTS = 24

-- Used to sanitize user-supplied functions
local function AssertTypes(param, ...)
	local allowedTypes = {}
	local typeString = ''
	for _, typeName in pairs({...}) do
		allowedTypes[typeName] = true
		typeString = typeString .. (typeString == '' and '' or ' or ') .. typeName
	end
	local theType = type(param)
	assert(allowedTypes[theType], typeString .. " type expected, got: " .. theType)
end

-- Helper function for Determinant of 3x3, not in CameraUtils for performance reasons
local function Det3x3(a: number,b: number,c: number,d: number,e: number,f: number,g: number,h: number,i: number): number
	return (a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g))
end

-- Smart Circle mode needs the intersection of 2 rays that are known to be in the same plane
-- because they are generated from cross products with a common vector. This function is computing
-- that intersection, but it's actually the general solution for the point halfway between where
-- two skew lines come nearest to each other, which is more forgiving.
local function RayIntersection(p0: Vector3, v0: Vector3, p1: Vector3, v1: Vector3): Vector3
	local v2 = v0:Cross(v1)
	local d1 = p1.X - p0.X
	local d2 = p1.Y - p0.Y
	local d3 = p1.Z - p0.Z
	local denom = Det3x3(v0.X,-v1.X,v2.X,v0.Y,-v1.Y,v2.Y,v0.Z,-v1.Z,v2.Z)

	if (denom == 0) then
		return ZERO_VECTOR3 -- No solution (rays are parallel)
	end

	local t0 = Det3x3(d1,-v1.X,v2.X,d2,-v1.Y,v2.Y,d3,-v1.Z,v2.Z) / denom
	local t1 = Det3x3(v0.X,d1,v2.X,v0.Y,d2,v2.Y,v0.Z,d3,v2.Z) / denom
	local s0 = p0 + t0 * v0
	local s1 = p1 + t1 * v1
	local s = s0 + 0.5 * ( s1 - s0 )

	-- 0.25 studs is a threshold for deciding if the rays are
	-- close enough to be considered intersecting, found through testing
	if (s1-s0).Magnitude < 0.25 then
		return s
	else
		return ZERO_VECTOR3
	end
end

--[[ The Module ]]--
local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion"))
local Invisicam = setmetatable({}, BaseOcclusion)
Invisicam.__index = Invisicam

function Invisicam.new()
	local self = setmetatable(BaseOcclusion.new(), Invisicam)

	self.char = nil
	self.humanoidRootPart = nil
	self.torsoPart = nil
	self.headPart = nil

	self.childAddedConn = nil
	self.childRemovedConn = nil

	self.behaviors = {} 	-- Map of modes to behavior fns
	self.behaviors[MODE.LIMBS] = self.LimbBehavior
	self.behaviors[MODE.MOVEMENT] = self.MoveBehavior
	self.behaviors[MODE.CORNERS] = self.CornerBehavior
	self.behaviors[MODE.CIRCLE1] = self.CircleBehavior
	self.behaviors[MODE.CIRCLE2] = self.CircleBehavior
	self.behaviors[MODE.LIMBMOVE] = self.LimbMoveBehavior
	self.behaviors[MODE.SMART_CIRCLE] = self.SmartCircleBehavior
	self.behaviors[MODE.CHAR_OUTLINE] = self.CharacterOutlineBehavior

	self.mode = MODE.SMART_CIRCLE
	self.behaviorFunction = self.SmartCircleBehavior

	self.savedHits = {} 	-- Objects currently being faded in/out
	self.trackedLimbs = {}	-- Used in limb-tracking casting modes

	self.camera = game.Workspace.CurrentCamera

	self.enabled = false
	return self
end

function Invisicam:Enable(enable)
	self.enabled = enable

	if not enable then
		self:Cleanup()
	end
end

function Invisicam:GetOcclusionMode()
	return Enum.DevCameraOcclusionMode.Invisicam
end

--[[ Module functions ]]--
function Invisicam:LimbBehavior(castPoints)
	for limb, _ in pairs(self.trackedLimbs) do
		castPoints[#castPoints + 1] = limb.Position
	end
end

function Invisicam:MoveBehavior(castPoints)
	for i = 1, MOVE_CASTS do
		local position: Vector3, velocity: Vector3 = self.humanoidRootPart.Position, self.humanoidRootPart.Velocity
		local horizontalSpeed: number = Vector3.new(velocity.X, 0, velocity.Z).Magnitude / 2
		local offsetVector: Vector3 = (i - 1) * self.humanoidRootPart.CFrame.lookVector :: Vector3 * horizontalSpeed
		castPoints[#castPoints + 1] = position + offsetVector
	end
end

function Invisicam:CornerBehavior(castPoints)
	local cframe: CFrame = self.humanoidRootPart.CFrame
	local centerPoint: Vector3 = cframe.Position
	local rotation = cframe - centerPoint
	local halfSize = self.char:GetExtentsSize() / 2 --NOTE: Doesn't update w/ limb animations
	castPoints[#castPoints + 1] = centerPoint
	for i = 1, #CORNER_FACTORS do
		castPoints[#castPoints + 1] = centerPoint + (rotation * (halfSize * CORNER_FACTORS[i]))
	end
end

function Invisicam:CircleBehavior(castPoints)
	local cframe: CFrame
	if self.mode == MODE.CIRCLE1 then
		cframe = self.humanoidRootPart.CFrame
	else
		local camCFrame: CFrame = self.camera.CoordinateFrame
		cframe = camCFrame - camCFrame.Position + self.humanoidRootPart.Position
	end
	castPoints[#castPoints + 1] = cframe.Position
	for i = 0, CIRCLE_CASTS - 1 do
		local angle = (2 * math.pi / CIRCLE_CASTS) * i
		local offset = 3 * Vector3.new(math.cos(angle), math.sin(angle), 0)
		castPoints[#castPoints + 1] = cframe * offset
	end
end

function Invisicam:LimbMoveBehavior(castPoints)
	self:LimbBehavior(castPoints)
	self:MoveBehavior(castPoints)
end

function Invisicam:CharacterOutlineBehavior(castPoints)
	local torsoUp = self.torsoPart.CFrame.upVector.unit
	local torsoRight = self.torsoPart.CFrame.rightVector.unit

	-- Torso cross of points for interior coverage
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
	if self.headPart then
		castPoints[#castPoints + 1] = self.headPart.CFrame.p
	end

	local cframe = CFrame.new(ZERO_VECTOR3,Vector3.new(self.camera.CoordinateFrame.lookVector.X,0,self.camera.CoordinateFrame.lookVector.Z))
	local centerPoint = (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)

	local partsWhitelist = {self.torsoPart}
	if self.headPart then
		partsWhitelist[#partsWhitelist + 1] = self.headPart
	end

	for i = 1, CHAR_OUTLINE_CASTS do
		local angle = (2 * math.pi * i / CHAR_OUTLINE_CASTS)
		local offset = cframe * (3 * Vector3.new(math.cos(angle), math.sin(angle), 0))

		offset = Vector3.new(offset.X, math.max(offset.Y, -2.25), offset.Z)

		local ray = Ray.new(centerPoint + offset, -3 * offset)
		local hit, hitPoint = game.Workspace:FindPartOnRayWithWhitelist(ray, partsWhitelist, false)

		if hit then
			-- Use hit point as the cast point, but nudge it slightly inside the character so that bumping up against
			-- walls is less likely to cause a transparency glitch
			castPoints[#castPoints + 1] = hitPoint + 0.2 * (centerPoint - hitPoint).unit
		end
	end
end

function Invisicam:SmartCircleBehavior(castPoints)
	local torsoUp = self.torsoPart.CFrame.upVector.unit
	local torsoRight = self.torsoPart.CFrame.rightVector.unit

	-- SMART_CIRCLE mode includes rays to head and 5 to the torso.
	-- Hands, arms, legs and feet are not included since they
	-- are not canCollide and can therefore go inside of parts
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
	castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
	if self.headPart then
		castPoints[#castPoints + 1] = self.headPart.CFrame.p
	end

	local cameraOrientation = self.camera.CFrame - self.camera.CFrame.p
	local torsoPoint = Vector3.new(0,0.5,0) + (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)
	local radius = 2.5

	-- This loop first calculates points in a circle of radius 2.5 around the torso of the character, in the
	-- plane orthogonal to the camera's lookVector. Each point is then raycast to, to determine if it is within
	-- the free space surrounding the player (not inside anything). Two iterations are done to adjust points that
	-- are inside parts, to try to move them to valid locations that are still on their camera ray, so that the
	-- circle remains circular from the camera's perspective, but does not cast rays into walls or parts that are
	-- behind, below or beside the character and not really obstructing view of the character. This minimizes
	-- the undesirable situation where the character walks up to an exterior wall and it is made invisible even
	-- though it is behind the character.
	for i = 1, SMART_CIRCLE_CASTS do
		local angle = SMART_CIRCLE_INCREMENT * i - 0.5 * math.pi
		local offset = radius * Vector3.new(math.cos(angle), math.sin(angle), 0)
		local circlePoint = torsoPoint + cameraOrientation * offset

		-- Vector from camera to point on the circle being tested
		local vp = circlePoint - self.camera.CFrame.p

		local ray = Ray.new(torsoPoint, circlePoint - torsoPoint)
		local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
		local castPoint = circlePoint

		if hit then
			local hprime = hp + 0.1 * hitNormal.unit -- Slightly offset hit point from the hit surface
			local v0 = hprime - torsoPoint -- Vector from torso to offset hit point

			local perp = (v0:Cross(vp)).unit

			-- Vector from the offset hit point, along the hit surface
			local v1 = (perp:Cross(hitNormal)).unit

			-- Vector from camera to offset hit
			local vprime = (hprime - self.camera.CFrame.p).unit

			-- This dot product checks to see if the vector along the hit surface would hit the correct
			-- side of the invisicam cone, or if it would cross the camera look vector and hit the wrong side
			if ( v0.unit:Dot(-v1) < v0.unit:Dot(vprime)) then
				castPoint = RayIntersection(hprime, v1, circlePoint, vp)

				if castPoint.Magnitude > 0 then
					local SRay = Ray.new(hprime, castPoint - hprime)
					local Hit, hitPoint, HitNormal = game.Workspace:FindPartOnRayWithIgnoreList(SRay, {self.char}, false, false )

					if Hit then
						local hprime2 = hitPoint + 0.1 * HitNormal.unit
						castPoint = hprime2
					end
				else
					castPoint = hprime
				end
			else
				castPoint = hprime
			end

			local SRay = Ray.new(torsoPoint, (castPoint - torsoPoint))
			local Hit, hitPoint, _hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(SRay, {self.char}, false, false )

			if Hit then
				local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit
				castPoint = castPoint2
			end
		end

		castPoints[#castPoints + 1] = castPoint
	end
end

function Invisicam:CheckTorsoReference()
	if self.char then
		self.torsoPart = self.char:FindFirstChild("Torso")
		if not self.torsoPart then
			self.torsoPart = self.char:FindFirstChild("UpperTorso")
			if not self.torsoPart then
				self.torsoPart = self.char:FindFirstChild("HumanoidRootPart")
			end
		end

		self.headPart = self.char:FindFirstChild("Head")
	end
end

function Invisicam:CharacterAdded(char: Model, player: Player)
	-- We only want the LocalPlayer's character
	if player~=PlayersService.LocalPlayer then return end

	if self.childAddedConn then
		self.childAddedConn:Disconnect()
		self.childAddedConn = nil
	end
	if self.childRemovedConn then
		self.childRemovedConn:Disconnect()
		self.childRemovedConn = nil
	end

	self.char = char

	self.trackedLimbs = {}
	local function childAdded(child)
		if child:IsA("BasePart") then
			if LIMB_TRACKING_SET[child.Name] then
				self.trackedLimbs[child] = true
			end

			if child.Name == "Torso" or child.Name == "UpperTorso" then
				self.torsoPart = child
			end

			if child.Name == "Head" then
				self.headPart = child
			end
		end
	end

	local function childRemoved(child)
		self.trackedLimbs[child] = nil

		-- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use
		self:CheckTorsoReference()
	end

	self.childAddedConn = char.ChildAdded:Connect(childAdded)
	self.childRemovedConn = char.ChildRemoved:Connect(childRemoved)
	for _, child in pairs(self.char:GetChildren()) do
		childAdded(child)
	end
end

function Invisicam:SetMode(newMode: number)
	AssertTypes(newMode, 'number')
	for _, modeNum in pairs(MODE) do
		if modeNum == newMode then
			self.mode = newMode
			self.behaviorFunction = self.behaviors[self.mode]
			return
		end
	end
	error("Invalid mode number")
end

function Invisicam:GetObscuredParts()
	return self.savedHits
end

-- Want to turn off Invisicam? Be sure to call this after.
function Invisicam:Cleanup()
	for hit, originalFade in pairs(self.savedHits) do
		hit.LocalTransparencyModifier = originalFade
	end
end

function Invisicam:Update(dt: number, desiredCameraCFrame: CFrame, desiredCameraFocus: CFrame): (CFrame, CFrame)
	-- Bail if there is no Character
	if not self.enabled or not self.char then
		return desiredCameraCFrame, desiredCameraFocus
	end

	self.camera = game.Workspace.CurrentCamera

	-- TODO: Move this to a GetHumanoidRootPart helper, probably combine with CheckTorsoReference
	-- Make sure we still have a HumanoidRootPart
	if not self.humanoidRootPart then
		local humanoid = self.char:FindFirstChildWhichIsA("Humanoid")
		if humanoid and humanoid.RootPart then
			self.humanoidRootPart = humanoid.RootPart
		else
			-- Not set up with Humanoid? Try and see if there's one in the Character at all:
			self.humanoidRootPart = self.char:FindFirstChild("HumanoidRootPart")
			if not self.humanoidRootPart then
				-- Bail out, since we're relying on HumanoidRootPart existing
				return desiredCameraCFrame, desiredCameraFocus
			end
		end

		-- TODO: Replace this with something more sensible
		local ancestryChangedConn
		ancestryChangedConn = self.humanoidRootPart.AncestryChanged:Connect(function(child, parent)
			if child == self.humanoidRootPart and not parent then
				self.humanoidRootPart = nil
				if ancestryChangedConn and ancestryChangedConn.Connected then
					ancestryChangedConn:Disconnect()
					ancestryChangedConn = nil
				end
			end
		end)
	end

	if not self.torsoPart then
		self:CheckTorsoReference()
		if not self.torsoPart then
			-- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso
			return desiredCameraCFrame, desiredCameraFocus
		end
	end

	-- Make a list of world points to raycast to
	local castPoints = {}
	self.behaviorFunction(self, castPoints)

	-- Cast to get a list of objects between the camera and the cast points
	local currentHits = {}
	local ignoreList = {self.char}
	local function add(hit)
		currentHits[hit] = true
		if not self.savedHits[hit] then
			self.savedHits[hit] = hit.LocalTransparencyModifier
		end
	end

	local hitParts
	local hitPartCount = 0

	-- Hash table to treat head-ray-hit parts differently than the rest of the hit parts hit by other rays
	-- head/torso ray hit parts will be more transparent than peripheral parts when USE_STACKING_TRANSPARENCY is enabled
	local headTorsoRayHitParts = {}

	local perPartTransparencyHeadTorsoHits = TARGET_TRANSPARENCY
	local perPartTransparencyOtherHits = TARGET_TRANSPARENCY

	if USE_STACKING_TRANSPARENCY then

		-- This first call uses head and torso rays to find out how many parts are stacked up
		-- for the purpose of calculating required per-part transparency
		local headPoint = self.headPart and self.headPart.CFrame.p or castPoints[1]
		local torsoPoint = self.torsoPart and self.torsoPart.CFrame.p or castPoints[2]
		hitParts = self.camera:GetPartsObscuringTarget({headPoint, torsoPoint}, ignoreList)

		-- Count how many things the sample rays passed through, including decals. This should only
		-- count decals facing the camera, but GetPartsObscuringTarget does not return surface normals,
		-- so my compromise for now is to just let any decal increase the part count by 1. Only one
		-- decal per part will be considered.
		for i = 1, #hitParts do
			local hitPart = hitParts[i]
			hitPartCount = hitPartCount + 1 -- count the part itself
			headTorsoRayHitParts[hitPart] = true
			for _, child in pairs(hitPart:GetChildren()) do
				if child:IsA('Decal') or child:IsA('Texture') then
					hitPartCount = hitPartCount + 1 -- count first decal hit, then break
					break
				end
			end
		end

		if (hitPartCount > 0) then
			perPartTransparencyHeadTorsoHits = math.pow( ((0.5 * TARGET_TRANSPARENCY) + (0.5 * TARGET_TRANSPARENCY / hitPartCount)), 1 / hitPartCount )
			perPartTransparencyOtherHits = math.pow( ((0.5 * TARGET_TRANSPARENCY_PERIPHERAL) + (0.5 * TARGET_TRANSPARENCY_PERIPHERAL / hitPartCount)), 1 / hitPartCount )
		end
	end

	-- Now get all the parts hit by all the rays
	hitParts = self.camera:GetPartsObscuringTarget(castPoints, ignoreList)

	local partTargetTransparency = {}

	-- Include decals and textures
	for i = 1, #hitParts do
		local hitPart = hitParts[i]

		partTargetTransparency[hitPart] =headTorsoRayHitParts[hitPart] and perPartTransparencyHeadTorsoHits or perPartTransparencyOtherHits

		-- If the part is not already as transparent or more transparent than what invisicam requires, add it to the list of
		-- parts to be modified by invisicam
		if hitPart.Transparency < partTargetTransparency[hitPart] then
			add(hitPart)
		end

		-- Check all decals and textures on the part
		for _, child in pairs(hitPart:GetChildren()) do
			if child:IsA('Decal') or child:IsA('Texture') then
				if (child.Transparency < partTargetTransparency[hitPart]) then
					partTargetTransparency[child] = partTargetTransparency[hitPart]
					add(child)
				end
			end
		end
	end

	-- Invisibilize objects that are in the way, restore those that aren't anymore
	for hitPart, originalLTM in pairs(self.savedHits) do
		if currentHits[hitPart] then
			-- LocalTransparencyModifier gets whatever value is required to print the part's total transparency to equal perPartTransparency
			hitPart.LocalTransparencyModifier = (hitPart.Transparency < 1) and ((partTargetTransparency[hitPart] - hitPart.Transparency) / (1.0 - hitPart.Transparency)) or 0
		else -- Restore original pre-invisicam value of LTM
			hitPart.LocalTransparencyModifier = originalLTM
			self.savedHits[hitPart] = nil
		end
	end

	-- Invisicam does not change the camera values
	return desiredCameraCFrame, desiredCameraFocus
end

return Invisicam


--!nonstrict
--[[
	LegacyCamera - Implements legacy controller types: Attach, Fixed, Watch
	2018 Camera Update - AllYourBlox
--]]

local PITCH_LIMIT = math.rad(80)

local CameraInput = require(script.Parent:WaitForChild("CameraInput"))

--[[ Services ]]--
local PlayersService = game:GetService('Players')

--[[ The Module ]]--
local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
local LegacyCamera = setmetatable({}, BaseCamera)
LegacyCamera.__index = LegacyCamera

function LegacyCamera.new()
	local self = setmetatable(BaseCamera.new(), LegacyCamera)

	self.cameraType = Enum.CameraType.Fixed
	self.lastUpdate = tick()
	self.lastDistanceToSubject = nil

	return self
end

function LegacyCamera:GetModuleName()
	return "LegacyCamera"
end

--[[ Functions overridden from BaseCamera ]]--
function LegacyCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
	return BaseCamera.SetCameraToSubjectDistance(self,desiredSubjectDistance)
end

function LegacyCamera:Update(dt: number): (CFrame?, CFrame?)

	-- Cannot update until cameraType has been set
	if not self.cameraType then
		return nil, nil
	end

	local now = tick()
	local timeDelta = (now - self.lastUpdate)
	local camera = 	workspace.CurrentCamera
	local newCameraCFrame = camera.CFrame
	local newCameraFocus = camera.Focus
	local player = PlayersService.LocalPlayer

	if self.lastUpdate == nil or timeDelta > 1 then
		self.lastDistanceToSubject = nil
	end
	local subjectPosition: Vector3 = self:GetSubjectPosition()

	if self.cameraType == Enum.CameraType.Fixed then
		if subjectPosition and player and camera then
			local distanceToSubject = self:GetCameraToSubjectDistance()
			local newLookVector = self:CalculateNewLookVectorFromArg(nil, CameraInput.getRotation())

			newCameraFocus = camera.Focus -- Fixed camera does not change focus
			newCameraCFrame = CFrame.new(camera.CFrame.p, camera.CFrame.p + (distanceToSubject * newLookVector))
		end

	elseif self.cameraType == Enum.CameraType.Attach then
		local subjectCFrame = self:GetSubjectCFrame()
		local cameraPitch = camera.CFrame:ToEulerAnglesYXZ()
		local _, subjectYaw = subjectCFrame:ToEulerAnglesYXZ()

		cameraPitch = math.clamp(cameraPitch - CameraInput.getRotation().Y, -PITCH_LIMIT, PITCH_LIMIT)

		newCameraFocus = CFrame.new(subjectCFrame.p)*CFrame.fromEulerAnglesYXZ(cameraPitch, subjectYaw, 0)
		newCameraCFrame = newCameraFocus*CFrame.new(0, 0, self:StepZoom())

	elseif self.cameraType == Enum.CameraType.Watch then
		if subjectPosition and player and camera then
			local cameraLook = nil

			if subjectPosition == camera.CFrame.p then
				warn("Camera cannot watch subject in same position as itself")
				return camera.CFrame, camera.Focus
			end

			local humanoid = self:GetHumanoid()
			if humanoid and humanoid.RootPart then
				local diffVector = subjectPosition - camera.CFrame.p
				cameraLook = diffVector.unit

				if self.lastDistanceToSubject and self.lastDistanceToSubject == self:GetCameraToSubjectDistance() then
					-- Don't clobber the zoom if they zoomed the camera
					local newDistanceToSubject = diffVector.magnitude
					self:SetCameraToSubjectDistance(newDistanceToSubject)
				end
			end

			local distanceToSubject: number = self:GetCameraToSubjectDistance()
			local newLookVector: Vector3 = self:CalculateNewLookVectorFromArg(cameraLook, CameraInput.getRotation())

			newCameraFocus = CFrame.new(subjectPosition)
			newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition)

			self.lastDistanceToSubject = distanceToSubject
		end
	else
		-- Unsupported type, return current values unchanged
		return camera.CFrame, camera.Focus
	end

	self.lastUpdate = now
	return newCameraCFrame, newCameraFocus
end

return LegacyCamera