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

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

--TODO: add displayed achievements of choice
local PlayerInfoWindow = {}
PlayerInfoWindow.__index = PlayerInfoWindow

function PlayerInfoWindow.New(Player: Player, Parent: ScreenGui)
    local Window = setmetatable({
        PlayerShown = Player,
        Instance = script:FindFirstChild("PlayerInfoWindow"):Clone(),
        Connections = {},
        Removed = Signal.new(),
    }, PlayerInfoWindow)
    Window.Instance.Size = Utils.UDim.Zero

    --user portrait
    task.spawn(function()
        Window.Instance.Portrait.PortraitLabel.Image = Players:GetUserThumbnailAsync(Player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size352x352)
    end)

    --time played
    task.spawn(function()
        local HiddenTime = Utils.Instance.FindFirstChild(Player, "PlayerData.Settings.Privacy.HidePlaytime")
        local TimePlayed = Utils.PlayerData.GetPlayerStat(Player, "General.TimePlayed", false)

        Window.Instance.TimePlayed.TimeLabel.Text = HiddenTime.Value and "(Hidden)" or Utils.Math.ConvertToDHMS(TimePlayed.Value)
        Window.Instance.TimePlayed.TimeLabel.TextColor3 = HiddenTime.Value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)

        table.insert(Window.Connections, TimePlayed.Changed:Connect(function(value: number)
            Window.Instance.TimePlayed.TimeLabel.Text = HiddenTime.Value and "(Hidden)" or Utils.Math.ConvertToDHMS(value)
            Window.Instance.TimePlayed.TimeLabel.TextColor3 = HiddenTime.Value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)
        end))

        table.insert(Window.Connections, HiddenTime.Changed:Connect(function(value: boolean)
            Window.Instance.TimePlayed.TimeLabel.Text = value and "(Hidden)" or Utils.Math.ConvertToDHMS(TimePlayed.Value)
            Window.Instance.TimePlayed.TimeLabel.TextColor3 = value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)
        end))
    end)

    --surv wins
    task.spawn(function()
        local HiddenWins = Utils.Instance.FindFirstChild(Player, "PlayerData.Settings.Privacy.HideSurvivorWins")
        local SurvivorWins = Utils.PlayerData.GetPlayerStat(Player, "SurvivorStats.SurvivorWins", false)

        Window.Instance.Wins.Survivor.Amount.Text = HiddenWins.Value and "(Hidden)" or tostring(SurvivorWins.Value)
        Window.Instance.Wins.Survivor.Amount.TextColor3 = HiddenWins.Value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)

        table.insert(Window.Connections, SurvivorWins.Changed:Connect(function(value: number)
            Window.Instance.Wins.Survivor.Amount.Text = HiddenWins.Value and "(Hidden)" or tostring(value)
            Window.Instance.Wins.Survivor.Amount.TextColor3 = HiddenWins.Value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)
        end))

        table.insert(Window.Connections, HiddenWins.Changed:Connect(function(value: boolean)
            Window.Instance.Wins.Survivor.Amount.Text = value and "(Hidden)" or tostring(SurvivorWins.Value)
            Window.Instance.Wins.Survivor.Amount.TextColor3 = value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)
        end))
    end)
    --killer wins
    task.spawn(function()
        local HiddenWins = Utils.Instance.FindFirstChild(Player, "PlayerData.Settings.Privacy.HideKillerWins")
        local KillerWins = Utils.PlayerData.GetPlayerStat(Player, "KillerStats.KillerWins", false)

        Window.Instance.Wins.Killer.Amount.Text = HiddenWins.Value and "(Hidden)" or tostring(KillerWins.Value)
        Window.Instance.Wins.Killer.Amount.TextColor3 = HiddenWins.Value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)

        table.insert(Window.Connections, KillerWins.Changed:Connect(function(value: number)
            Window.Instance.Wins.Killer.Amount.Text = HiddenWins.Value and "(Hidden)" or tostring(value)
            Window.Instance.Wins.Killer.Amount.TextColor3 = HiddenWins.Value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)
        end))

        table.insert(Window.Connections, HiddenWins.Changed:Connect(function(value: boolean)
            Window.Instance.Wins.Killer.Amount.Text = value and "(Hidden)" or tostring(KillerWins.Value)
            Window.Instance.Wins.Killer.Amount.TextColor3 = value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)
        end))
    end)

    --equipped survivor
    task.spawn(function()
        local EquippedSurvivor = Utils.PlayerData.GetPlayerEquipped(Player, "Survivor", false)

        local LastSurvivor = ""

        local SkinConn

        local function Reload(Value: string)
            if LastSurvivor ~= Value then
                if SkinConn then
                    SkinConn:Disconnect()
                end
                local SkinValue = Utils.PlayerData.GetPlayerEquipped(Player, "Skins."..Value, false)

                local function ReloadSkin(Skin: string)
                    if #Skin <= 0 then
                        local Default = require(Utils.Instance.GetCharacterModule("Survivor", Value))

                        Window.Instance.CharactersUsing.Survivor.PortraitLabel.SkinLabel.Text = "Default"
                        Window.Instance.CharactersUsing.Survivor.PortraitLabel.PortraitImage.Image = Default.Config.Render

                        return
                    end

                    local SkinMod = require(Utils.Instance.GetCharacterModule("Survivor", Value, Skin))

                    Window.Instance.CharactersUsing.Survivor.PortraitLabel.SkinLabel.Text = SkinMod.Config.Name
                    Window.Instance.CharactersUsing.Survivor.PortraitLabel.PortraitImage.Image = SkinMod.Config.Render
                end

                SkinConn = SkinValue.Changed:Connect(ReloadSkin)
                table.insert(Window.Connections, SkinConn)
                ReloadSkin(SkinValue.Value)
            end

            Window.Instance.CharactersUsing.Survivor.NameLabel.Text = require(Utils.Instance.GetCharacterModule("Survivor", Value)).Config.Name
        end

        EquippedSurvivor.Changed:Connect(Reload)
        Reload(EquippedSurvivor.Value)
    end)
    --equipped killer
    task.spawn(function()
        local EquippedKiller = Utils.PlayerData.GetPlayerEquipped(Player, "Killer", false)

        local LastKiller = ""

        local SkinConn

        local function Reload(Value: string)
            if LastKiller ~= Value then
                if SkinConn then
                    SkinConn:Disconnect()
                end
                local SkinValue = Utils.PlayerData.GetPlayerEquipped(Player, "Skins."..Value, false)

                local function ReloadSkin(Skin: string)
                    if #Skin <= 0 then
                        local Default = require(Utils.Instance.GetCharacterModule("Killer", Value))

                        Window.Instance.CharactersUsing.Killer.PortraitLabel.SkinLabel.Text = "Default"
                        Window.Instance.CharactersUsing.Killer.PortraitLabel.PortraitImage.Image = Default.Config.Render

                        return
                    end

                    local SkinMod = require(Utils.Instance.GetCharacterModule("Killer", Value, Skin))

                    Window.Instance.CharactersUsing.Killer.PortraitLabel.SkinLabel.Text = SkinMod.Config.Name
                    Window.Instance.CharactersUsing.Killer.PortraitLabel.PortraitImage.Image = SkinMod.Config.Render
                end

                SkinConn = SkinValue.Changed:Connect(ReloadSkin)
                table.insert(Window.Connections, SkinConn)
                ReloadSkin(SkinValue.Value)
            end

            Window.Instance.CharactersUsing.Killer.NameLabel.Text = require(Utils.Instance.GetCharacterModule("Killer", Value)).Config.Name
        end

        EquippedKiller.Changed:Connect(Reload)
        Reload(EquippedKiller.Value)
    end)

    Window.Instance.NameContainer.PlayerName.Text = "@"..Player.Name
    Window.Instance.NameContainer.PlayerDisplayName.Text = Player.DisplayName

    Window.Instance.Parent = Parent

    local HideButton = Window.Instance:FindFirstChild("Hide")
    table.insert(Window.Connections, HideButton.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
    end))
    table.insert(Window.Connections, HideButton.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
    end))
    table.insert(Window.Connections, HideButton.MouseButton1Click:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
        Window:Hide()
    end))

    Window:Show()

    return Window
end

function PlayerInfoWindow:Show()
    TweenService:Create(self.Instance, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.334, 0.463)}):Play()
end

function PlayerInfoWindow:Hide(instant: boolean?)
    if instant == nil then
        instant = false
    end
    self.Removed:Fire()
    self.Removed:DisconnectAll()
    for _, connection in self.Connections do
        if typeof(connection) == "thread" then
            if coroutine.status(connection :: thread) ~= "running" then
                task.cancel(connection)
            end
        else
            connection:Disconnect()
        end
    end
    if not instant then
        TweenService:Create(self.Instance, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play()
    else
        self.Instance.Size = Utils.UDim.Zero
    end
    Debris:AddItem(self.Instance, 0.5)
end

return PlayerInfoWindow


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

local Utils = require(ReplicatedStorage.Modules.Utils)
local Sounds = require(ReplicatedStorage.Modules.Sounds)
local InputManager = require(script.Parent.Parent.InputManager)
local PlayerInfoWindow = require(script.PlayerInfoWindow)

local PlayerList = {}

local LocalPlayer = Players.LocalPlayer

function PlayerList:Init()
    local UI = Utils.Instance.FindFirstChild(script, "PlayerList")
    UI.Parent = Utils.Instance.FindFirstChild(LocalPlayer.PlayerGui, "PlayerList")

    local PlayerPrefab = Utils.Instance.FindFirstChild(script, "PlayerContainer")

    local ToggleButton = Utils.Instance.FindFirstChild(UI, "Topbar.ToggleButton")

    local CurrentPlayerWindow

    local Open = true
    local function Toggle(Force: boolean?)
        Open = Force ~= nil and Force or not Open
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})

        ToggleButton.Text = Open and ">" or "<"
        TweenService:Create(UI, TweenInfo.new(0.245, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {Position = UDim2.fromScale(Open and 1 or 1.148, 0.065)}):Play()
    end

    local function SetupPlayer(Player: Player)
        local Container = PlayerPrefab:Clone()
        if Player == LocalPlayer then
            Container.LayoutOrder = -1
        end
        Container.Name = tostring(Player.UserId)

        --name
        Container.PlayerLabel.Text = Player.DisplayName

        --money
        local HiddenMoney = Utils.Instance.FindFirstChild(Player, "PlayerData.Settings.Privacy.HideMoney", 5)
        local MoneyValue = Utils.Instance.FindFirstChild(Player, "leaderstats.Money", 5)

        Container.MoneyLabel.Text = HiddenMoney.Value and "(Hidden)" or tostring(MoneyValue.Value)
        Container.MoneyLabel.TextColor3 = HiddenMoney.Value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)

        MoneyValue.Changed:Connect(function(value: number)
            Container.MoneyLabel.Text = HiddenMoney.Value and "(Hidden)" or tostring(value)
            Container.MoneyLabel.TextColor3 = HiddenMoney.Value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)
        end)
        HiddenMoney.Changed:Connect(function(value: boolean)
            Container.MoneyLabel.Text = value and "(Hidden)" or tostring(MoneyValue.Value)
            Container.MoneyLabel.TextColor3 = value and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(255, 255, 255)
        end)

        --malice
        local MaliceValue = Utils.Instance.FindFirstChild(Player, "leaderstats.Malice")
        Container.MaliceLabel.Text = tostring(MaliceValue.Value)
        MaliceValue.Changed:Connect(function(value: number)
            Container.MaliceLabel.Text = tostring(value)
        end)

        Container.MouseEnter:Connect(function()
            Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
        end)
        Container.MouseLeave:Connect(function()
            Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
        end)
        Container.MouseButton1Click:Connect(function()
            Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
            if CurrentPlayerWindow then
                local ClickedSame = CurrentPlayerWindow.PlayerShown == Player
                CurrentPlayerWindow:Hide()
                if ClickedSame then
                    return
                end
            end

            CurrentPlayerWindow = PlayerInfoWindow.New(Player, UI.Parent)
            CurrentPlayerWindow.Removed:Connect(function()
                CurrentPlayerWindow = nil
            end)
        end)

        Container.Parent = UI.Content
    end

    Utils.Character.ObserveCharacter(LocalPlayer, function(Char: Model)
        local Role = Char:FindFirstChild("Role")
        if Role.Value ~= "Spectator" then
            if CurrentPlayerWindow then
                CurrentPlayerWindow:Hide(true)
            end

            UI.Visible = false
        else
            UI.Visible = true
        end
    end)

    InputManager:GetInputAction("Miscellaneous.TogglePlayerList").Pressed:Connect(function()
        Toggle()
    end)
    ToggleButton.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
    end)
    ToggleButton.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
    end)
    ToggleButton.MouseButton1Click:Connect(function()
        Toggle()
    end)

    Utils.Player.ObservePlayers(SetupPlayer)
    Players.PlayerRemoving:Connect(function(Player: Player)
        if UI.Content:FindFirstChild(tostring(Player.UserId)) then
            UI.Content[tostring(Player.UserId)]:Destroy()
        end
    end)
end

return PlayerList


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

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

local RewardNotifications = {}

function RewardNotifications:Init()
    local Container = Utils.Instance.FindFirstChild(script, "RewardNotificationContainer")
    Container.Parent = Utils.Instance.FindFirstChild(Players.LocalPlayer.PlayerGui, "RewardNotifications")

    local Prefab = Utils.Instance.FindFirstChild(script, "RewardNotification")
    local AchievementPrefab = Utils.Instance.FindFirstChild(script, "AchievementNotification")

    Network:SetConnection("ShowRewardNotification", "REMOTE_EVENT", function(Module: ModuleScript | string, Type: string)
        Type = Type or "QuotedGrantedSkin"

        local TypeLower = Type:lower()

        local IsAchievement = TypeLower:find("achievement") ~= nil

        local Card = (IsAchievement and AchievementPrefab or Prefab):Clone()
        local Info = IsAchievement and Utils.Instance.GetAchievementData(Module) or require(Module)

        Card.ImageContainer.Render.Image = IsAchievement and (Info.Icon or "rbxasset://textures/ui/GuiImagePlaceholder.png") or Info.Config.Render

        local Text

        if IsAchievement then
            Text = `"{Info.Title}" achievement obtained!`
        else
            Text = (TypeLower:find("bought") and "Bought" or "Received").." \""..tostring(Info.Config.Name).."\""

            if TypeLower:find("emote") then
                Text = Text.." emote"
            elseif TypeLower:find("skin") then
                Text = Text.." skin"
            elseif TypeLower:find("character") then
                Text = Text.." character"
            end
        end

        Card.TextContainer.RewardLabel.Text = Text

        local Quote = not IsAchievement and Info.Config.Quote or Info.Description
        if TypeLower:find("quoted") then
            Quote = "\""..Quote.."\""
        end

        Card.TextContainer.RewardQuote.Text = Quote

        Card.Size = Utils.UDim.Zero
        Card.Parent = Container
        TweenService:Create(Card, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {Size = UDim2.fromScale(1, 0.2)}):Play()
        task.wait(5)
        TweenService:Create(Card, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.In), {Size = Utils.UDim.Zero}):Play()
        task.wait(1)
        Card:Destroy()
    end)
end

return RewardNotifications


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

local Utils = require(ReplicatedStorage.Modules.Utils)

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

local RoundPlayerList = {
    UI = nil,
    Conns = {},
}

local TemplateFolder = Utils.Instance.FindFirstChild(script, "Templates")
local Prefabs = {
    UI = Utils.Instance.FindFirstChild(script, "RoundPlayerListUI"),
    Card = Utils.Instance.FindFirstChild(TemplateFolder, "Card"),
}
local UIParent = Utils.Instance.FindFirstChild(LocalPlayer.PlayerGui, "RoundPlayerList")

function RoundPlayerList:Init()
    Utils.Character.ObserveCharacter(LocalPlayer, function(Char: Model)
        if RoundPlayerList.UI then
            RoundPlayerList.UI:Destroy()
        end
        for _, ConnectionList in RoundPlayerList.Conns do
            for _, Connection in ConnectionList do
                Connection:Disconnect()
            end
        end
        RoundPlayerList.Conns = {}
        RoundPlayerList._InitUI(Char)
    end)
end

function RoundPlayerList._InitUI(Char: Model)
    if Char:FindFirstChild("Role").Value == "Spectator" then
        return
    end

    local UI = Prefabs.UI:Clone()
    UI.Parent = UIParent
    RoundPlayerList.UI = UI

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

    local Conns: {[string]: RBXScriptConnection} = {}

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

        local Card = Prefabs.Card:Clone()
        Card.Name = Character.Name

        --should show the skin portrait if equipped
        Card.Portrait.Image = require(Utils.Instance.GetCharacterModule("Survivor", Character:GetAttribute("CharacterName"), Character:GetAttribute("CharacterSkinName"))).Config.Render
        --should show the root character's name without caring about the skin
        Card.CharacterName.Text = require(Utils.Instance.GetCharacterModule("Survivor", Character:GetAttribute("CharacterName"))).Config.Name

        local Humanoid: Humanoid = Character:FindFirstChildWhichIsA("Humanoid")

        if Character == LocalPlayer.Character then
            Card.PlayerHealth.TextTransparency = 1
            Card.CharacterName.TextColor3 = Color3.fromRGB(255, 255, 127)
            Card.PortraitOutline.ImageColor3 = Color3.fromRGB(255, 255, 127)
            Card.PlayerName.Text = Character.Name.." (You)"
        else
            Card.PlayerName.Text = Character.Name

            local PlayerConns = {}

            local function HealthUpdate(Health)
                --stop checking when they die
                if Humanoid.Health <= 0 and Card then
                    Card.Portrait.ImageColor3 = Color3.fromRGB(161, 0, 0)
                    Card.PortraitOutline.ImageColor3 = Color3.fromRGB(255, 0, 0)

                    Card.PlayerState.TextColor3 = Color3.fromRGB(255, 0, 0)
                    Card.PlayerState.Text = "DEAD"
                    Card.PlayerState.TextTransparency = 1

                    Card.PlayerHealth.TextTransparency = 1

                    for _, Connection in PlayerConns do
                        Connection:Disconnect()
                    end
                    return
                end

                local HealthPercentageMagnitude = Humanoid.Health / Humanoid.MaxHealth
                Card.PlayerHealth.TextColor3 = Color3.fromRGB(
                    Utils.Math.MapToRange(HealthPercentageMagnitude, 0, 1, 1, 0) * 180 + 75,
                    HealthPercentageMagnitude * 180 + 75,
                    75
                )
                Card.PlayerHealth.Text = tostring(HealthPercentageMagnitude * 100).."%"
            end

            table.insert(PlayerConns, Humanoid.HealthChanged:Connect(HealthUpdate))
            table.insert(PlayerConns, Players.PlayerRemoving:Connect(function(Player: Player)
                if not Character then
                    return
                end

                --stop checking when they leave and are still alive
                if Player.Name == Character.Name then
                    for _, c in PlayerConns do
                        c:Disconnect()
                    end

                    Card.Portrait.ImageColor3 = Color3.fromRGB(200, 200, 200)
                    Card.PortraitOutline.ImageColor3 = Color3.fromRGB(150, 150, 150)

                    Card.PlayerState.TextColor3 = Color3.fromRGB(170, 170, 170)
                    Card.PlayerState.Text = "LOST"
                    Card.PlayerState.TextTransparency = 0

                    Card.PlayerHealth.TextTransparency = 1
                end
            end))

            HealthUpdate(Humanoid.Health)

            Conns[Character.Name] = PlayerConns
        end

        Card.Parent = UI
    end

    RoundPlayerList.Conns = Conns
end

return RoundPlayerList


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

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

local LocalPlayer = Players.LocalPlayer

local Rules = {
    Enabled = true,
    UIInstance = nil,
    Opened = false,
}

local Ruleset: {{
        Type: "Separator" | "SectionEntry" | "Text",
        Text: string,
    }} = {
    {
        Type = "Separator",
        Text = "Rule section N.º 1",
    },
    {
        Type = "SectionEntry",
        Text = "Pretty cool rule N.º 1!",
    },
    {
        Type = "SectionEntry",
        Text = "Pretty cool feature N.º 2!",
    },
    {
        Type = "Text",
        Text = "Quick note N.º 1!",
    },
    {
        Type = "SectionEntry",
        Text = "Pretty cool rule N.º 3!",
    },
    {
        Type = "Text",
        Text = "Quick note N.º 2!",
    },
    {
        Type = "Separator",
        Text = "Rule section N.º 2",
    },
    {
        Type = "SectionEntry",
        Text = "Pretty cool rule N.º 1!",
    },
    {
        Type = "Text",
        Text = "Quick note N.º 1!",
    },
    {
        Type = "SectionEntry",
        Text = "Pretty cool rule N.º 2!",
    },
    {
        Type = "SectionEntry",
        Text = "Pretty cool rule N.º 3!",
    },
    {
        Type = "Text",
        Text = "Quick note N.º 2!",
    },
}

local PrefabFolder = Utils.Instance.FindFirstChild(script, "Prefabs")
local Prefabs = {
    Separator = Utils.Instance.FindFirstChild(PrefabFolder, "Separator"),
    SectionEntry = Utils.Instance.FindFirstChild(PrefabFolder, "SectionEntry"),
    Text = Utils.Instance.FindFirstChild(PrefabFolder, "PlainText"),
}

local UIParent = Utils.Instance.FindFirstChild(LocalPlayer.PlayerGui, "Menus")
function Rules:Init()
    if Rules.UIInstance then
        Rules.UIInstance.Parent = UIParent
        Rules.UIInstance.Size = Utils.UDim.Zero
        Rules.UIInstance.Visible = false
        return
    end

    local UI = Utils.Instance.FindFirstChild(script, "Rules")
    UI.Size = Utils.UDim.Zero
    UI.Visible = false
    Rules.UIInstance = UI
    UI.Parent = UIParent

    local Content = UI.Content

    Utils.Instance.ObserveProperty(Rules.UIInstance, "Size", function(value: UDim2)
        Rules.UIInstance.Visible = value ~= Utils.UDim.Zero
    end)

    for order, entry in Ruleset do
        local Inst = Prefabs[entry.Type]:Clone()
        Inst.Title.Text = entry.Text
        Inst.LayoutOrder = order
        Inst.Parent = Content
    end
end

function Rules.Open(Time: number)
    if not Rules.Enabled or not LocalPlayer.Character.Role or LocalPlayer.Character.Role.Value ~= "Spectator" or not SideBar.Enabled then
        return "Prevented"
    end

    if Time > 0 then
        TweenService:Create(Rules.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.4, 0.7)}):Play()
    else
        Rules.UIInstance.Size = UDim2.fromScale(0.4, 0.7)
    end
    Rules.Opened = true

    return
end

function Rules.Close(Time: number)
    if Time > 0 then
        TweenService:Create(Rules.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play()
    else
        Rules.UIInstance.Size = Utils.UDim.Zero
    end
    Rules.Opened = false
end

function Rules.Toggle(toggle: boolean)
    if not Rules.Enabled and toggle then
        Rules:Init()
    end
    if not toggle and Rules.Enabled then
        Rules.UIInstance.Parent = script
        Rules.UIInstance.Size = Utils.UDim.Zero
    end
    Rules.Enabled = toggle
end

return Rules


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

local BoolSetting = {}

function BoolSetting:New(Bool: BoolValue, ExistingCheckbox: Frame?, Callback: (Value: boolean) -> ()?)
    if not Bool then
        Bool = Instance.new("BoolValue")
    end

    local BoolTable = setmetatable({
        SettingValue = Bool,
    }, BoolSetting)

    BoolTable.Instance = ExistingCheckbox or script.Checkbox:Clone()
    BoolTable.Instance.CheckboxButton.Checked.Visible = Bool.Value
    BoolTable.Instance.CheckboxButton.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
    end)
    BoolTable.Instance.CheckboxButton.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
    end)
    BoolTable.Instance.CheckboxButton.MouseButton1Click:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
        if Callback then
            task.spawn(Callback, not Bool.Value)
        end
        self:OnUpdate(BoolTable, not Bool.Value)
        Network:FireServerConnection("UpdateSetting", "REMOTE_EVENT", Bool, not Bool.Value)
    end)

    return BoolTable
end

function BoolSetting:OnUpdate(MetaTable, Value)
    MetaTable.Value = Value
    MetaTable.Instance.CheckboxButton.Checked.Visible = MetaTable.Value
end

return BoolSetting


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

local Utils = require(ReplicatedStorage.Modules.Utils)

local AFKLabel = Utils.Instance.FindFirstChild(Players.LocalPlayer.PlayerGui, "AFKLabel")

return function(Value: boolean)
    AFKLabel.Enabled = Value
end


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

return function(NewValue: number)
    LocalPlayer:SetAttribute("BaseFOV", NewValue)
end


local SoundService = game:GetService("SoundService")
return function(NewValue: number)
    SoundService.SoundGroups.Master.Music.Volume = NewValue / 100
end


local SoundService = game:GetService("SoundService")
return function(NewValue: number)
    SoundService.SoundGroups.Master.SFX.Volume = NewValue / 100
end


return function(Value: boolean)
    for _, Player: Model in workspace.Players:GetChildren() do
        local Hitboxes = Player:FindFirstChild("Hitboxes")
        if Hitboxes then
            for _, hitbox in Hitboxes:GetChildren() do
                hitbox.Transparency = Value and hitbox:GetAttribute("VisibleTransparency") or 1
            end
        end
    end
end


local SoundService = game:GetService("SoundService")
return function(NewValue: number)
    SoundService.SoundGroups.Master.UI.Volume = NewValue / 100
end


local SoundService = game:GetService("SoundService")
return function(NewValue: number)
    SoundService.SoundGroups.Master.VoiceLines.Volume = NewValue / 100
end


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

local NumberSetting = {}

function NumberSetting:New(NumberValue: NumberValue, ExistingInput: TextBox?, Callback: (Value: number) -> ()?)
    if not NumberValue then
        NumberValue = Instance.new("NumberValue")
    end

    local NumberTable = setmetatable({
        SettingValue = NumberValue,
    }, NumberSetting)

    NumberTable.Instance = ExistingInput or script.NumberInput:Clone()
    NumberTable.Value = NumberValue.Value
    NumberTable.Instance.Input.Text = tostring(NumberTable.Value)

    NumberTable.Instance.Input.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
    end)
    NumberTable.Instance.Input.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
    end)
    NumberTable.Instance.Input.FocusLost:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
    end)

    local Bounds = {
        Min = NumberValue:GetAttribute("MinValue") or 0,
        Max = NumberValue:GetAttribute("MaxValue") or 100,
    }
    local TextBox: TextBox = NumberTable.Instance.Input

    local LastNumber = NumberTable.Value
    TextBox.FocusLost:Connect(function()
        local StrNum = TextBox.Text:match("%d+")
        if not StrNum or #StrNum <= 0 then
            TextBox.Text = tostring(LastNumber)
            return
        end

        local NewValue = tonumber(StrNum)
        NewValue = math.clamp(NewValue, Bounds.Min, Bounds.Max)
        LastNumber = NewValue

        TextBox.Text = tostring(NewValue)

        if Callback then
            task.spawn(Callback, NewValue)
        end

        Network:FireServerConnection("UpdateSetting", "REMOTE_EVENT", NumberValue, NewValue)
    end)

    return NumberTable
end

return NumberSetting


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

local StringSetting = {}

function StringSetting:New(StringValue: StringValue, ExistingInput: TextBox?, Callback: (Value: string) -> ()?)
    if not StringValue then
        StringValue = Instance.new("StringValue")
    end

    local StringTable = setmetatable({
        SettingValue = StringValue,
    }, StringSetting)

    StringTable.Instance = ExistingInput or script.StringInput:Clone()
    StringTable.Value = StringValue.Value
    StringTable.Instance.Input.Text = StringTable.Value

    StringTable.Instance.Input.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
    end)
    StringTable.Instance.Input.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
    end)
    StringTable.Instance.Input.FocusLost:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
    end)

    local TextBox: TextBox = StringTable.Instance.Input

    TextBox.FocusLost:Connect(function()
        if Callback then
            task.spawn(Callback, TextBox.Text)
        end

        Network:FireServerConnection("UpdateSetting", "REMOTE_EVENT", StringValue, TextBox.Text)
    end)

    return StringTable
end

return StringSetting


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

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

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

local Settings = {
    Opened = false,
    Enabled = true,
    UIInstance = nil,
}

local Classes = {
    Bool = require(Utils.Instance.FindFirstChild(script, "Bool")),
    Number = require(Utils.Instance.FindFirstChild(script, "Number")),
    String = require(Utils.Instance.FindFirstChild(script, "String")),
}
local Prefabs = {
    UIPrefab = Utils.Instance.FindFirstChild(script, "SettingsScreen"),
    Section = Utils.Instance.FindFirstChild(script, "Section"),
}

local UIParent = Utils.Instance.FindFirstChild(LocalPlayer.PlayerGui, "Menus")

function Settings:Init()
    if Settings.UIInstance then
        Settings.UIInstance.Parent = UIParent
        Settings.UIInstance.Size = Utils.UDim.Zero
        Settings.UIInstance.Visible = false
        return
    end

    local UI = Prefabs.UIPrefab
    UI.Size = Utils.UDim.Zero
    UI.Visible = false
    UI.Parent = UIParent
    Settings.UIInstance = UI
    local Content = UI.SettingsContainer.Contents

    local CurrentLayoutOrder = 0

    local Folders = Players.LocalPlayer.PlayerData.Settings:GetChildren()

    table.sort(Folders, function(a, b)
        return a:GetAttribute("LayoutOrder") < b:GetAttribute("LayoutOrder")
    end)

    for _, SettingFolder in Folders do
        CurrentLayoutOrder += 1
        local SettingSection = Prefabs.Section:Clone()
        SettingSection.Name = SettingFolder.Name.."Section"
        SettingSection.TextLabel.Text = SettingFolder.Name
        SettingSection.LayoutOrder = CurrentLayoutOrder

        local SettingsInSection = {}
        for _, Setting in SettingFolder:GetChildren() do
            local Type = nil --gets type of setting
            if Setting:IsA("BoolValue") then
                Type = "Bool"
            elseif Setting:IsA("NumberValue") then
                Type = "Number"
            elseif Setting:IsA("StringValue") then
                Type = "String"
            else
                warn("what setting even is this?")
            end

            --indexes it to a table in order to sort it later
            table.insert(SettingsInSection, {
                Instance = Setting,
                LayoutOrder = Setting:GetAttribute("LayoutOrder"),
                Type = Type,
            })
        end
        --sorts by LayoutOrder
        table.sort(SettingsInSection, function(a, b)
            return a.LayoutOrder < b.LayoutOrder
        end)

        for _, Setting in ipairs(SettingsInSection) do
            CurrentLayoutOrder += 1

            local Callback = nil
            if script.Connections:FindFirstChild(Setting.Instance.Name) then
                Callback = require(script.Connections:FindFirstChild(Setting.Instance.Name))
            end

            local Inst = Classes[Setting.Type]:New(Setting.Instance, nil, Callback)
            local ActualInst = Inst.Instance
            ActualInst.LayoutOrder = CurrentLayoutOrder
            ActualInst.Name = Setting.Instance.Name

            ActualInst.SettingName.Text = Setting.Instance:GetAttribute("DisplayTitle") or ActualInst.SettingName.Text
            ActualInst.SettingDesc.Text = Setting.Instance:GetAttribute("DisplayDescription") or ActualInst.SettingDesc.Text

            if Callback then
                task.spawn(Callback, Inst.SettingValue.Value)
            end

            ActualInst.Parent = Content
        end

        SettingSection.Parent = Content
    end

    Utils.Instance.ObserveProperty(Settings.UIInstance, "Size", function(value: UDim2)
        Settings.UIInstance.Visible = value ~= Utils.UDim.Zero
    end)
end

function Settings.Open(Time: number)
    if not Settings.Enabled or not LocalPlayer.Character.Role or LocalPlayer.Character.Role.Value ~= "Spectator" or not SideBar.Enabled then
        return "Prevented"
    end

    TweenService:Create(Settings.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Full20Offset}):Play()
    Settings.Opened = true

    return
end

function Settings.Close(Time: number)
    if Time > 0 then
        TweenService:Create(Settings.UIInstance, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play()
    else
        Settings.UIInstance.Size = Utils.UDim.Zero
        Settings.UIInstance.Visible = false
    end
    Settings.Opened = false
end

function Settings.Toggle(toggle: boolean)
    if not Settings.Enabled and toggle then
        Settings:Init()
    end
    if not toggle and Settings.Enabled then
        Settings.UIInstance.Parent = script
        Settings.UIInstance.Size = Utils.UDim.Zero
    end
    Settings.Enabled = toggle
end

return Settings


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

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

local BuyWindow = {}
BuyWindow.__index = BuyWindow

function BuyWindow.New(Shop, Module: ModuleScript, skin: boolean?, Parent: Frame, Card)
    local Window = setmetatable({
        Checking = false,
        Instance = script:FindFirstChild("Window"):Clone(),
        Connections = {},
        Removed = Signal.new(),
        Purchased = Signal.new(),
    }, BuyWindow)
    if skin == nil then
        skin = false
    end
    local CharInfo = require(Module)
    Window.Instance.PriceTag.Text = "Purchase "..CharInfo.Config.Name.." for "..(CharInfo.Config.Price > 0 and CharInfo.Config.Price.."$" or "FREE").."?"
    Window.Instance.Parent = Parent
    Window.Instance.Size = Utils.UDim.Zero

    table.insert(Window.Connections, Window.Instance.Buy.Button.MouseButton1Click:Connect(function()
        if Window.Checking then
            return
        end
        Window.Checking = true
        Window.Instance.Buy.TextLabel.Text = "..."
        local Success = Network:FireServerConnection("BuyItem", "REMOTE_FUNCTION", Module)
        if Success then
            Window.Instance.Buy.TextLabel.Text = "Purchased!"
            Card.Container.Owned.BackgroundTransparency = 0.55
            Card.Container.Owned.TextLabel.TextTransparency = 0
            Window.Purchased:Fire()
            table.insert(Window.Connections, task.delay(0.75, function()
                Window:Hide()
            end))
        else
            Window.Instance.Buy.TextLabel.Text = "you're too poor."
            Window.Checking = false
        end
    end))
    table.insert(Window.Connections, Window.Instance.Cancel.Button.MouseButton1Click:Connect(function()
        if Window.Checking then
            return
        end
        Window:Hide()
    end))
    Window:Show()

    return Window
end

function BuyWindow:Show()
    TweenService:Create(self.Instance, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.253, 0.202)}):Play()
end

function BuyWindow:Hide(instant: boolean?)
    if instant == nil then
        instant = false
    end
    self.Removed:Fire()
    self.Removed:DisconnectAll()
    self.Purchased:DisconnectAll()
    for _, connection in self.Connections do
        if typeof(connection) == "thread" then
            if coroutine.status(connection :: thread) ~= "running" then
                task.cancel(connection)
            end
        else
            connection:Disconnect()
        end
    end
    if not instant then
        TweenService:Create(self.Instance, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play()
    else
        self.Instance.Size = Utils.UDim.Zero
    end
    Debris:AddItem(self.Instance, 0.5)
end

return BuyWindow


local GuiService = game:GetService("GuiService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TextService = game:GetService("TextService")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")

local SideBar = require(script.Parent.SideBar)
local Types = require(ReplicatedStorage.Classes.Types)
local Tooltip = require(ReplicatedStorage.Classes.Tooltip)
local Utils = require(ReplicatedStorage.Modules.Utils)
local Sounds = require(ReplicatedStorage.Modules.Sounds)
local Janitor = require(ReplicatedStorage.Packages.Janitor)

local BuyWindow = require(script.BuyWindow)

local CharacterFolder = ReplicatedStorage.Assets.Characters

local Shop = {
    Opened = false,
    Enabled = true,
    SelectedSection = "Survivors",
    SkinMenu = {
        Open = false,
        ShowingCharacter = "",
    },
    InfoMenu = {
        Open = false,
        ShowingCharacter = "",
    },
    LoadedItems = {
        Killers = {},
        Survivors = {},
        Emotes = {},
        Skins = {},
    },
    ItemModules = {},
    SelectedItem = {
        Name = "",
        Type = "",
        Card = nil,
    },
    UIInstance = nil,
    BuyWindow = nil,

    PreviewsEnabled = true,
}

local LocalPlayer = Players.LocalPlayer

local UIParent = Utils.Instance.FindFirstChild(LocalPlayer.PlayerGui, "Menus")

local Buttons = {
    Purchase = nil,
    Info = nil,
    Skins = nil,
}

local Sizes = {
    SideBar = UDim2.fromScale(0.3, 1),
    ClosedSideBar = Utils.UDim.FullY,
}

local PreviewInstances = {
    Render = nil,
    Title = nil,
    EmotePreview = nil,
}

local Templates = {
    CardTemplate = nil,
    Header = nil,
    Text = nil,
    Quote = nil,
    Separator = nil,
    InfoSection = nil,
}

local UI
local ShopContainer
local KillerContainer
local SurvivorContainer
local EmoteContainer

local SideContainer
local SkinContainer
local InfoContainer
local PreviewContainer
local ButtonsContainer

local EmoteRig

local Sections = {}

local LoadedEmoteTracks = {}

function Shop.ReorderSkins()
    local Ordered = {
        Owned = {},
        Unowned = {},
    }
    local LO = 0

    for name, skin in Shop.LoadedItems.Skins do
        if Utils.PlayerData.GetPlayerOwned(LocalPlayer, "Skins."..Shop.SelectedItem.Name.."."..name) then
            table.insert(Ordered.Owned, skin)
        else
            table.insert(Ordered.Unowned, skin)
        end
    end

    table.sort(Ordered.Owned, function(a, b)
        local Modules = {
            A = require(Utils.Instance.GetCharacterModule(Shop.SelectedItem.Type, Shop.SelectedItem.Name, a.Name)),
            B = require(Utils.Instance.GetCharacterModule(Shop.SelectedItem.Type, Shop.SelectedItem.Name, b.Name)),
        }

        return
            Modules.A.Config.Price == Modules.B.Config.Price and a.Name < b.Name
            or Modules.A.Config.Price < Modules.B.Config.Price
    end)
    table.sort(Ordered.Unowned, function(a, b)
        local Modules = {
            A = require(Utils.Instance.GetCharacterModule(Shop.SelectedItem.Type, Shop.SelectedItem.Name, a.Name)),
            B = require(Utils.Instance.GetCharacterModule(Shop.SelectedItem.Type, Shop.SelectedItem.Name, b.Name)),
        }

        return
            Modules.A.Config.Price == Modules.B.Config.Price and a.Name < b.Name
            or Modules.A.Config.Price < Modules.B.Config.Price
    end)

    for _, skin in ipairs(Ordered.Unowned) do
        LO += 1
        skin.LayoutOrder = LO
    end
    for _, skin in ipairs(Ordered.Owned) do
        LO += 1
        skin.LayoutOrder = LO
    end
end

function Shop.ToggleSkinsMenu(Open: boolean?)
    if Open == nil then
        Open = not Shop.SkinMenu.Open
    end

    if Open then
        Shop.ToggleInfoMenu(false)

        local Skins = ReplicatedStorage.Characters.Skins[Shop.SelectedItem.Type.."s"]:FindFirstChild(Shop.SelectedItem.Name)
        if not Skins then
            return
        end

        --if there are no visible skins, return
        local VisibleSkins = Skins:GetChildren()
        for skinIndex = #VisibleSkins, 1, -1 do
            local skin = VisibleSkins[skinIndex]
            if not (skin:HasTag("Dev") or skin:HasTag("Milestone")) then
                continue
            end

            table.remove(VisibleSkins[skinIndex])
        end

        if #VisibleSkins <= 0 then
            return
        end

        for _, skin in Shop.LoadedItems.Skins do
            if SkinContainer.SkinPreviewCache:FindFirstChild(skin.Name) then
                SkinContainer.SkinPreviewCache[skin.Name]:Destroy()
            end

            skin:Destroy()
        end

        table.clear(Shop.LoadedItems.Skins)
        Shop.ItemModules.Skins = Shop.ItemModules.Skins or {}
        table.clear(Shop.ItemModules.Skins)

        local function CheckSkinOwnLabels()
            for name, skin in Shop.LoadedItems.Skins do
                if Utils.PlayerData.GetPlayerOwned(LocalPlayer, "Skins."..Shop.SelectedItem.Name.."."..name) then
                    skin.Container.Owned.BackgroundTransparency = 0.55
                    skin.Container.Owned.TextLabel.TextTransparency = 0
                end
            end
        end

        SkinContainer.Parent.Label.TextLabel.Text = require(Utils.Instance.GetCharacterModule(Shop.SelectedItem.Type, Shop.SelectedItem.Name)).Config.Name.." Skins"

        local CanMakePreviews = Shop.PreviewsEnabled and Utils.PlayerData.GetPlayerSetting(LocalPlayer, "Performance.SkinPreviews")
        for _, Skin in VisibleSkins do
            local Module = Utils.Instance.GetCharacterModule(Shop.SelectedItem.Type, Shop.SelectedItem.Name, Skin.Name)

            if Module:HasTag("Dev") or Module:HasTag("Milestone") then
                continue
            end

            local Info = require(Module)
            local name = Module.Name

            local Card = Templates.CardTemplate:Clone()
            Card.Name = name
            local Container = Card.Container
            Card.Parent = SkinContainer.ScrollingFrame
            Container.Title.Text = Info.Config.Name
            Container.CharacterRender.Image = Info.Config.Render
            Container.Price.Text = Info.Config.Price > 0 and tostring(Info.Config.Price).."$" or "FREE"

            if Info.Config.CardFrame then
                Container.Outline.Image = Info.Config.CardFrame.Image or Container.Outline.Image
                Container.Outline.Size = Info.Config.CardFrame.Size or Container.Outline.Size
            end

            local SkinPreview = Container.Preview.ViewportFrame
            if CanMakePreviews then
                task.defer(function()
                    local SkinModel = Utils.Instance.FindFirstChild(CharacterFolder, "Skins."..Shop.SelectedItem.Type.."."..Shop.SelectedItem.Name.."."..name, 0)
                    if not SkinModel then
                        warn("[Shop.ToggleSkinsMenu]: Item preview model can't be found! Make sure it's located in ReplicatedStorage/Characters/Skins/"..Shop.SelectedItem.Type.."/"..Shop.SelectedItem.Name.."s! ("..name..")")
                        return
                    end

                    -- i'm going fucking insane. -dys
                    local Camera = Instance.new("Camera")
                    Camera.FieldOfView = 10
                    Camera.Parent = SkinPreview
                    local WorldModel = Instance.new("WorldModel")
                    WorldModel.Parent = Camera
                    SkinPreview.CurrentCamera = Camera

                    SkinModel = SkinModel:Clone()
                    if not SkinModel.Humanoid:FindFirstChildWhichIsA("Animator") then
                        Instance.new("Animator").Parent = SkinModel.Humanoid
                    end
                    Utils.Character.GetRootPart(SkinModel).Anchored = true
                    SkinModel:PivotTo(CFrame.new(0, 10000, 0) * CFrame.fromEulerAnglesXYZ(0, math.rad(180), 0))

                    Camera.CFrame = CFrame.new(0, 10000, 6 * SkinModel:GetExtentsSize().Y)

                    SkinModel.Parent = SkinContainer.SkinPreviewCache

                    Utils.Character.LoadAnimationFromID(SkinModel, Info.Config.AnimationIDs.PreviewAnimation or Info.Config.AnimationIDs.IdleAnimation, false):Play(0)

                    Utils.Instance.ObserveProperty(SkinPreview, "ImageTransparency", function(value: number)
                        if value < 1 then
                            if SkinModel.Parent ~= WorldModel then
                                SkinModel.Parent = WorldModel
                            end
                        elseif SkinModel.Parent ~= SkinContainer.SkinPreviewCache then
                            SkinModel.Parent = SkinContainer.SkinPreviewCache
                        end
                    end)
                end)
            end

            --create tables if not there
            Shop.ItemModules.Skins = Shop.ItemModules.Skins or {}
            Shop.ItemModules.Skins[Shop.SelectedItem.Name] = Shop.ItemModules.Skins[Shop.SelectedItem.Name] or {}

            Shop.LoadedItems.Skins[name] = Card
            Shop.ItemModules.Skins[Shop.SelectedItem.Name][name] = Info

            -- reminder that these connections disconnect automatically when the parent is destroyed! -dys
            Container.Interact.MouseEnter:Connect(function()
                if CanMakePreviews then
                    TweenService:Create(SkinPreview, TweenInfo.new(0.25), {
                        ImageTransparency = 0
                    }):Play()
                    TweenService:Create(SkinPreview.Parent, TweenInfo.new(0.25), {
                        BackgroundTransparency = 0
                    }):Play()
                end
                TweenService:Create(Container, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
                    Size = UDim2.fromScale(1.065, 1.065)
                }):Play()
                Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
            end)
            Container.Interact.MouseLeave:Connect(function()
                if CanMakePreviews then
                    TweenService:Create(SkinPreview, TweenInfo.new(0.25), {
                        ImageTransparency = 1
                    }):Play()
                    TweenService:Create(SkinPreview.Parent, TweenInfo.new(0.25), {
                        BackgroundTransparency = 1
                    }):Play()
                end
                TweenService:Create(Container, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
                    Size = Utils.UDim.Full
                }):Play()
                Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
            end)
            Container.Interact.MouseButton1Click:Connect(function()
                Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
                if Container.Owned.BackgroundTransparency >= 1 then
                    Shop.CreateBuyWindow(Skin, true, UI, Card)
                end

                CheckSkinOwnLabels()
            end)

            if Info.Config.Origin and Info.Config.Origin.Icon then
                Container.Origin.Visible = true
                Container.Origin.Image = Info.Config.Origin.Icon
                Container.Origin.MouseEnter:Connect(function()
                    Tooltip.New(Info.Config.Origin.TooltipText)
                    Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
                end)
                Container.Origin.MouseLeave:Connect(function()
                    Tooltip.New()
                    Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
                end)
            else
                Container.Origin.Visible = false
            end
        end

        CheckSkinOwnLabels()

        Shop.ReorderSkins()

        TweenService:Create(ShopContainer.Parent, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.FullY}):Play()
        TweenService:Create(SkinContainer.Parent, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.7, 1)}):Play()

        Shop.SkinMenu.Open = true
    else
        TweenService:Create(ShopContainer.Parent, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.7, 1)}):Play()
        TweenService:Create(SkinContainer.Parent, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.FullY}):Play()

        Shop.SkinMenu.Open = false
    end

    TweenService:Create(InfoContainer.Parent, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.FullY}):Play()
end

function Shop.ToggleInfoMenu(Open: boolean?)
    if Open == nil then
        Open = not Shop.InfoMenu.Open
    end

    if Open then
        Shop.ToggleSkinsMenu(false)
    end

    if Open then
        for _, Child in InfoContainer:GetChildren() do
            if not Child:IsA("UIComponent") then
                Child:Destroy()
            end
        end

        local CharacterInfo = require(Utils.Instance.GetCharacterModule(Shop.SelectedItem.Type, Shop.SelectedItem.Name))

        if not CharacterInfo.Config.Description then
            return
        end

        Shop.InfoMenu.Open = true

        local TextsToCheck = {}

        for SectionIndex: number, Section: {
            Type: "Text" | "Header" | "Separator" | "Quote",
            Text: string | {string},
            Image: string?,
        } in CharacterInfo.Config.Description do
            local SectionContainer = Templates.InfoSection:Clone()
            SectionContainer.LayoutOrder = SectionIndex
            SectionContainer.Parent = InfoContainer

            local Text = ""
            if typeof(Section.Text) == "table" then
                for index, paragraph in ipairs(Section.Text) do
                    if index > 1 then
                        Text = Text.."\n"
                    end
                    Text = Text..paragraph
                end
            else
                Text = Section.Text
            end

            local Pref = Templates[Section.Type]:Clone()
            if Section.Type ~= "Text" then
                (Section.Type == "Separator" and Pref.Title or Pref).Text = Section.Text
            else
                Pref.Title.Text = Text
                Pref.Title.MaxVisibleGraphemes = 0
                if Section.Image then
                    Pref.Image.Image = Section.Image
                    Pref.Image.Visible = true
                end
                table.insert(TextsToCheck, Pref)
                TweenService:Create(Pref.Title, TweenInfo.new(0.03 * #Text, Enum.EasingStyle.Linear), {MaxVisibleGraphemes = #Text}):Play()
            end
            Pref.Parent = SectionContainer
        end

        task.spawn(function()
            local PossibleEffects = {}

            local function AddEffectToList(Inst: ModuleScript | Folder)
                for _, Effect in Inst:GetChildren() do
                    if Effect:IsA("Folder") then
                        AddEffectToList(Effect)
                        continue
                    end

                    if not Effect:IsA("ModuleScript") then
                        continue
                    end

                    PossibleEffects[Effect.Name] = require(Effect).Description or "UNKNOWN"
                end
            end

            AddEffectToList(ReplicatedStorage.Effects)

            local LocalPlayer = Players.LocalPlayer
            local CurrentTooltip

            local function GetSpaceWidth(font, textSize, maxX)
                return TextService:GetTextSize(" ", textSize, font, Vector2.new(maxX, 1e5)).X
            end

            while task.wait() and Shop.InfoMenu.Open do
                local ui = Shop.UIInstance
                local char = LocalPlayer.Character
                local role = char and char:FindFirstChild("Role")

                if not (ui and ui.Visible and role and role.Value == "Spectator") then
                    if CurrentTooltip then
                        CurrentTooltip = Tooltip.New()
                    end
                    continue
                end

                local mousePos = UserInputService:GetMouseLocation() - GuiService:GetGuiInset()
                local foundEffect = false

                for _, textLabel in TextsToCheck do
                    if foundEffect then break end

                    local text = textLabel.Title
                    local absPos, absSize = text.AbsolutePosition, text.AbsoluteSize
                    local font, textSize = text.Font, text.TextSize
                    local maxWidth = absSize.X
                    local spaceWidth = GetSpaceWidth(font, textSize, maxWidth)

                    -- bound check
                    if not (mousePos.X >= absPos.X and mousePos.X <= absPos.X + absSize.X and
                            mousePos.Y >= absPos.Y and mousePos.Y <= absPos.Y + absSize.Y) then
                        continue
                    end

                    local lineHeight = TextService:GetTextSize("A", textSize, font, Vector2.new(maxWidth, 1e5)).Y
                    local totalY = 0

                    for _, rawLine in ipairs(text.Text:gsub("<.->", ""):split("\n")) do
                        local currentLine = {}
                        local currentWidth = 0

                        local wrappedLines = {}
                        for _, word in ipairs(rawLine:split(" ")) do
                            local wordSize = TextService:GetTextSize(word, textSize, font, Vector2.new(maxWidth, 1e5)).X
                            if currentWidth + wordSize > maxWidth then
                                table.insert(wrappedLines, currentLine)
                                currentLine = {}
                                currentWidth = 0
                            end
                            table.insert(currentLine, word)
                            currentWidth += wordSize + spaceWidth
                        end
                        if #currentLine > 0 then
                            table.insert(wrappedLines, currentLine)
                        end

                        for _, lineWords in ipairs(wrappedLines) do
                            if foundEffect then break end

                            local minY = absPos.Y + totalY
                            local maxY = minY + lineHeight
                            if mousePos.Y >= minY and mousePos.Y <= maxY then
                                local lineX = absPos.X

                                for _, word in ipairs(lineWords) do
                                    local wSize = TextService:GetTextSize(word, textSize, font, Vector2.new(maxWidth, 1e5)).X
                                    local minX, maxX = lineX, lineX + wSize

                                    if mousePos.X >= minX and mousePos.X <= maxX then
                                        local desc = PossibleEffects[word]
                                        if desc then
                                            CurrentTooltip = Tooltip.New(desc)
                                            foundEffect = true
                                            break
                                        end
                                    end
                                    lineX += wSize + spaceWidth
                                end
                            end
                            totalY += lineHeight
                        end
                    end
                end

                if not foundEffect then
                    CurrentTooltip = Tooltip.New()
                end
            end
        end)

        TweenService:Create(ShopContainer.Parent, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.FullY}):Play()
        TweenService:Create(InfoContainer.Parent, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.7, 1)}):Play()
    else
        TweenService:Create(ShopContainer.Parent, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0.7, 1)}):Play()
        TweenService:Create(InfoContainer.Parent, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.FullY}):Play()

        Shop.InfoMenu.Open = false
    end
end

local function SwitchSection(sectionName: string)
    if not Shop.Opened or Shop.SelectedSection == sectionName then return end

    TweenService:Create(SideContainer, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = UDim2.fromScale(0, 1)}):Play()

    if workspace.Sounds:FindFirstChild("EmotePreview") then
        workspace.Sounds.EmotePreview:Destroy()
    end

    for _, section in Sections do
        section.Visible = section.Name == sectionName
    end

    Shop.SelectedSection = sectionName
end

function Shop:Init()
    UI = Utils.Instance.FindFirstChild(script, "ShopUI"):Clone()
    UI.Size = Utils.UDim.Zero
    UI.Visible = false
    UI.Parent = UIParent
    Shop.UIInstance = UI

    local Container = UI.Container
    ShopContainer = Container.ShopContainer.Content
    KillerContainer = ShopContainer.Killers
    SurvivorContainer = ShopContainer.Survivors
    EmoteContainer = ShopContainer.Emotes
    SkinContainer = Container.SkinsContainer.Content
    InfoContainer = Container.InfoContainer.Contents

    Sections = {
        KillerContainer,
        SurvivorContainer,
        EmoteContainer,
    }

    for _, button in Container.ShopContainer.Topbar:GetChildren() do
        if button:IsA("ImageButton") and button.Visible then
            button.MouseEnter:Connect(function()
                Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
            end)
            button.MouseLeave:Connect(function()
                Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
            end)
            button.MouseButton1Click:Connect(function()
                Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
                SwitchSection(button.Name)
            end)
        end
    end

    local templates = Utils.Instance.FindFirstChild(script, "Templates")
    Templates.CardTemplate = Templates.CardTemplate or templates.CardTemplate
    Templates.Header = Templates.Header or templates.Header
    Templates.Text = Templates.Text or templates.TextContainer
    Templates.Quote = Templates.Quote or templates.Quote
    Templates.Separator = Templates.Separator or templates.Separator
    Templates.InfoSection = Templates.InfoSection or templates.InfoSection

    SideContainer = Container.SideContainer
    PreviewContainer = SideContainer.PreviewContainer

    PreviewInstances.Render = PreviewContainer.Render
    PreviewInstances.Render.Image = ""
    PreviewInstances.Title = PreviewContainer.Title
    PreviewInstances.EmotePreview = PreviewContainer.Emote
    EmoteRig = PreviewContainer.Emote.EmotePreview.Rig

    ButtonsContainer = SideContainer.ButtonsContainer

    Buttons.Purchase = ButtonsContainer.Purchase
    Buttons.Skins = ButtonsContainer.Skins
    Buttons.Info = ButtonsContainer.MoreInfo

    local JanitorInstance = Janitor.new()
    JanitorInstance:LinkToInstance(UI)

    JanitorInstance:Add(Buttons.Purchase.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
    end))
    JanitorInstance:Add(Buttons.Purchase.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
    end))
    JanitorInstance:Add(Buttons.Purchase.MouseButton1Click:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
        if not Utils.PlayerData.GetPlayerOwned(LocalPlayer, Shop.SelectedItem.Type.."s."..Shop.SelectedItem.Name) then
            if Shop.SelectedItem.Type == "Emote" then
                Shop.CreateBuyWindow(Utils.Instance.GetEmoteModule(Shop.SelectedItem.Name), false, UI, Shop.SelectedItem.Card)
                return
            end

            Shop.CreateBuyWindow(Utils.Instance.GetCharacterModule(Shop.SelectedItem.Type, Shop.SelectedItem.Name), false, UI, Shop.SelectedItem.Card)
        end
    end))

    JanitorInstance:Add(Buttons.Skins.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
    end))
    JanitorInstance:Add(Buttons.Skins.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
    end))
    JanitorInstance:Add(Buttons.Skins.MouseButton1Click:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})

        TweenService:Create(SideContainer, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Sizes.SideBar}):Play() --assuring the sidebar is open

        Shop.ToggleSkinsMenu()
    end))

    JanitorInstance:Add(Buttons.Info.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
    end))
    JanitorInstance:Add(Buttons.Info.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
    end))
    JanitorInstance:Add(Buttons.Info.MouseButton1Click:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})

        TweenService:Create(SideContainer, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Sizes.SideBar}):Play() --assuring the sidebar is open

        Shop.ToggleInfoMenu()
    end))

    Utils.Instance.ObserveProperty(Shop.UIInstance, "Size", function(value: UDim2)
        Shop.UIInstance.Visible = value ~= Utils.UDim.Zero
    end)

    Utils.Instance.ObserveProperty(InfoContainer.Parent, "Size", function(value: UDim2)
        if not Shop.InfoMenu.Open and value.X.Scale < 0.05 and #InfoContainer:GetChildren() > 0 then
            for _, Child in InfoContainer:GetChildren() do
                if not Child:IsA("UIComponent") then
                    Child:Destroy()
                end
            end
        end
    end)

    Utils.Instance.ObserveProperty(SkinContainer.Parent, "Size", function(value: UDim2)
        SkinContainer.Parent.Label.TextLabel.Visible = value.X.Scale >= 0.05
    end)

    Shop.ReloadContent(true)
end

function Shop.Open(Time: number)
    if not Shop.Enabled or not LocalPlayer.Character.Role or LocalPlayer.Character.Role.Value ~= "Spectator" or not SideBar.Enabled then
        return "Prevented"
    end

    Shop.ToggleSkinsMenu(false)
    Shop.ToggleInfoMenu(false)
    PreviewInstances.Render.Image = ""
    PreviewInstances.Render.ImageTransparency = 0
    PreviewInstances.EmotePreview.Visible = false
    ButtonsContainer.Skins.Visible = true
    ButtonsContainer.MoreInfo.Visible = true
    for _, emoteTrack in LoadedEmoteTracks do
        if emoteTrack.Animation ~= nil then
            emoteTrack.Animation:Stop(0)
        else
            for _, EmoteTrack in emoteTrack do
                EmoteTrack.Animation:Stop(0)
            end
        end
    end
    if workspace.Sounds:FindFirstChild("EmotePreview") then
        workspace.Sounds.EmotePreview:Destroy()
    end
    SkinContainer.Parent.Size = Utils.UDim.FullY
    InfoContainer.Parent.Size = Utils.UDim.FullY
    SideContainer.Size = Utils.UDim.FullY
    ShopContainer.Parent.Size = UDim2.fromScale(0.7, 1)
    Shop.SkinMenu.Open = false

    TweenService:Create(UI, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Full20Offset}):Play()
    Shop.Opened = true

    return
end

function Shop.Close(Time: number)
    if Shop.BuyWindow then
        Shop.BuyWindow:Hide(Time <= 0)
    end
    if workspace.Sounds:FindFirstChild("EmotePreview") then
        workspace.Sounds.EmotePreview:Destroy()
    end
    if Time > 0 then
        TweenService:Create(UI, TweenInfo.new(Time, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Utils.UDim.Zero}):Play()
    else
        UI.Size = Utils.UDim.Zero
        UI.Visible = false
    end
    Shop.Opened = false
end

function Shop.Toggle(toggle: boolean)
    if not Shop.Enabled and toggle then
        Shop:Init()
    end
    if not toggle and Shop.Enabled then
        if Shop.BuyWindow then
            Shop.BuyWindow:Hide(true)
        end
        if workspace.Sounds:FindFirstChild("EmotePreview") then
            workspace.Sounds.EmotePreview:Destroy()
        end
        table.clear(Shop.ItemModules)
        Shop.LoadedItems = {
            Killers = {},
            Survivors = {},
            Emotes = {},
            Skins = {},
        }
        Shop.SelectedItem = {
            Name = "",
            Type = "",
            Card = nil,
        }
        Shop.SelectedSection = "Survivors"
        Shop.UIInstance:Destroy()
        Shop.SkinMenu = {
            Open = false,
        }
    end
    Shop.Enabled = toggle
end

function Shop.OnCharButtonPress(codeName: string, info: Types.Killer | Types.Survivor | Types.Emote, Module: ModuleScript, Type: string, Card: Frame)
    if not Shop.Opened then
        return
    end

    PreviewInstances.Render.Image = info.Config.Render
    PreviewInstances.Title.Text = info.Config.Name

    if Utils.PlayerData.GetPlayerOwned(LocalPlayer, Type.."s."..codeName) then
        Buttons.Purchase.Title.Text = "Purchased"
    else
        Buttons.Purchase.Title.Text = "Purchase"
    end

    ButtonsContainer.CurrentItemPrice.Text = info.Config.Price > 0 and tostring(info.Config.Price).."$" or "FREE"

    Shop.SelectedItem.Name = codeName
    Shop.SelectedItem.Type = Type
    Shop.SelectedItem.Card = Card

    TweenService:Create(SideContainer, TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Size = Sizes.SideBar}):Play()

    if Type == "Emote" then
        PreviewInstances.EmotePreview.Visible = true
        PreviewInstances.Render.ImageTransparency = 1
        ButtonsContainer.Skins.Visible = false
        ButtonsContainer.MoreInfo.Visible = false

        if workspace.Sounds:FindFirstChild("EmotePreview") then
            workspace.Sounds.EmotePreview:Destroy()
        end
        for _, emoteTrack in LoadedEmoteTracks do
            if emoteTrack.Animation ~= nil then
                emoteTrack.Animation:Stop(0)
            else
                for _, EmoteTrack in emoteTrack do
                    EmoteTrack.Animation:Stop(0)
                end
            end
        end

        local AnimTrack = LoadedEmoteTracks[Module.Name]
        if not AnimTrack then
            local AnimIDs = info.AnimationIds
            if typeof(AnimIDs) == "string" then
                LoadedEmoteTracks[Module.Name] = {
                    Animation = Utils.Character.LoadAnimationFromID(EmoteRig, AnimIDs),
                    Sound = nil,
                }
            else
                LoadedEmoteTracks[Module.Name] = {}

                for _, ID: string in AnimIDs do
                    LoadedEmoteTracks[Module.Name][ID] = {
                        Animation = Utils.Character.LoadAnimationFromID(EmoteRig, ID),
                        Sound = nil,
                    }
                end
            end

            if info.SoundIds then
                if typeof(info.SoundIds) == "string" then
                    if typeof(AnimIDs) == "string" then
                        LoadedEmoteTracks[Module.Name].Sound = info.SoundIds
                    else
                        for _, id in AnimIDs do
                            LoadedEmoteTracks[Module.Name][id].Sound = info.SoundIds
                        end
                    end
                else
                    for animId, id in info.SoundIds do
                        LoadedEmoteTracks[Module.Name][animId].Sound = id
                    end
                end
            end

            AnimTrack = LoadedEmoteTracks[Module.Name]
        end

        if not AnimTrack.Animation then
            local Selected = AnimTrack[math.random(1, #AnimTrack)]
            Selected.Animation:Play(0)
            Sounds.PlaySound(Selected.Sound, {Name = "EmotePreview"})
        else
            AnimTrack.Animation:Play(0)
            Sounds.PlaySound(AnimTrack.Sound, {Name = "EmotePreview"})
        end
    else
        PreviewInstances.EmotePreview.Visible = false
        PreviewInstances.Render.ImageTransparency = 0
        ButtonsContainer.Skins.Visible = true
        ButtonsContainer.MoreInfo.Visible = true
    end
end

function Shop.ReloadContent(CreateCards: boolean?)
    if CreateCards == nil then
        CreateCards = false
    end

    local Ordered = {
        Owned = {},
        Unowned = {},
    }
    local LO = 0

    local CanMakePreviews = Shop.PreviewsEnabled and Utils.PlayerData.GetPlayerSetting(LocalPlayer, "Performance.SkinPreviews")

    local function SetupCard(module, Type: "Survivor" | "Killer" | "Emote")
        if module:HasTag("Dev") then
            return
        end

        LO += 1

        local Parent = if Type == "Killer" then KillerContainer elseif Type == "Survivor" then SurvivorContainer else EmoteContainer

        if not CreateCards then
            Parent:FindFirstChild(module.Name).LayoutOrder = LO
            return
        end

        local name = module.Name

        if Shop.LoadedItems[Type.."s"][name] then
            return
        end

        local Module = Type == "Emote" and Utils.Instance.GetEmoteModule(name) or Utils.Instance.GetCharacterModule(Type, name)
        local Info = require(Module)

        local Card = Templates.CardTemplate:Clone()
        local Container = Card.Container
        Card.Name = name
        Card.LayoutOrder = LO
        Card.Parent = Parent
        Container.Title.Text = Info.Config.Name
        Container.CharacterRender.Image = Info.Config.Render
        Container.Price.Text = Info.Config.Price > 0 and tostring(Info.Config.Price).."$" or "FREE"

        if Info.Config.CardFrame then
            Container.Outline.Image = Info.Config.CardFrame.Image or Container.Outline.Image
            Container.Outline.Size = Info.Config.CardFrame.Size or Container.Outline.Size
        end

        local Preview = Container.Preview.ViewportFrame
        if CanMakePreviews and Type ~= "Emote" then
            task.defer(function()
                local Model = Utils.Instance.FindFirstChild(CharacterFolder, Type.."."..name, 0)
                if not Model then
                    warn("[Shop.ReloadContent]: Item preview model can't be found! Make sure it's located in ReplicatedStorage/Characters/"..Type.."! ("..name..")")
                    return
                end

                -- i'm going fucking insane. -dys
                local Camera = Instance.new("Camera")
                Camera.FieldOfView = 10
                Camera.Parent = Preview
                local WorldModel = Instance.new("WorldModel")
                WorldModel.Parent = Camera
                Preview.CurrentCamera = Camera

                Model = Model:Clone()
                if not Model.Humanoid:FindFirstChildWhichIsA("Animator") then
                    Instance.new("Animator").Parent = Model.Humanoid
                end
                Utils.Character.GetRootPart(Model).Anchored = true
                Model:PivotTo(CFrame.new(0, 10000, 0) * CFrame.fromEulerAnglesXYZ(0, math.rad(180), 0))

                Camera.CFrame = CFrame.new(0, 10000, 6 * Model:GetExtentsSize().Y)

                Model.Parent = ShopContainer.PreviewCache

                Utils.Character.LoadAnimationFromID(Model, Info.Config.AnimationIDs.PreviewAnimation or Info.Config.AnimationIDs.IdleAnimation, false):Play(0)

                Utils.Instance.ObserveProperty(Preview, "ImageTransparency", function(value: number)
                    if value < 1 then
                        if Model.Parent ~= WorldModel then
                            Model.Parent = WorldModel
                        end
                    elseif Model.Parent ~= ShopContainer.PreviewCache then
                        Model.Parent = ShopContainer.PreviewCache
                    end
                end)
            end)
        end

        if Utils.PlayerData.GetPlayerOwned(LocalPlayer, Type.."s."..name) then
            Container.Owned.BackgroundTransparency = 0.55
            Container.Owned.TextLabel.TextTransparency = 0
        end

        --create table if not there
        Shop.ItemModules[Type.."s"] = Shop.ItemModules[Type.."s"] or {}

        Shop.LoadedItems[Type.."s"][name] = Card
        Shop.ItemModules[Type.."s"][name] = Info
        Container.Interact.MouseEnter:Connect(function()
            if CanMakePreviews and Type ~= "Emote" then
                TweenService:Create(Preview, TweenInfo.new(0.25), {
                    ImageTransparency = 0
                }):Play()
                TweenService:Create(Preview.Parent, TweenInfo.new(0.25), {
                    BackgroundTransparency = 0
                }):Play()
            end
            TweenService:Create(Container, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
                Size = UDim2.fromScale(1.065, 1.065)
            }):Play()
            Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
        end)
        Container.Interact.MouseLeave:Connect(function()
            if CanMakePreviews and Type ~= "Emote" then
                TweenService:Create(Preview, TweenInfo.new(0.25), {
                    ImageTransparency = 1
                }):Play()
                TweenService:Create(Preview.Parent, TweenInfo.new(0.25), {
                    BackgroundTransparency = 1
                }):Play()
            end
            TweenService:Create(Container, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
                Size = Utils.UDim.Full
            }):Play()
            Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
        end)
        Container.Interact.MouseButton1Click:Connect(function()
            if Shop.SkinMenu.Open then
                return
            end
            Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
            Shop.OnCharButtonPress(name, Info, Module, Type, Card)
        end)

        if Info.Config.Origin and Info.Config.Origin.Icon then
            Container.Origin.Visible = true
            Container.Origin.Image = Info.Config.Origin.Icon

            local TooltipAvailable = Info.Config.Origin.TooltipText ~= nil

            Container.Origin.MouseEnter:Connect(function()
                if TooltipAvailable then
                    Tooltip.New(Info.Config.Origin.TooltipText)
                end

                Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
            end)
            Container.Origin.MouseLeave:Connect(function()
                if TooltipAvailable then
                    Tooltip.New()
                end

                Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
            end)
        else
            Container.Origin.Visible = false
        end
    end

    local function SortTableByPriceAndName(TableToSort)
        table.sort(TableToSort, function(a, b)
            local Modules = {
                A = require(a),
                B = require(b),
            }

            return
                Modules.A.Config.Price == Modules.B.Config.Price and a.Name < b.Name
                or Modules.A.Config.Price < Modules.B.Config.Price
        end)
    end

    local function SetupBatch(Type: "Killer" | "Survivor" | "Emote")
        Ordered = {
            Owned = {},
            Unowned = {},
        }
        LO = 0

        for _, module in ReplicatedStorage[Type == "Emote" and "Assets" or "Characters"][Type.."s"]:GetChildren() do
            table.insert(Ordered[Utils.PlayerData.GetPlayerOwned(LocalPlayer, Type.."s."..module.Name) and "Owned" or "Unowned"], module)
        end

        SortTableByPriceAndName(Ordered.Owned)
        SortTableByPriceAndName(Ordered.Unowned)

        for _, module in ipairs(Ordered.Unowned) do
            SetupCard(module, Type)
        end
        for _, module in ipairs(Ordered.Owned) do
            SetupCard(module, Type)
        end
    end

    SetupBatch("Killer")
    SetupBatch("Survivor")
    SetupBatch("Emote")
end

function Shop.CreateBuyWindow(Module: ModuleScript, skin: boolean?, Parent: Frame, Card: Frame)
    if Shop.BuyWindow then
        return
    end

    local Window = BuyWindow.New(Shop, Module, skin, Parent, Card)
    Window.Purchased:Connect(function()
        if Utils.PlayerData.GetPlayerOwned(LocalPlayer, Shop.SelectedItem.Type.."s."..Shop.SelectedItem.Name) then
            Buttons.Purchase.Title.Text = "Purchased"
        else
            Buttons.Purchase.Title.Text = "Purchase"
        end
        Shop.ReloadContent()
        Shop.ReorderSkins()
    end)
    Window.Removed:Connect(function()
        Shop.BuyWindow = nil
    end)

    Shop.BuyWindow = Window
end

return Shop


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

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

local LocalPlayer = Players.LocalPlayer

local SideBar = {
    Enabled = true,
    ToggleDuration = 0.5,
    MenuModules = {},
}

type Button = {
    Render: string,
    Type: "Button" | "SmallButton",
    InvertedRender: string?,
    MenuModule: any,
    LayoutOrder: number,
    CloseOnOtherOpen: boolean,
    CloseOthersOnOpen: boolean,
}

local InvertedInfo = TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

local UI
local BarButtonPrefabs = {
    Button = Utils.Instance.FindFirstChild(script, "Button"),
    SmallButton = Utils.Instance.FindFirstChild(script, "SmallButton"),
}

local function TweenInverted(BarButton, Inverted: boolean)
    for _, Child in BarButton:GetChildren() do
        if #Child:GetChildren() > 0 then
            TweenInverted(Child, Inverted)
        end

        if Child:IsA("TextLabel") then
            local Value = Inverted and Color3.fromRGB(0, 0, 0) or Color3.fromRGB(255, 255, 255)
            TweenService:Create(Child, InvertedInfo, {TextColor3 = Value}):Play()
        elseif Child:IsA("ImageLabel") or Child:IsA("ImageButton") then
            local Value = not Inverted and Color3.fromRGB(0, 0, 0) or Color3.fromRGB(255, 255, 255)
            local Transp = 0
            if Child.Name:lower():find("inverted") then
                Transp = Inverted and 0 or 1
            elseif Child.Parent.Name ~= "Holder" then
                continue
            end

            TweenService:Create(Child, InvertedInfo, {ImageTransparency = Transp, BackgroundColor3 = Value}):Play()
        end
    end
end

function SideBar.Toggle(visible: boolean)
    UI.Visible = visible
    SideBar.Enabled = visible

    for _, m in SideBar.MenuModules do
        if not m.Button then
            continue
        end
        TweenInverted(m.Button, false)
    end
end

function SideBar.CloseAll(Remaining: any)
    for _, menu in SideBar.MenuModules do
        if menu == Remaining or not menu.CloseOnOtherOpen or not menu.UIInstance then continue end
        if menu.Opened then
            menu.Close(SideBar.ToggleDuration)
            if not menu.Button then
                continue
            end
            TweenInverted(menu.Button, false)
        end
    end
end

function SideBar.OpenMenu(Menu)
    Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
    if not Menu.Opened then
        if Menu.Open(SideBar.ToggleDuration) == "Prevented" then
            return
        end
        if Menu.Button then
            TweenInverted(Menu.Button, true)
        end
        if Menu.CloseOthersOnOpen then
            SideBar.CloseAll(Menu)
        end
    else
        Menu.Close(SideBar.ToggleDuration)
        if Menu.Button then
            TweenInverted(Menu.Button, false)
        end
    end
end

function SideBar:Init()
    UI = Utils.Instance.FindFirstChild(script, "SideBarUI"):Clone()
    UI.Parent = Utils.Instance.FindFirstChild(LocalPlayer.PlayerGui, "SideBar")

    local MoneyLabel = UI.Money.CurrentBalance

    local Cash = Utils.PlayerData.GetPlayerStat(LocalPlayer, "Currency.Money", false)
    MoneyLabel.Text = tostring(Cash.Value).." $"
    Cash.Changed:Connect(function(value: number)
        MoneyLabel.Text = tostring(value).." $"
    end)

    local ButtonContainer = UI.Buttons

    local PlayerUIModules = LocalPlayer.PlayerScripts.UI

    local Buttons: {[string]: Button} = {
        Shop = {
            Render = "rbxassetid://140391842164127",
            Type = "Button",
            InvertedRender = "rbxassetid://106935872381429",
            MenuModule = PlayerUIModules.Shop,
            LayoutOrder = 0,
            CloseOnOtherOpen = true,
            CloseOthersOnOpen = true,
        },
        Inventory = {
            Render = "rbxassetid://124486892591913",
            Type = "Button",
            InvertedRender = "rbxassetid://97795375278271",
            MenuModule = PlayerUIModules.Inventory,
            LayoutOrder = 1,
            CloseOnOtherOpen = true,
            CloseOthersOnOpen = true,
        },
        Achievements = {
            Render = "rbxassetid://113899511427023",
            Type = "Button",
            InvertedRender = "rbxassetid://70778277265275",
            MenuModule = PlayerUIModules.AchievementsMenu,
            LayoutOrder = 2,
            CloseOnOtherOpen = true,
            CloseOthersOnOpen = true,
        },
        Stats = {
            Render = "rbxassetid://130488549713780",
            Type = "Button",
            InvertedRender = "rbxassetid://72911514685580",
            MenuModule = PlayerUIModules.StatsMenu,
            LayoutOrder = 3,
            CloseOnOtherOpen = true,
            CloseOthersOnOpen = true,
        },
        Spectate = {
            Render = "rbxassetid://89862280432932",
            Type = "Button",
            InvertedRender = "rbxassetid://134398825115015",
            MenuModule = PlayerUIModules.Spectate,
            LayoutOrder = 4,
            CloseOnOtherOpen = false,
            CloseOthersOnOpen = false,
        },
        Settings = {
            Render = "rbxassetid://95524701319726",
            Type = "Button",
            InvertedRender = "rbxassetid://113951492478970",
            MenuModule = PlayerUIModules.Settings,
            LayoutOrder = 5,
            CloseOnOtherOpen = true,
            CloseOthersOnOpen = true,
        },
        Credits = {
            Render = "rbxassetid://82434587437333",
            Type = "Button",
            InvertedRender = "rbxassetid://113410904986274",
            MenuModule = PlayerUIModules.Credits,
            LayoutOrder = 6,
            CloseOnOtherOpen = true,
            CloseOthersOnOpen = true,
        },
        Rules = {
            Render = "rbxassetid://83147016587963",
            Type = "SmallButton",
            InvertedRender = "rbxassetid://90249013398230",
            MenuModule = PlayerUIModules.RulesMenu,
            LayoutOrder = 7,
            CloseOnOtherOpen = true,
            CloseOthersOnOpen = true,
        },
        Changelog = {
            Render = "rbxassetid://82434587437333",
            Type = "SmallButton",
            InvertedRender = "rbxassetid://113410904986274",
            MenuModule = PlayerUIModules.Changelog,
            LayoutOrder = 8,
            CloseOnOtherOpen = true,
            CloseOthersOnOpen = true,
        },
    }

    for name, button: Button in Buttons do
        local BarButton = BarButtonPrefabs[button.Type]:Clone()
        BarButton.Name = name
        BarButton.Icon.Image = button.Render
        BarButton.Button.Image = button.Render
        BarButton.InvertedIcon.Image = button.InvertedRender or BarButton.InvertedIcon.Image
        BarButton.Inverted.Image = button.InvertedRender or BarButton.Inverted.Image
        BarButton.Title.Holder.Label.Text = name
        BarButton.LayoutOrder = button.LayoutOrder
        local Menu = require(button.MenuModule)
        Menu.CloseOnOtherOpen = button.CloseOnOtherOpen
        Menu.CloseOthersOnOpen = button.CloseOthersOnOpen
        BarButton.Button.MouseButton1Click:Connect(function()
            SideBar.OpenMenu(Menu)
        end)
        Menu.Button = BarButton

        --TODO: replace these variables only if the size is the hidden one to handle resolution changes
        local OriginalSize = BarButton.Size
        local HoverSize = UDim2.fromScale(OriginalSize.X.Scale * 1.2, OriginalSize.Y.Scale * 1.2)

        SideBar.MenuModules[name] = Menu
        BarButton.Parent = ButtonContainer

        local OriginalYLabelBound = BarButton.Title.Holder.Label.AbsoluteSize.Y
        BarButton.MouseEnter:Connect(function()
            Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
            TweenService:Create(BarButton, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {Size = HoverSize}):Play()
            -- Why does `TextLabel.AutomaticSize` not fucking work with `TextLabel.TextScaled`? No idea! I'm mentally insane! :D
            -- `TextLabel.TextSize` doesn't either. What world are we living in?
            --
            -- reading this 2 months later makes me realize i was a dumbass -Dys 2026
            local Size = TextService:GetTextSize(BarButton.Title.Holder.Label.Text, OriginalYLabelBound, BarButton.Title.Holder.Label.Font, BarButton.Title.Holder.Label.AbsoluteSize)
            TweenService:Create(BarButton.Title.Holder, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
                Position = UDim2.new(0.15, Size.X + 56, 0.5, 0)
            }):Play()
        end)
        BarButton.MouseLeave:Connect(function()
            Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
            TweenService:Create(BarButton, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {Size = OriginalSize}):Play()
            TweenService:Create(BarButton.Title.Holder, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
                Position = UDim2.new(0, 0, 0.5, 0)
            }):Play()
        end)

        Utils.Character.ObserveCharacter(LocalPlayer, function(char: Model)
            if not Menu.UIInstance then
                return
            end
            Menu.Toggle(char:FindFirstChild("Role").Value == "Spectator")
        end)
    end

    Utils.Character.ObserveCharacter(LocalPlayer, function(char: Model)
        SideBar.Toggle(char:FindFirstChild("Role").Value == "Spectator")
    end)
end

return SideBar


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

local Sounds = require(ReplicatedStorage.Modules.Sounds)
local SideBar = require(script.Parent.SideBar)
local Utils = require(ReplicatedStorage.Modules.Utils)

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

local Spectate = {
    Opened = false,
    Enabled = true,
    UIInstance = nil,

    PlayersAvailable = {},
    CurSelected = 1,
}

local UIParent = Utils.Instance.FindFirstChild(LocalPlayer.PlayerGui, "Menus")
local ButtonTweenInfo = TweenInfo.new(0.35, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)

function Spectate:Init()
    if Spectate.UIInstance then
        Spectate.UIInstance.Visible = false
        return
    end

    local UI = Utils.Instance.FindFirstChild(script, "SpectateUI")
    UI.Visible = false
    UI.Parent = UIParent
    Spectate.UIInstance = UI

    UI.Buttons.Left.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
        TweenService:Create(UI.Buttons.Left, ButtonTweenInfo, {Size = UDim2.fromScale(0.14, 0.44)}):Play()
    end)
    UI.Buttons.Right.MouseEnter:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHover, {Volume = 0.2})
        TweenService:Create(UI.Buttons.Right, ButtonTweenInfo, {Size = UDim2.fromScale(0.14, 0.44)}):Play()
    end)
    UI.Buttons.Left.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
        TweenService:Create(UI.Buttons.Left, ButtonTweenInfo, {Size = UDim2.fromScale(0.12, 0.377)}):Play()
    end)
    UI.Buttons.Right.MouseLeave:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonHoverStop, {Volume = 0.2})
        TweenService:Create(UI.Buttons.Right, ButtonTweenInfo, {Size = UDim2.fromScale(0.12, 0.377)}):Play()
    end)

    UI.Buttons.Left.MouseButton1Click:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
        Spectate.ChangeSelection(-1)
    end)
    UI.Buttons.Right.MouseButton1Click:Connect(function()
        Sounds.PlaySound(Sounds.CommonlyUsedSounds.ButtonPress, {Volume = 0.8})
        Spectate.ChangeSelection(1)
    end)

    RunService.PreRender:Connect(function(_delta: number)
        Spectate._Update()
    end)

    local function OnCharacterAdded(Player: Player, Character: Model)
        local Role = Character:FindFirstChild("Role")

        if Role.Value == "Spectator" then

            local FoundPlayer = table.find(Spectate.PlayersAvailable, Player.Name)
            if FoundPlayer then

                table.remove(Spectate.PlayersAvailable, FoundPlayer)

                if Spectate.CurSelected <= FoundPlayer then
                    Spectate.ChangeSelection(-1)
                end

            end

        elseif not table.find(Spectate.PlayersAvailable, Player.Name) then

            table.insert(Spectate.PlayersAvailable, Player.Name)
            Spectate.ChangeSelection()
        end
    end

    Utils.Player.ObservePlayers(function(Player: Player)
        Utils.Character.ObserveCharacter(Player, function(Character: Model)
            OnCharacterAdded(Player, Character)
        end)
    end)

    Spectate.ChangeSelection()
end

function Spectate.Open()
    if not Spectate.Enabled or not SideBar.Enabled or RoundState.Value == "Lobby" then
        return "Prevented"
    end

    Spectate.Opened = true
    Spectate.UIInstance.Visible = true
    Spectate.ChangeSelection()
    Spectate._Update()

    return
end

function Spectate.Close()
    Spectate.Opened = false
    Spectate.UIInstance.Visible = false
    if LocalPlayer.Character and LocalPlayer.Character:FindFirstChildWhichIsA("Humanoid") then
        workspace.CurrentCamera.CameraSubject = LocalPlayer.Character.Humanoid
    end
end

function Spectate.Toggle(toggle: boolean)
    Spectate:Close()
    Spectate.Enabled = toggle
end

function Spectate.ChangeSelection(amount: number?)
    amount = amount or 0

    Spectate.CurSelected = Utils.Math.WrapNum(Spectate.CurSelected + amount, 1, #Spectate.PlayersAvailable)
    if #Spectate.PlayersAvailable > 0 and not Spectate.PlayersAvailable[Spectate.CurSelected] then
        Spectate.ChangeSelection(amount ~= 0 and amount or 1)
    end
end

function Spectate._Update()
    if not Spectate.UIInstance.Visible then
        return
    end

    if #Spectate.PlayersAvailable <= 0 then
        Spectate.UIInstance.PlayerText.DisplayName.Text = "PlayerDisplayName"
        Spectate.UIInstance.PlayerText.Username.Text = "PlayerUsername"
        Spectate.UIInstance.PlayerText.CharacterName.Text = "CharacterName (SkinName)"

        Spectate.UIInstance.PlayerText.Health.Text = "100%"
        Spectate.UIInstance.PlayerText.Health.TextColor3 = Color3.fromRGB(75, 255, 75)

        return
    end

    local Char = workspace.Players:FindFirstChild(Spectate.PlayersAvailable[Spectate.CurSelected])
    if not Char then
        return
    end

    local Role = Char:FindFirstChild("Role")
    if Role.Value == "Spectator" then
        return
    end

    local Humanoid = Char:FindFirstChildWhichIsA("Humanoid")
    workspace.CurrentCamera.CameraSubject = Humanoid

    Spectate.UIInstance.PlayerText.DisplayName.Text = Players:GetPlayerFromCharacter(Char).DisplayName
    Spectate.UIInstance.PlayerText.Username.Text = Char.Name

    local CharText = Char:GetAttribute("CharacterName")
    if Char:GetAttribute("CharacterSkinName") then
        CharText = CharText.." ("..Char:GetAttribute("CharacterSkinName")..")"
    end
    Spectate.UIInstance.PlayerText.CharacterName.Text = CharText

    local HealthPercentageMagnitude = Humanoid.Health / Humanoid.MaxHealth
    Spectate.UIInstance.PlayerText.Health.TextColor3 = Color3.fromRGB(
        Utils.Math.MapToRange(HealthPercentageMagnitude, 0, 1, 1, 0) * 180 + 75,
        HealthPercentageMagnitude * 180 + 75,
        75
    )
    Spectate.UIInstance.PlayerText.Health.Text = tostring(HealthPercentageMagnitude * 100).."%"
end

return Spectate