local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local Config = require(Avalog.Config)
local States = require(Avalog.SourceCode.Client.UI.States)
local Categories = require(Avalog.SourceCode.Shared.Categories)

local Children = Fusion.Children

local CatalogItemsTab = require(script.Parent.CatalogItemsTab)
local SkinTab = require(script.Parent.SkinTab)
local BuildTab = require(script.Parent.BuildTab)
local EquippedItemsTab = require(script.Parent.EquippedItemsTab)
local InventoryItemsTab = require(script.Parent.InventoryItemsTab)
local PrimaryItemsTab = require(script.Parent.PrimaryItemsTab)

local TAB_COMPONENTS = {
	CatalogItemsTab = CatalogItemsTab,
	SkinTab = SkinTab,
	BuildTab = BuildTab,
	EquippedItemsTab = EquippedItemsTab,
	InventoryItemsTab = InventoryItemsTab,
	PrimaryItemsTab = PrimaryItemsTab,
}

export type Props = { [string]: any } & {}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		CatalogItemsTab = CatalogItemsTab,
		SkinTab = SkinTab,
		BuildTab = BuildTab,
		EquippedItemsTab = EquippedItemsTab,
		InventoryItemsTab = InventoryItemsTab,
		PrimaryItemsTab = PrimaryItemsTab,
	})
	local Theme = OnyxUI.Themer.Theme:now()
	local Modes = Config:Get().EditModes

	local ActiveMode = Scope:Computed(function(Use)
		local ModeValue = Use(States.Catalog.Mode)
		return Modes[ModeValue]
	end)
	local ActiveCategory = Scope:Computed(function(Use)
		local ActiveModeValue = Use(ActiveMode)
		local CategoryValue = Use(States.Catalog.Category)

		return Categories[ActiveModeValue][CategoryValue]
	end)
	local ActiveTab = Scope:Computed(function(Use)
		local ActiveModeValue = Use(ActiveMode)
		local CategoryValue = Use(ActiveCategory)

		if ActiveModeValue == "Marketplace" then
			if CategoryValue.Name == "Featured" then
				return "PrimaryItemsTab"
			else
				return "CatalogItemsTab"
			end
		elseif ActiveModeValue == "Inventory" then
			return "InventoryItemsTab"
		elseif ActiveModeValue == "Customize" then
			if CategoryValue and CategoryValue.Name then
				if CategoryValue.Name == "Skin" then
					return "SkinTab"
				elseif CategoryValue.Name == "Build" then
					return "BuildTab"
				elseif CategoryValue.Name == "Equipped" then
					return "EquippedItemsTab"
				end
			end
		end

		return nil
	end)

	return Scope:Frame(OnyxUI.Util.CombineProps(Props, {
		Name = "Tab",
		FlexMode = Enum.UIFlexMode.Fill,
		Padding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0.75"]))
		end),
		ListEnabled = true,
		ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
		ListVerticalFlex = Enum.UIFlexAlignment.Fill,
		ClipsDescendants = true,

		[Children] = {
			-- Only render the active tab
			Scope:Computed(function(Use, Scope)
				local TabComponent = TAB_COMPONENTS[Use(ActiveTab)]

				if TabComponent then
					return TabComponent(Scope, {
						Shown = true,
					})
				end
			end),
		},
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local States = require(Avalog.SourceCode.Client.UI.States)
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)

local Children = Fusion.Children

export type Props = { [string]: any } & {
	Options: Fusion.UsedAs<{ string }>?,
	Selection: Fusion.UsedAs<number>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components)
	local Theme = OnyxUI.Themer.Theme:now()

	local Options = OnyxUI.Util.Fallback(Props.Options, {})
	local Selection = Scope:EnsureValue(OnyxUI.Util.Fallback(Props.Selection, 1))

	Scope:Observer(Options):onChange(function()
		Selection:set(1)
	end)

	local Tabs = Scope:Scroller(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		ListEnabled = true,
		ListFillDirection = Enum.FillDirection.Horizontal,
		ListPadding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0"]))
		end),
		ScrollingDirection = Enum.ScrollingDirection.X,
		AutomaticSize = Enum.AutomaticSize.XY,
		AutomaticCanvasSize = Enum.AutomaticSize.X,
		CanvasSize = UDim2.fromScale(1.5, 0),
		ScrollBarThickness = 0,
		ScrollBarImageTransparency = 1,

		[Children] = {
			Scope:ForValues(Options, function(Use, Scope, Option)
				local OptionsValue = Use(Options)

				return OnyxUI.Themer.Theme:is(Fusion.peek(States.Theme)):during(function()
					return Scope:Button {
						Name = "OptionButton",
						Content = Scope:Computed(function(Use)
							return { Option }
						end),
						Style = Scope:Computed(function(Use)
							if Use(Options)[Use(Selection)] == Option then
								return "Filled"
							else
								return "Ghost"
							end
						end),
						Color = Theme.Colors.BaseContent.Main,
						SizeVariant = "Medium",
						LayoutOrder = Scope:Computed(function()
							return table.find(OptionsValue, Option) or 0
						end),

						OnActivated = function()
							Selection:set(table.find(OptionsValue, Option))
						end,
					}
				end)
			end),
		},
	}))

	Scope:Observer(Options):onChange(function()
		Tabs.CanvasPosition = Vector2.new(0, 0)
	end)

	return Tabs
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local Version = require(Avalog.SourceCode.Shared.Version)

local Children = Fusion.Children

export type Props = {
	Open: Fusion.UsedAs<boolean>?,
	Link: Fusion.UsedAs<string>?,
	Logo: Fusion.UsedAs<string>?,
	UpdateAvailable: Fusion.UsedAs<boolean>?,
} & { [string]: any }

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components)
	local Theme = OnyxUI.Themer.Theme:now()

	local Open = Scope:EnsureValue(OnyxUI.Util.Fallback(Props.Open, false))
	local Link = OnyxUI.Util.Fallback(Props.Link, "https://devforum.com")
	local Logo = OnyxUI.Util.Fallback(Props.Logo, "")
	local UpdateAvailable = OnyxUI.Util.Fallback(Props.UpdateAvailable, false)
	local Color = OnyxUI.Util.Fallback(Props.Color, Theme.Colors.Neutral.Dark)

	return Scope:Frame(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		ListEnabled = true,
		ListFillDirection = Enum.FillDirection.Horizontal,
		ListVerticalAlignment = Enum.VerticalAlignment.Center,
		ListPadding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0.25"]))
		end),
		BackgroundColor3 = Color,
		BackgroundTransparency = 0,
		CornerRadius = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.CornerRadius["Full"]))
		end),
		Padding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0.25"]))
		end),

		[Children] = {
			Scope:IconButton {
				Image = Logo,
				ContentSize = Theme.TextSize["1.25"],
				Color = Scope:Computed(function(Use)
					if Use(UpdateAvailable) then
						return Use(Theme.Colors.Warning.Main)
					else
						return Use(Theme.Colors.Neutral.Main)
					end
				end),
				ContentColor = Scope:Computed(function(Use)
					if Use(UpdateAvailable) then
						return OnyxUI.Util.Colors.Black
					else
						return OnyxUI.Util.Colors.White
					end
				end),
				PaddingLeft = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				PaddingRight = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				PaddingTop = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				PaddingBottom = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				CornerRadius = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.CornerRadius["Full"]))
				end),
				StrokeThickness = Theme.StrokeThickness["1"],
				StrokeEnabled = true,
				StrokeColor = Theme.Colors.Neutral.Dark,

				OnActivated = function()
					local OpenValue = Fusion.peek(Open)

					Open:set(not OpenValue)
				end,
			},
			Scope:Frame {
				BackgroundColor3 = Theme.Colors.Neutral.Main,
				BackgroundTransparency = 0,
				CornerRadius = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.CornerRadius["Full"]))
				end),
				Scale = Scope:Spring(
					Scope:Computed(function(Use)
						local OpenValue = Use(Open)

						if OpenValue == true then
							return 1
						else
							return 0
						end
					end),
					Theme.SpringSpeed["2"],
					Theme.SpringDampening["1.5"]
				),
				ListEnabled = true,
				ListFillDirection = Enum.FillDirection.Horizontal,
				ListVerticalAlignment = Enum.VerticalAlignment.Center,
				ListPadding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0"]))
				end),
				Padding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				Visible = Open,

				[Children] = {
					Scope:Text {
						Text = `Avalog {Version} • `,
					},
					Scope:TextInput {
						Text = Scope:Computed(function(Use)
							local LinkValue = Use(Link)

							return `{LinkValue}`
						end),
						TextTransparency = 0,
						TextEditable = false,
						Disabled = true,
						StrokeEnabled = false,
						PaddingLeft = UDim.new(),
						PaddingRight = UDim.new(),
						PaddingTop = UDim.new(),
						PaddingBottom = UDim.new(),
					},
				},
			},
		},
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)
local ServiceProvider = require(Avalog.SourceCode.Client.ServiceProvider)
local Version = require(Avalog.SourceCode.Shared.Version)
local UpdatesIndicator = require(script.Parent.Parent.Components.UpdatesIndicator)
local CatalogMenu = require(script.Parent.Parent.Components.CatalogMenu)
local AvatarViewport = require(script.Parent.Parent.Components.AvatarViewport)

local Children = Fusion.Children

export type Props = {
	Parent: Fusion.UsedAs<Instance>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, OnyxUI.Util, OnyxUI.Components, {
		CatalogMenu = CatalogMenu,
		AvatarViewport = AvatarViewport,
		UpdatesIndicator = UpdatesIndicator,
	})
	local Theme = OnyxUI.Themer.Theme:now()

	local AvatarManager = ServiceProvider:GetAvatarManager()

	return Scope:New "ScreenGui" {
		Name = script.Name,
		Parent = Props.Parent,
		DisplayOrder = 9,
		Enabled = States.Open,

		[Children] = {
			Scope:New "ScreenGui" {
				Name = "Backdrop",
				DisplayOrder = 9 - 1,
				ScreenInsets = Enum.ScreenInsets.None,

				[Children] = {
					Scope:Frame {
						AnchorPoint = Vector2.new(0.5, 0.5),
						Position = UDim2.fromScale(0.5, 0.5),
						Size = UDim2.fromScale(1, 1),
						BackgroundColor3 = Theme.Colors.Base.Main,
						BackgroundTransparency = Scope:Spring(
							Scope:Computed(function(Use)
								return (Use(States.Open) and 0) or 1
							end),
							Theme.SpringSpeed["1.5"],
							Theme.SpringDampening["1.5"]
						),
						Active = States.Open,
						Interactable = States.Open,
					},
				},
			},
			Scope:Frame {
				Name = "Menu",
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = Scope:Spring(
					Scope:Computed(function(Use)
						if Use(States.Open) then
							return UDim2.fromScale(0.5, 0.5)
						else
							return UDim2.fromScale(0.5, 0.5) + UDim2.fromOffset(0, Use(Theme.Spacing["4"]))
						end
					end),
					Theme.SpringSpeed["0.5"],
					Theme.SpringDampening["1.5"]
				),
				Size = UDim2.fromScale(1, 1),
				AutomaticSize = Enum.AutomaticSize.None,
				ListEnabled = true,
				ListFillDirection = Enum.FillDirection.Horizontal,
				ListVerticalFlex = Enum.UIFlexAlignment.Fill,
				ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
				ListPadding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0"]))
				end),
				Active = States.Open,
				Interactable = States.Open,

				[Children] = {
					Scope:CatalogMenu {
						OnExit = function()
							AvatarManager:UpdateServer(Fusion.peek(States.Avatar.Data))

							States.Open:set(false)
						end,
					},
					Scope:AvatarViewport {},
				},
			},
			Scope:UpdatesIndicator {
				Link = "devforum.roblox.com/t/3513102",
				Logo = "rbxassetid://109049301067448",
				UpdateAvailable = Scope:Computed(function(Use)
					return Version ~= Use(States.CloudConfig).latestVersion
				end),
				AnchorPoint = Vector2.new(0, 1),
				Position = Scope:Computed(function(Use)
					return UDim2.fromScale(0, 1) + UDim2.fromOffset(Use(Theme.Spacing["1"]), -Use(Theme.Spacing["1"]))
				end),
			},
		},
	}
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local Fusion = require(Avalog.Parent.Fusion)
local OnyxUI = require(Avalog.Parent.OnyxUI)
local States = require(Avalog.SourceCode.Client.UI.States)
local StateExtensions = require(Avalog.SourceCode.Client.UI.StateExtensions)
local DarkTheme = require(Avalog.SourceCode.Client.UI.Themes.Dark)
local ServiceProvider = require(Avalog.SourceCode.Client.ServiceProvider)
local LocalAvatar = require(Avalog.SourceCode.Client.Avatar.LocalAvatar)

local AvalogMenu = require(script.Parent.AvalogMenu)

return function(Target: Instance)
	local Scope = Fusion.scoped(Fusion, OnyxUI.Components, {
		AvalogMenu = AvalogMenu,
	})
	local Theme = OnyxUI.Themer.Theme:now()

	StateExtensions:Initialize()

	ServiceProvider:Setup({
		AvatarManager = LocalAvatar,
	})

	States.Open:set(true)

	States.Theme:set(DarkTheme)

	OnyxUI.Themer.Theme:is(Fusion.peek(States.Theme)):during(function()
		return Scope:AvalogMenu {
			Parent = Target,
		}
	end)

	Scope:Button {
		Content = { "Toggle" },
		Parent = Target,
		AnchorPoint = Vector2.new(0, 0),
		Position = Scope:Computed(function(Use)
			return UDim2.new(0, Use(Theme.Spacing["1"]), 0, Use(Theme.Spacing["1"]))
		end),

		OnActivated = function()
			States.Open:set(not Fusion.peek(States.Open))
		end,
	}

	return function()
		Scope:doCleanup()
		StateExtensions:Destroy()
	end
end


local Players = game:GetService("Players")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local States = require(script.Parent.Parent.States)
local Fusion = require(Avalog.Parent.Fusion)
local CatalogItems = require(Avalog.SourceCode.Client.UI.StateExtensions.CatalogItems)
local ServiceProvider = require(Avalog.SourceCode.Client.ServiceProvider)
local HumanoidDescriber = require(Avalog.Parent.HumanoidDescriber)
local DeepEquals = require(Avalog.SourceCode.Shared.Utils.DeepEquals)

local Avatar = {
	_Connections = {},
}

function Avatar:_LogHistoryEntry(NewData: HumanoidDescriber.Data)
	local HistoryDataValue = Fusion.peek(States.Avatar.DataHistory)

	local AlreadyExists = false
	for _, DataEntry in ipairs(HistoryDataValue) do
		if DeepEquals(DataEntry, NewData) then
			AlreadyExists = true
			break
		end
	end

	if AlreadyExists == false then
		table.insert(HistoryDataValue, NewData)
		States.Avatar.UndoHistory:set({})
	end

	States.Avatar.DataHistory:set(HistoryDataValue)
end

function Avatar:_UpdateEquippedBundles()
	local EquippedAssetIdsValue = Fusion.peek(States.Avatar.EquippedAssetIds)
	local BundleIds = {}

	for _, AssetId in ipairs(EquippedAssetIdsValue) do
		local BundleId = CatalogItems:GetBundleIdFromAsset(AssetId)
		if BundleId and (table.find(BundleIds, BundleId) == nil) then
			table.insert(BundleIds, BundleId)
		end
	end

	States.Avatar.EquippedBundleIds:set(BundleIds)
end

function Avatar:_UpdateEquippedAssetIds()
	local AvatarManager = ServiceProvider:GetAvatarManager()
	local AssetIdsValue = Fusion.peek(States.Avatar.AssetIds)
	local UnequippedAssetIds = {}

	States.Avatar.EquippedAssetIds:set(AssetIdsValue)

	for _, AssetId in ipairs(AssetIdsValue) do
		local CatalogInfo = CatalogItems:GetItem(AssetId, Enum.InfoType.Asset)
		if CatalogInfo and (CatalogInfo.AssetType == Enum.AvatarAssetType.EmoteAnimation) then
			if AvatarManager.HumanoidDescriber:GetEquippedEmote(CatalogInfo.Name) == nil then
				table.insert(UnequippedAssetIds, AssetId)
			end
		end
	end

	for _, AssetId in ipairs(UnequippedAssetIds) do
		local Index = table.find(AssetIdsValue, AssetId)
		if Index ~= nil then
			table.remove(AssetIdsValue, Index)
		end
	end

	States.Avatar.EquippedAssetIds:set(AssetIdsValue)
end

function Avatar:_HandleAvatar()
	local AvatarManager = ServiceProvider:GetAvatarManager()

	if AvatarManager.Character == nil then
		-- Reset all states
		States.Avatar.Character:set(nil)
		States.Avatar.Loaded:set(false)
		States.Avatar.Data:set(nil)
		States.Avatar.DataHistory:set({})
		States.Avatar.UndoHistory:set({})
		States.Avatar.AssetIds:set({})
		States.Avatar.EquippedAssetIds:set({})
		States.Avatar.EquippedBundleIds:set({})
		return
	end

	States.Avatar.Loaded:set(false)

	local Data = AvatarManager.HumanoidDescriber:GetData()

	local function Update(NewData)
		States.Avatar.Data:set(NewData)
		self:_LogHistoryEntry(NewData)

		States.Avatar.AssetIds:set(AvatarManager.HumanoidDescriber:GetAssets())
		self:_UpdateEquippedAssetIds()
		self:_UpdateEquippedBundles()

		AvatarManager:GetUnownedItems()
	end

	States.Avatar.Character:set(AvatarManager.Character)

	AvatarManager.HumanoidDescriber.Updated:Connect(function(_, NewData)
		Update(NewData)
	end)
	task.spawn(function()
		Update(Data)
	end)

	States.Avatar.Loaded:set(true)
end

function Avatar:Stop()
	for _, Connection: RBXScriptConnection in ipairs(self._Connections) do
		Connection:Disconnect()
	end
end

function Avatar:Start()
	local AvatarManager = ServiceProvider:GetAvatarManager()
	table.insert(
		Avatar._Connections,
		AvatarManager.Updated:Connect(function()
			Avatar:_HandleAvatar()
		end)
	)

	Avatar:_HandleAvatar()
end

return Avatar


local AssetService = game:GetService("AssetService")
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local Future = require(Avalog.Parent.Future)
local AssetTypeFromId = require(Avalog.SourceCode.Shared.Utils.AssetTypeFromId)

export type CatalogItem = {
	Id: number,
	Name: string,
	Type: Enum.AvatarItemType,
	AssetType: Enum.AvatarAssetType?,
	Price: number,
	Offsale: boolean,
}

local INFO_TYPE_TABLES = {
	[Enum.InfoType.Asset] = "KnownAssets",
	[Enum.InfoType.Bundle] = "KnownBundles",
}
local INFO_TYPE_AVATAR_ITEM_TYPES = {
	[Enum.InfoType.Asset] = Enum.AvatarItemType.Asset,
	[Enum.InfoType.Bundle] = Enum.AvatarItemType.Bundle,
}

local CatalogItems = {
	KnownAssets = {},
	KnownBundles = {},
	OwnedAssets = {},
	OwnedBundles = {},
	BundleDetails = {},
	BundleAssets = {},
}

function CatalogItems:GetBundleIdFromAsset(AssetId: number)
	for BundleId, AssetIds in pairs(self.BundleAssets) do
		if table.find(AssetIds, AssetId) ~= nil then
			return BundleId
		end
	end

	return nil
end

function CatalogItems:GetBundle(BundleId: number)
	if self.BundleDetails[BundleId] == nil then
		local Success, Result = Future.Try(function()
			return AssetService:GetBundleDetailsAsync(BundleId)
		end):Await()

		if Success then
			self.BundleAssets[BundleId] = {}
			for _, Item in ipairs(Result.Items) do
				if Item.Type == "Asset" then
					table.insert(self.BundleAssets[BundleId], Item.Id)
				end
			end

			self.BundleDetails[BundleId] = Result
		end
	end

	return self.BundleDetails[BundleId]
end

function CatalogItems:OwnsItem(Id: number, Type: Enum.AvatarItemType)
	local ItemTypeTables = {
		[Enum.AvatarItemType.Asset] = self.OwnedAssets,
		[Enum.AvatarItemType.Bundle] = self.OwnedBundles,
	}
	local RelevantTable = ItemTypeTables[Type]

	if RelevantTable then
		if table.find(RelevantTable, Id) == nil then
			local Success, Result = Future.Try(function()
				if Type == Enum.AvatarItemType.Asset then
					return MarketplaceService:PlayerOwnsAsset(Players.LocalPlayer, Id)
				elseif Type == Enum.AvatarItemType.Bundle then
					return MarketplaceService:PlayerOwnsBundle(Players.LocalPlayer, Id)
				else
					return nil
				end
			end):Await()
			if Success and Result then
				table.insert(RelevantTable, Id)
			end
		end

		return table.find(RelevantTable, Id) ~= nil
	end

	return false
end

function CatalogItems:GetItem(Id: number, InfoType: Enum.InfoType): CatalogItem?
	local RelevantTable = self[INFO_TYPE_TABLES[InfoType]]
	local AvatarItemType = INFO_TYPE_AVATAR_ITEM_TYPES[InfoType]

	local ExistingEntry = self:GetExistingItem(Id, InfoType)

	if ExistingEntry then
		return ExistingEntry
	else
		local Success, Result = Future.Try(function()
			return MarketplaceService:GetProductInfo(Id, InfoType)
		end):Await()
		if Success then
			local Item = {
				Id = Result.AssetId,
				Name = Result.Name,
				Price = Result.PriceInRobux,
				Type = AvatarItemType,
				AssetType = AssetTypeFromId(Result.AssetTypeId),
				Offsale = not Result.IsForSale,
			}

			RelevantTable[Id] = Item
			return Item
		end
	end

	return nil
end

function CatalogItems:GetExistingItem(Id: number, InfoType: Enum.InfoType): CatalogItem?
	local RelevantTable = self[INFO_TYPE_TABLES[InfoType]]
	local AvatarItemType = INFO_TYPE_AVATAR_ITEM_TYPES[InfoType]

	if RelevantTable and AvatarItemType then
		local KnownEntry = RelevantTable[Id]
		if KnownEntry then
			return KnownEntry
		end
	end

	return nil
end

return CatalogItems


local AvatarEditorService = game:GetService("AvatarEditorService")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local Future = require(Avalog.Parent.Future)
local Signal = require(Avalog.Parent.Signal)
local CatalogItems = require(script.Parent.CatalogItems)

export type SearchParams = {
	SearchKeyword: string?,
	MinPrice: number?,
	MaxPrice: number?,
	SortType: Enum.CatalogSortType?,
	SortAggregation: Enum.CatalogSortAggregation?,
	CategoryFilter: Enum.CatalogCategoryFilter?,
	SalesTypeFilter: Enum.SalesTypeFilter?,
	BundleTypes: { Enum.BundleType }?,
	AssetTypes: { Enum.AvatarAssetType }?,
	IncludeOffSale: boolean?,
	CreatorName: string?,
	Limit: number?,
}

local CatalogSearch = {
	Searches = {},
}

function CatalogSearch.Search(Params: SearchParams)
	local Search = {
		Results = {},
		Pages = nil,
		Updated = Signal.new(),
	}

	function Search:LoadNextPage()
		return Future.Try(function()
			assert(self.Pages ~= nil, "Pages does not exist.")

			self.Pages:AdvanceToNextPageAsync()

			return self.Pages:GetCurrentPage()
		end):After(function(Success, Result)
			if Success and Result then
				self:_LoadPage(Result)
			end
		end)
	end

	function Search:_LoadPage(Page: { [number]: any })
		for _, Entry in ipairs(Page) do
			local Tables = {
				Asset = CatalogItems.KnownAssets,
				Bundle = CatalogItems.KnownBundles,
			}
			local AppropriateTable = Tables[Entry.ItemType]

			if AppropriateTable then
				local Item: CatalogItems.CatalogItem = {
					Id = Entry.Id,
					Name = Entry.Name,
					Price = Entry.Price,
					Offsale = Entry.IsOffSale,
					Type = Enum.AvatarItemType[Entry.ItemType],
					AssetType = (Entry.AssetType and Enum.AvatarAssetType[Entry.AssetType]) or nil,
				}

				AppropriateTable[Entry.Id] = Item
			end

			table.insert(self.Results, Entry)
		end

		self.Updated:Fire()
	end

	function Search:Destroy()
		self.Results = {}
		self.Updated:Destroy()

		if self.Pages ~= nil then
			self.Pages:Destroy()
		end
	end

	CatalogSearch.Pages(Params):After(function(Success, Result)
		if Success then
			Search.Pages = Result
			Search:_LoadPage(Result:GetCurrentPage())
		end
	end)

	return Search
end

function CatalogSearch.Pages(Params: SearchParams)
	local CatalogSearchParams = CatalogSearch.Params(Params)

	return Future.Try(function()
		return AvatarEditorService:SearchCatalog(CatalogSearchParams)
	end)
end

function CatalogSearch.Params(Params: SearchParams)
	local CatalogSearchParams = CatalogSearchParams.new()

	for Key, Value in pairs(Params) do
		CatalogSearchParams[Key] = Value
	end

	return CatalogSearchParams
end

function CatalogSearch:Stop()
	for Hash, Search in pairs(self.Searches) do
		Search:Destroy()
		self.Searches[Hash] = nil
	end
end

return CatalogSearch


local RunService = game:GetService("RunService")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local States = require(script.Parent.Parent.States)
local Sift = require(Avalog.Parent.Sift)
local Types = require(Avalog.SourceCode.Shared.Types)

local Zap
if RunService:IsRunning() then
	Zap = require(Avalog.SourceCode.Shared.Zap.client)
end

local CloudConfig = {}

function CloudConfig.Start()
	if Zap ~= nil then
		Zap.CloudConfigReset.SetCallback(function()
			CloudConfig:FetchData()
		end)

		CloudConfig.FetchData()
	end
end

function CloudConfig.FetchData()
	if Zap == nil then
		return
	end

	local Data = Zap.GetCloudConfig.Call()
	if Data ~= nil then
		local SafeData = Sift.Dictionary.mergeDeep(Types.AvalogConfig.Fallback, Data)
		States.CloudConfig:set(SafeData)
	end
end

return CloudConfig


local AvatarEditorService = game:GetService("AvatarEditorService")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local States = require(script.Parent.Parent.States)
local Future = require(Avalog.Parent.Future)
local Signal = require(Avalog.Parent.Signal)
local HashDictionary = require(Avalog.SourceCode.Shared.Utils.HashDictionary)

local Inventory = {
	Searches = {},
}

function Inventory.Search(AssetTypes: { Enum.AvatarAssetType })
	local QueryHash = HashDictionary(AssetTypes)
	if Inventory.Searches[QueryHash] ~= nil then
		return Inventory.Searches[QueryHash]
	end

	local Search = {
		Results = {},
		Pages = nil,
		Updated = Signal.new(),
	}

	function Search:LoadNextPage()
		return Future.Try(function()
			assert(self.Pages ~= nil, "Pages does not exist.")
			assert(not self.Pages.IsFinished, "Pages is finished.")

			self.Pages:AdvanceToNextPageAsync()
			return self.Pages:GetCurrentPage()
		end):After(function(Success, Result)
			if Success and Result then
				self:_LoadPage(Result)
			end
		end)
	end

	function Search:_LoadPage(Page: { [number]: any })
		for _, Entry in ipairs(Page) do
			table.insert(self.Results, Entry)
		end

		self.Updated:Fire()
	end

	function Search:Destroy()
		self.Results = {}
		self.Updated:Destroy()

		if self.Pages ~= nil then
			self.Pages:Destroy()
		end
	end

	Inventory.Pages(AssetTypes):After(function(Success, Result)
		if Success then
			Search.Pages = Result
			Search:_LoadPage(Result:GetCurrentPage())
		end
	end)

	Inventory.Searches[QueryHash] = Search

	return Search
end

function Inventory.Pages(AssetTypes: { Enum.AvatarAssetType }?)
	return Future.Try(function()
		return AvatarEditorService:GetInventory(AssetTypes)
	end)
end

function Inventory:Start()
	AvatarEditorService.PromptAllowInventoryReadAccessCompleted:Connect(function(Result)
		if Result == Enum.AvatarPromptResult.Success then
			States.Inventory.AccessAllowed:set(true)
		else
			States.Inventory.AccessAllowed:set(false)
		end
	end)
end

return Inventory


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local States = require(script.Parent.Parent.States)
local Categories = require(Avalog.SourceCode.Shared.Categories)
local Fusion = require(Avalog.Parent.Fusion)
local Config = require(Avalog.Config)

local Navigation = {}

local Scope = Fusion.scoped(Fusion)

Navigation.Mode = Scope:Computed(function(Use)
	return Config:Get().EditModes[Use(States.Catalog.Mode)]
end)
Navigation.Category = Scope:Computed(function(Use)
	local ActiveModeValue = Use(Navigation.Mode)
	local CategoryValue = Use(States.Catalog.Category)

	return Categories[ActiveModeValue][CategoryValue]
end)
Navigation.SubCategory = Scope:Computed(function(Use)
	local CategoryValue = Use(Navigation.Category)
	local SubCategoryValue = Use(States.Catalog.SubCategory)

	if CategoryValue and CategoryValue.SubCategories then
		return CategoryValue.SubCategories[SubCategoryValue]
	else
		return nil
	end
end)

return Navigation


local Workspace = game:GetService("Workspace")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local Fusion = require(Avalog.Parent.Fusion)
local States = require(script.Parent.Parent.States)

local Out = Fusion.Out

local Scope = Fusion.scoped(Fusion)

local Screen = {}

Scope:Hydrate(Workspace.CurrentCamera) {
	[Out "ViewportSize"] = States.Screen.Resolution,
}

return Screen


local Avalog = script.Parent.Parent.Parent.Parent
local Bootloader = require(Avalog.Parent.Bootloader)

local function GetModules()
	local Modules = {}

	for _, Child in ipairs(script:GetChildren()) do
		if Child:IsA("ModuleScript") then
			table.insert(Modules, Child)
		end
	end

	return Modules
end

return Bootloader(GetModules())


local Avalog = script.Parent.Parent.Parent.Parent
local Fusion = require(Avalog.Parent.Fusion)
local Types = require(Avalog.SourceCode.Shared.Types)

local Scope = Fusion.scoped(Fusion)

local States = {
	Theme = Scope:Value(nil),
	Catalog = {
		SearchQuery = Scope:Value(""),
		Mode = Scope:Value(1),
		Category = Scope:Value(1),
		SubCategory = Scope:Value(1),
	},
	Avatar = {
		Data = Scope:Value(nil),
		DataHistory = Scope:Value({}),
		UndoHistory = Scope:Value({}),
		AssetIds = Scope:Value({}),
		EquippedAssetIds = Scope:Value({}),
		EquippedBundleIds = Scope:Value({}),
		Character = Scope:Value(nil),
		Loaded = Scope:Value(false),
	},
	Checkout = {
		Loading = Scope:Value(false),
	},
	Inventory = {
		AccessAllowed = Scope:Value(false),
	},
	Featured = {
		MessageShown = Scope:Value(true),
		PrefixItems = Scope:Value({}),
		Creators = Scope:Value({}),
	},
	CloudConfig = Scope:Value(Types.AvalogConfig.Fallback),
	Screen = {
		Resolution = Scope:Value(Vector2.new()),
	},
	Open = Scope:Value(false),
}

return States


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local ColorUtils = require(Avalog.Parent.ColorUtils)

local Util = OnyxUI.Util
local Themer = OnyxUI.Themer

local Scope = Fusion.scoped(Fusion)

return Themer.NewTheme(Scope, {
	Colors = {
		Primary = {
			Main = Color3.fromHex("fbfafd"),
		},
		Secondary = {
			Main = Color3.fromHex("fbfafd"),
		},
		Accent = {
			Main = Color3.fromHex("fbfafd"),
		},

		Neutral = {
			Main = Color3.fromHex("#242429"),
		},
		NeutralContent = {
			Main = Util.Colors.Stone["100"],
			Dark = ColorUtils.Darken(Util.Colors.Stone["100"], 0.2),
		},

		Base = {
			Main = Color3.fromHex("0A0B14"),
		},
		BaseContent = {
			Main = Color3.fromHex("fbfafd"),
		},

		Success = {
			Main = Util.Colors.Green["600"],
		},
		Error = {
			Main = Util.Colors.Red["500"],
		},
		Warning = {
			Main = Util.Colors.Amber["500"],
		},
		Info = {
			Main = Util.Colors.Cyan["400"],
		},
	},
	Font = {
		Body = "rbxasset://fonts/families/Montserrat.json",
		Heading = "rbxasset://fonts/families/Montserrat.json",
		Monospace = "rbxasset://fonts/families/Montserrat.json",
	},
	FontWeight = {
		Body = Enum.FontWeight.SemiBold,
		Bold = Enum.FontWeight.Bold,
		Heading = Enum.FontWeight.Bold,
	},
	TextSize = {
		Base = 18,
	},
	CornerRadius = {
		Base = 6,
	},
	StrokeThickness = {
		Base = 1,
	},
	Spacing = {
		Base = 16,
	},
	SpringSpeed = {
		Base = 60,
	},
	SpringDampening = {
		Base = 1.25,
	},
	Sound = {
		Hover = Scope:New "Sound" {
			SoundId = "rbxassetid://10066936758",
			Volume = 0,
		},
		Click = Scope:New "Sound" {
			SoundId = "rbxassetid://16480549841",
			Volume = 0,
		},
		Focus = Scope:New "Sound" {
			SoundId = "rbxassetid://16480549841",
			Volume = 0,
		},
		Switch = Scope:New "Sound" {
			SoundId = "rbxassetid://9119713951",
			Volume = 0,
		},
	},
	Emphasis = {
		Light = 0.1,
		Regular = 0.15,
		Strong = 0.5,
		Contrast = 1,
	},
})


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local ColorUtils = require(Avalog.Parent.ColorUtils)

local Util = OnyxUI.Util
local Themer = OnyxUI.Themer

local Scope = Fusion.scoped(Fusion)

return Themer.NewTheme(Scope, {
	Colors = {
		Primary = {
			Main = Color3.fromHex("#0A0B14"),
		},
		Secondary = {
			Main = Color3.fromHex("#0A0B14"),
		},
		Accent = {
			Main = Color3.fromHex("#0A0B14"),
		},

		Neutral = {
			Main = Color3.fromHex("#e3e2e6"),
			Light = Util.Colors.Stone["50"],
			Dark = Util.Colors.Stone["100"],
		},
		NeutralContent = {
			Main = Util.Colors.Stone["800"],
			Dark = ColorUtils.Darken(Util.Colors.Stone["800"], 0.2),
		},

		Base = {
			Main = Color3.fromHex("#FBFAFD"),
		},
		BaseContent = {
			Main = Color3.fromHex("#0A0B14"),
		},

		Success = {
			Main = Util.Colors.Green["500"],
		},
		Error = {
			Main = Util.Colors.Red["500"],
		},
		Warning = {
			Main = Util.Colors.Amber["500"],
		},
		Info = {
			Main = Util.Colors.Cyan["400"],
		},
	},
	Font = {
		Body = "rbxasset://fonts/families/Montserrat.json",
		Heading = "rbxasset://fonts/families/Montserrat.json",
		Monospace = "rbxasset://fonts/families/Montserrat.json",
	},
	FontWeight = {
		Body = Enum.FontWeight.SemiBold,
		Bold = Enum.FontWeight.Bold,
		Heading = Enum.FontWeight.Bold,
	},
	TextSize = {
		Base = 18,
	},
	CornerRadius = {
		Base = 6,
	},
	StrokeThickness = {
		Base = 1,
	},
	Spacing = {
		Base = 16,
	},
	SpringSpeed = {
		Base = 60,
	},
	SpringDampening = {
		Base = 1.25,
	},
	Sound = {
		Hover = Scope:New "Sound" {
			SoundId = "rbxassetid://10066936758",
			Volume = 0,
		},
		Click = Scope:New "Sound" {
			SoundId = "rbxassetid://16480549841",
			Volume = 0,
		},
		Focus = Scope:New "Sound" {
			SoundId = "rbxassetid://16480549841",
			Volume = 0,
		},
		Switch = Scope:New "Sound" {
			SoundId = "rbxassetid://9119713951",
			Volume = 0,
		},
	},
	Emphasis = {
		Light = 0.1,
		Regular = 0.15,
		Strong = 0.5,
		Contrast = 1,
	},
})


local Players = game:GetService("Players")

local Avalog = script.Parent.Parent.Parent
local Fusion = require(Avalog.Parent.Fusion)
local StateExtensions = require(Avalog.SourceCode.Client.UI.StateExtensions)
local OnyxUI = require(Avalog.Parent.OnyxUI)
local States = require(script.States)
local Config = require(Avalog.Config)

local AvalogMenu = require(script.ScreenGuis.AvalogMenu)

local UIService = {
	Scope = Fusion.scoped(Fusion),
}

function UIService:Start()
	StateExtensions:Initialize()
	StateExtensions:Start()

	States.Theme:set(Config:Get().Theme)

	OnyxUI.Themer.Theme:is(Fusion.peek(States.Theme)):during(function()
		AvalogMenu(self.Scope, {
			Parent = Players.LocalPlayer:WaitForChild("PlayerGui"),
		})
	end)
end

return UIService


local MarketplaceService = game:GetService("MarketplaceService")

local Avalog = script.Parent.Parent.Parent
local Future = require(Avalog.Parent.Future)
local RateLimit = require(Avalog.Parent.RateLimit)
local HumanoidDescriber = require(Avalog.Parent.HumanoidDescriber)
local Zap = require(Avalog.SourceCode.Shared.Zap.server)
local EnumSerDes = require(Avalog.Parent.EnumSerDes)
local PlayerDataStore = require(Avalog.SourceCode.Server.PlayerDataStorage.PlayerDataStore)
local Squash = require(Avalog.Parent.Squash)
local AvatarDataSerDes = require(Avalog.SourceCode.Server.PlayerDataStorage.SerDes.AvatarData)
local Config = require(Avalog.Config)

local CONFIG = Config:Get()
local RATE_LIMITS = {
	Update = RateLimit(3, 1),
	BulkPurchaseItems = RateLimit(1, 2),
}

local Avatar = {
	Avatars = {},
}

function Avatar:HandleProfile(Player: Player, Profile)
	if not Player:HasAppearanceLoaded() then
		Player.CharacterAppearanceLoaded:Wait()
	end

	Player.CharacterAppearanceLoaded:Connect(function()
		local Data = Profile.Data:Get()

		if Data.Avatar.Data ~= nil then
			local Cursor = Squash.frombuffer(Data.Avatar.Data)
			local AvatarData = AvatarDataSerDes.des(Cursor)

			Avatar:ApplyData(Player, AvatarData)
		end
	end)

	Profile.Data:Observe(function(_, Data)
		if Data.Avatar.Data then
			local Cursor = Squash.frombuffer(Data.Avatar.Data)
			local AvatarData = AvatarDataSerDes.des(Cursor)

			Avatar:ApplyData(Player, AvatarData)
		end
	end)
end

function Avatar:BulkPurchaseItems(Player: Player, Items: { { Id: string, Type: Enum.AvatarItemType } })
	Future.Try(function()
		return MarketplaceService:PromptBulkPurchase(Player, Items, {})
	end):After(function(Success, Result)
		if not Success then
			warn(Result)
		end
	end)
end

function Avatar:UpdateData(Player: Player, Data: HumanoidDescriber.Data)
	local Profile = PlayerDataStore:Get(Player)

	if Profile ~= nil then
		Profile.Session:patch(PlayerDataStore.Actions.SetAvatarData, Data)
	else
		Avatar:ApplyData(Player, Data)
	end
end

function Avatar:ApplyData(Player: Player, Data: HumanoidDescriber.Data)
	local _, Humanoid, HumanoidDescription = Avatar:GetCharacterObjects(Player)
	if Humanoid and HumanoidDescription then
		local HumanoidDescriberObject = HumanoidDescriber(HumanoidDescription)

		if Data.Clothing.Shirt == 0 then
			Data.Clothing.Shirt = 855777286
		end
		if Data.Clothing.Pants == 0 then
			Data.Clothing.Pants = 855782781
		end

		HumanoidDescriberObject:UpdateData(function()
			return Data
		end)

		Avatar:Apply(Player, HumanoidDescription):Await()

		HumanoidDescriberObject:Destroy()
	end
end

function Avatar:Apply(Player: Player, HumanoidDescription: HumanoidDescription)
	local Character = Player.Character

	if Character then
		local Humanoid = Character:FindFirstChildOfClass("Humanoid")
		if Humanoid then
			return Future.Try(function()
				Humanoid:ApplyDescriptionReset(HumanoidDescription)
			end)
		end
	end

	return false
end

function Avatar:GetCharacterObjects(Player: Player): (Model?, Humanoid?, HumanoidDescription?)
	local Character = Player.Character

	if Character then
		local Humanoid = Character:FindFirstChildOfClass("Humanoid")
		if Humanoid then
			return Character, Humanoid, Humanoid:FindFirstChildOfClass("HumanoidDescription")
		end
	end

	return nil
end

function Avatar:Start()
	Zap.BulkPurchaseAvatarItems.SetCallback(function(Player, Items)
		if RATE_LIMITS.BulkPurchaseItems(Player.UserId) then
			local SafeItems: HumanoidDescriber.Data = EnumSerDes.DeserializeTree(Items)

			return Avatar:BulkPurchaseItems(Player, SafeItems)
		end
	end)

	Zap.UpdateAvatar.SetCallback(function(Player, Data): ()
		if not CONFIG.AvatarSavingAllowed then
			return
		end

		if RATE_LIMITS.Update(Player.UserId) then
			local SafeData: HumanoidDescriber.Data = EnumSerDes.DeserializeTree(Data)

			return Avatar:UpdateData(Player, SafeData)
		end
	end)

	PlayerDataStore.Loaded:Connect(function(Player, Profile)
		Avatar:HandleProfile(Player, Profile)
	end)

	for Player, Profile in pairs(PlayerDataStore.Profiles) do
		Avatar:HandleProfile(Player, Profile)
	end
end

return Avatar


local Avalog = script.Parent.Parent.Parent.Parent
local t = require(Avalog.Parent.t)

return t.strictInterface({
	Avatar = t.strictInterface({
		Data = t.optional(t.buffer),
	}),
})


local Avalog = script.Parent.Parent.Parent.Parent
local Squash = require(Avalog.Parent.Squash)
local DataValidater = require(script.Parent.DataValidater)
local NewPlayerDataStore = require(Avalog.SourceCode.Storage.Utils.PlayerDataStore)
local AvatarDataSerDes = require(script.Parent.SerDes.AvatarData)
local HumanoidDescriber = require(Avalog.Parent.HumanoidDescriber)

export type PlayerData = {
	Avatar: {
		Data: buffer?,
	},
}

local PlayerDataStore = NewPlayerDataStore {
	name = "AvalogPlayerData",
	migrations = {},
	default_data = function()
		return {
			Avatar = {
				Data = nil,
			},
		}
	end,
	schema = DataValidater,
	pull_delay = 60 * 2,
}

local Actions = {
	SetAvatarData = function(Data: PlayerData, AvatarData: HumanoidDescriber.Data)
		assert(AvatarData ~= nil, "Given AvatarData is nil")

		local Cursor = Squash.cursor()
		AvatarDataSerDes.ser(Cursor, AvatarData)

		Data.Avatar.Data = Squash.tobuffer(Cursor)

		return Data
	end,
}

PlayerDataStore.Store:action(Actions.SetAvatarData)

PlayerDataStore.Actions = Actions

return PlayerDataStore


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local Squash = require(Avalog.Parent.Squash)

local S = Squash
local T = Squash.T

local AccessorySpec = S.record {
	AssetId = T(S.number(8)),
	AccessoryType = T(S.EnumItem(Enum.AccessoryType)),
	Order = T(S.opt(S.number(4))),
	Puffiness = T(S.opt(S.number(8))),
	IsLayered = T(S.opt(S.boolean())),
	Position = T(S.opt(S.Vector3(S.number(8)))),
	Rotation = T(S.opt(S.Vector3(S.number(8)))),
	Scale = T(S.opt(S.Vector3(S.number(8)))),
}

local EquippedEmote = S.record {
	Name = T(S.string()),
	Slot = T(S.number(4)),
}

local HumanoidDescriber = S.record {
	Accessories = T(S.array(AccessorySpec)),
	Emotes = T(S.map(S.string(), S.array(S.number(8)))),
	EquippedEmotes = T(S.array(EquippedEmote)),
	Face = T(S.number(8)),
	Scale = T(S.record {
		BodyType = T(S.number(8)),
		Depth = T(S.number(8)),
		Head = T(S.number(8)),
		Height = T(S.number(8)),
		Proportion = T(S.number(8)),
		Width = T(S.number(8)),
	}),
	Animations = T(S.record {
		Walk = T(S.number(8)),
		Run = T(S.number(8)),
		Fall = T(S.number(8)),
		Climb = T(S.number(8)),
		Swim = T(S.number(8)),
		Idle = T(S.number(8)),
		Mood = T(S.number(8)),
		Jump = T(S.number(8)),
	}),
	BodyParts = T(S.record {
		Head = T(S.number(8)),
		Torso = T(S.number(8)),
		LeftArm = T(S.number(8)),
		RightArm = T(S.number(8)),
		LeftLeg = T(S.number(8)),
		RightLeg = T(S.number(8)),
	}),
	BodyPartColors = T(S.record {
		Head = T(S.Color3()),
		Torso = T(S.Color3()),
		LeftArm = T(S.Color3()),
		RightArm = T(S.Color3()),
		LeftLeg = T(S.Color3()),
		RightLeg = T(S.Color3()),
	}),
	Clothing = T(S.record {
		Shirt = T(S.number(8)),
		TShirt = T(S.number(8)),
		Pants = T(S.number(8)),
	}),
}

return HumanoidDescriber


local Avalog = script.Parent.Parent.Parent
local PlayerDataStore = require(script.PlayerDataStore)
local Config = require(Avalog.Config)

local CONFIG = Config:Get()

local PlayerDataStorage = {
	Store = PlayerDataStore,
}

function PlayerDataStorage:Start()
	if CONFIG.DataStoreEnabled then
		PlayerDataStore:Start()
	end
end

return PlayerDataStorage


local ASSET_ACCESSORY_TYPES = {
	[Enum.AvatarAssetType.Hat] = Enum.AccessoryType.Hat,
	[Enum.AvatarAssetType.HairAccessory] = Enum.AccessoryType.Hair,
	[Enum.AvatarAssetType.FaceAccessory] = Enum.AccessoryType.Face,
	[Enum.AvatarAssetType.NeckAccessory] = Enum.AccessoryType.Neck,
	[Enum.AvatarAssetType.ShoulderAccessory] = Enum.AccessoryType.Shoulder,
	[Enum.AvatarAssetType.FrontAccessory] = Enum.AccessoryType.Front,
	[Enum.AvatarAssetType.BackAccessory] = Enum.AccessoryType.Back,
	[Enum.AvatarAssetType.WaistAccessory] = Enum.AccessoryType.Waist,

	[Enum.AvatarAssetType.ShirtAccessory] = Enum.AccessoryType.Shirt,
	[Enum.AvatarAssetType.TShirtAccessory] = Enum.AccessoryType.TShirt,
	[Enum.AvatarAssetType.SweaterAccessory] = Enum.AccessoryType.Sweater,
	[Enum.AvatarAssetType.JacketAccessory] = Enum.AccessoryType.Jacket,
	[Enum.AvatarAssetType.PantsAccessory] = Enum.AccessoryType.Pants,
	[Enum.AvatarAssetType.DressSkirtAccessory] = Enum.AccessoryType.DressSkirt,
	[Enum.AvatarAssetType.ShortsAccessory] = Enum.AccessoryType.Shorts,
	[Enum.AvatarAssetType.LeftShoeAccessory] = Enum.AccessoryType.LeftShoe,
	[Enum.AvatarAssetType.RightShoeAccessory] = Enum.AccessoryType.RightShoe,

	[Enum.AvatarAssetType.EyelashAccessory] = Enum.AccessoryType.Eyelash,
	[Enum.AvatarAssetType.EyebrowAccessory] = Enum.AccessoryType.Eyebrow,
}

return ASSET_ACCESSORY_TYPES


local Icons = require(script.Icons)
local Images = require(script.Images)

return {
	Icons = Icons,
	Images = Images,
}


local AvatarAssetTypes = {
	Types = {
		RigidAccessories = {
			Enum.AvatarAssetType.BackAccessory,
			Enum.AvatarAssetType.FaceAccessory,
			Enum.AvatarAssetType.HairAccessory,
			Enum.AvatarAssetType.NeckAccessory,
			Enum.AvatarAssetType.FrontAccessory,
			Enum.AvatarAssetType.WaistAccessory,
			Enum.AvatarAssetType.ShoulderAccessory,
			Enum.AvatarAssetType.Hat,
		},
		LayeredClothing = {
			Enum.AvatarAssetType.PantsAccessory,
			Enum.AvatarAssetType.ShirtAccessory,
			Enum.AvatarAssetType.TShirtAccessory,
			Enum.AvatarAssetType.ShortsAccessory,
			Enum.AvatarAssetType.DressSkirtAccessory,
			Enum.AvatarAssetType.JacketAccessory,
			Enum.AvatarAssetType.SweaterAccessory,
			Enum.AvatarAssetType.RightShoeAccessory,
			Enum.AvatarAssetType.LeftShoeAccessory,
		},
		ClassicClothing = {
			Enum.AvatarAssetType.Shirt,
			Enum.AvatarAssetType.TShirt,
			Enum.AvatarAssetType.Pants,
		},
		Emotes = {
			Enum.AvatarAssetType.EmoteAnimation,
		},
		Animations = {
			Enum.AvatarAssetType.RunAnimation,
			Enum.AvatarAssetType.FallAnimation,
			Enum.AvatarAssetType.IdleAnimation,
			Enum.AvatarAssetType.WalkAnimation,
			Enum.AvatarAssetType.SwimAnimation,
			Enum.AvatarAssetType.ClimbAnimation,
			Enum.AvatarAssetType.JumpAnimation,
			Enum.AvatarAssetType.MoodAnimation,
		},
		BodyParts = {
			Enum.AvatarAssetType.Torso,
			Enum.AvatarAssetType.LeftArm,
			Enum.AvatarAssetType.RightArm,
			Enum.AvatarAssetType.LeftLeg,
			Enum.AvatarAssetType.RightLeg,
			Enum.AvatarAssetType.DynamicHead,
		},
		FacialFeatures = {
			Enum.AvatarAssetType.EyebrowAccessory,
			Enum.AvatarAssetType.EyelashAccessory,
		},
		Gear = {
			Enum.AvatarAssetType.Gear,
		},
	},
}

function AvatarAssetTypes:Combine(AssetsTypeLists: { { Enum.AvatarAssetType } }): { Enum.AvatarAssetType }
	local CombinedAssetTypes = {}

	for _, AssetTypes in ipairs(AssetsTypeLists) do
		for _, AssetType in ipairs(AssetTypes) do
			if table.find(CombinedAssetTypes, AssetType) == nil then
				table.insert(CombinedAssetTypes, AssetType)
			end
		end
	end

	return CombinedAssetTypes
end

function AvatarAssetTypes:Flatten(AssetTypeCategories: { [string]: { Enum.AvatarAssetType } })
	local AssetsTypeLists = {}

	for _, AssetsTypeList in pairs(AssetTypeCategories) do
		table.insert(AssetsTypeLists, AssetsTypeList)
	end

	return AssetsTypeLists
end

AvatarAssetTypes.Types.All = AvatarAssetTypes:Combine(AvatarAssetTypes:Flatten(AvatarAssetTypes.Types))
AvatarAssetTypes.Types.Clothing =
	AvatarAssetTypes:Combine({ AvatarAssetTypes.Types.ClassicClothing, AvatarAssetTypes.Types.LayeredClothing })
AvatarAssetTypes.Types.Accessories = AvatarAssetTypes:Combine({
	AvatarAssetTypes.Types.RigidAccessories,
	AvatarAssetTypes.Types.LayeredClothing,
	AvatarAssetTypes.Types.FacialFeatures,
})

return AvatarAssetTypes


local Categories = require(script.Parent)

local function CategoriesToNames(CategoriesTable: { Categories.Category })
	local Names = {}

	for _, Category in ipairs(CategoriesTable) do
		table.insert(Names, Category.Name)
	end

	return Names
end

return CategoriesToNames


local AvatarAssetTypes = require(script.Parent.AvatarAssetTypes)

export type Category = {
	Name: string,
	CategoryFilter: Enum.CatalogCategoryFilter,
	SubCategories: { Category }?,
	AssetTypes: { Enum.AvatarAssetType }?,
	BundleTypes: { Enum.BundleType }?,
}

local function PurgeBundles(Categories: { Category }): { Category }
	local Result = {}

	for _, Category in Categories do
		local PurgedSubCategories = nil

		if Category.SubCategories then
			PurgedSubCategories = PurgeBundles(Category.SubCategories)

			if #PurgedSubCategories == 0 then
				PurgedSubCategories = nil
			end
		end

		if Category.AssetTypes or PurgedSubCategories then
			table.insert(Result, {
				Name = Category.Name,
				AssetTypes = Category.AssetTypes,
				BundleTypes = Category.BundleTypes,
				SubCategories = PurgedSubCategories,
			})
		end
	end

	return Result
end

local Categories: { [string]: { Category } } = {
	Marketplace = {
		{
			Name = "Featured",
			CategoryFilter = Enum.CatalogCategoryFilter.Recommended,
			AssetTypes = AvatarAssetTypes.Types.All,
			BundleTypes = {
				Enum.BundleType.Animations,
				Enum.BundleType.BodyParts,
				Enum.BundleType.DynamicHead,
				Enum.BundleType.DynamicHeadAvatar,
				Enum.BundleType.Shoes,
			},
		},
		{
			Name = "All",
			AssetTypes = AvatarAssetTypes.Types.All,
			BundleTypes = {
				Enum.BundleType.Animations,
				Enum.BundleType.BodyParts,
				Enum.BundleType.DynamicHead,
				Enum.BundleType.DynamicHeadAvatar,
				Enum.BundleType.Shoes,
			},
		},
		{
			Name = "Body",
			SubCategories = {
				{
					Name = "Bodies",
					BundleTypes = {
						Enum.BundleType.BodyParts,
					},
				},
				{ Name = "Hair", AssetTypes = { Enum.AvatarAssetType.HairAccessory } },
				{
					Name = "Heads",
					BundleTypes = { Enum.BundleType.DynamicHead },
					AssetTypes = { Enum.AvatarAssetType.DynamicHead },
				},
			},
		},
		{
			Name = "Clothing",
			SubCategories = {
				{ Name = "Shirts", AssetTypes = { Enum.AvatarAssetType.ShirtAccessory } },
				{ Name = "T-Shirts", AssetTypes = { Enum.AvatarAssetType.TShirtAccessory } },
				{ Name = "Sweaters", AssetTypes = { Enum.AvatarAssetType.SweaterAccessory } },
				{ Name = "Jackets", AssetTypes = { Enum.AvatarAssetType.JacketAccessory } },
				{ Name = "Pants", AssetTypes = { Enum.AvatarAssetType.PantsAccessory } },
				{ Name = "Dresses & Skirts", AssetTypes = { Enum.AvatarAssetType.DressSkirtAccessory } },
				{ Name = "Shorts", AssetTypes = { Enum.AvatarAssetType.ShortsAccessory } },
				{
					Name = "Shoes",
					AssetTypes = { Enum.AvatarAssetType.LeftShoeAccessory, Enum.AvatarAssetType.RightShoeAccessory },
					BundleTypes = { Enum.BundleType.Shoes },
				},
				{ Name = "Classic Shirts", AssetTypes = { Enum.AvatarAssetType.Shirt } },
				{ Name = "Classic T-Shirts", AssetTypes = { Enum.AvatarAssetType.TShirt } },
				{ Name = "Classic Pants", AssetTypes = { Enum.AvatarAssetType.Pants } },
			},
		},
		{
			Name = "Accessories",
			SubCategories = {
				{ Name = "Head", AssetTypes = { Enum.AvatarAssetType.Hat } },
				{ Name = "Face", AssetTypes = { Enum.AvatarAssetType.FaceAccessory } },
				{ Name = "Neck", AssetTypes = { Enum.AvatarAssetType.NeckAccessory } },
				{ Name = "Shoulder", AssetTypes = { Enum.AvatarAssetType.ShoulderAccessory } },
				{ Name = "Front", AssetTypes = { Enum.AvatarAssetType.FrontAccessory } },
				{ Name = "Back", AssetTypes = { Enum.AvatarAssetType.BackAccessory } },
				{ Name = "Waist", AssetTypes = { Enum.AvatarAssetType.WaistAccessory } },
			},
		},
		{
			Name = "Animation",
			SubCategories = {
				{
					Name = "Bundles",
					BundleTypes = { Enum.BundleType.Animations },
					AssetTypes = AvatarAssetTypes.Types.Animations,
				},
				{ Name = "Emotes", AssetTypes = AvatarAssetTypes.Types.Emotes },
			},
		},
	},
	Inventory = {},
	Customize = { { Name = "Equipped" }, { Name = "Skin" }, { Name = "Build" } },
}

Categories.Inventory = PurgeBundles(Categories.Marketplace)
table.remove(Categories.Inventory, 1)

return Categories


return {
	BodyType = { Min = 0, Max = 1 },
	Proportion = { Min = 0, Max = 1 },
	Height = { Min = 0.9, Max = 1.05 },
	Width = { Min = 0.7, Max = 1 },
	Depth = { Min = 0.85, Max = 1 },
	Head = { Min = 0.95, Max = 1 },
}


export type ItemType = "Asset" | "Bundle"

export type ItemSale = {
	experienceId: string,
	jobId: string,
	itemId: string,
	itemType: ItemType,
	robuxPrice: number,
	unitsSold: number,
}

export type AvalogUsageSnapshot = {
	experienceId: string,
	jobId: string,
	uptime: number,
	totalMenuOpens: number,
	totalMenuTime: number,
	totalUserSessions: number,
	totalUserPlaytime: number,
}

export type Item = {
	itemId: string,
	itemType: ItemType,
	tintColor: string?,
}

export type PromotedItem = Item & {
	promotionId: string,
	bid: number,
	startTime: number,
	endTime: number,
}

export type AvalogConfig = {
	latestVersion: string,
	featuredItems: { [number]: Item },
	pinnedItems: { [number]: Item },
	promotedItems: { [number]: PromotedItem },
}

return {
	AvalogConfig = {
		Fallback = {
			latestVersion = "0.0.0",
			featuredItems = {},
			pinnedItems = {},
			promotedItems = {},
		},
	},
}


local ASSET_TYPES = Enum.AvatarAssetType:GetEnumItems()

local function AssetTypeFromId(Id: number): Enum.AvatarAssetType?
	for _, AssetType in ipairs(ASSET_TYPES) do
		if AssetType.Value == Id then
			return AssetType
		end
	end

	return nil
end

return AssetTypeFromId


local function DeepEquals(TableA: { [any]: any }, TableB: { [any]: any })
	for Key, Value in pairs(TableA) do
		if (typeof(Value) == "table") and (typeof(TableB[Key]) == "table") then
			if not DeepEquals(TableB[Key], Value) then
				return false
			end
		elseif Value ~= TableB[Key] then
			return false
		end
	end

	return true
end

return DeepEquals


local MD5 = require(script.Parent.MD5)

local function ConcatDictionary(Dictionary: { [any]: any }): string
	local Result = ""

	for Key, Value in pairs(Dictionary) do
		local Additive = ""

		if typeof(Value) == "table" then
			Additive = ConcatDictionary(Value)
		else
			Additive = tostring(Key) .. "=" .. tostring(Value) .. "&"
		end

		Result = Result .. Additive
	end

	return Result:sub(1, -2)
end

local function HashDictionary(Dictionary: { [any]: any })
	return MD5.sumhexa(ConcatDictionary(Dictionary))
end

return HashDictionary


local function SquashNumber(Value, Min, Max)
	local Difference = Max - Min
	return Min + (Value * Difference)
end

return SquashNumber


local function StretchNumber(Output, Min, Max)
	local Difference = Max - Min
	return (Output - Min) / Difference
end

return StretchNumber


local Players = game:GetService("Players")

local Avalog = script.Parent.Parent.Parent.Parent
local Skilift = require(Avalog.Parent.Skilift)
local Signal = require(Avalog.Parent.Signal)
local Stator = require(Avalog.Parent.Stator)
local Sift = require(Avalog.Parent.Sift)

local function NewPlayerDataStore(Options)
	local PlayerDataStore = {
		Store = Skilift.store(Options),

		Profiles = {},

		Loaded = Signal.new(),
		Unloaded = Signal.new(),
	}

	function PlayerDataStore:Load(Player: Player)
		if PlayerDataStore.Profiles[Player] ~= nil then
			return
		end

		local Profile = {
			Session = PlayerDataStore.Store:load(`{Player.UserId}`),
			Data = Stator(nil, {}),
		}

		Profile.Session:userid(Player.UserId)

		Profile.Session:updated(function(Data)
			Profile.Data:Update(function()
				return Sift.Dictionary.copyDeep(Data)
			end)
		end)

		PlayerDataStore.Profiles[Player] = Profile
		PlayerDataStore.Loaded:Fire(Player, Profile)
	end

	function PlayerDataStore:Unload(Player: Player)
		local Profile = PlayerDataStore:Get(Player)

		if Profile ~= nil then
			Profile.Session:stop()
			PlayerDataStore.Profiles[Player] = nil

			PlayerDataStore.Unloaded:Fire(Player)
		end
	end

	function PlayerDataStore:Get(Player: Player)
		return PlayerDataStore.Profiles[Player]
	end

	function PlayerDataStore:Start()
		for _, Player in ipairs(Players:GetPlayers()) do
			PlayerDataStore:Load(Player)
		end

		Players.PlayerAdded:Connect(function(Player)
			PlayerDataStore:Load(Player)
		end)

		Players.PlayerRemoving:Connect(function(Player)
			PlayerDataStore:Unload(Player)
		end)
	end

	return PlayerDataStore
end

return NewPlayerDataStore


local LoadingGui = script:WaitForChild("LoadingGui")
local Main = LoadingGui:WaitForChild("LoadingScreen"):WaitForChild("Main")
local Percent = Main:WaitForChild("Percent")
local AmountTxt = Main:WaitForChild("Amount")
local CurrentlyInitting = Main:WaitForChild("CurrentlyInitting")
local Skip = Main:WaitForChild("Skip")

LoadingGui.Parent = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui")

while not game:IsLoaded() do
    task.wait()
end

task.wait(1)

local assets = {}
local loadedAssets = {}
local function addAssetsToLoad(assetList)
    for _, asset in assetList do
        table.insert(assets, asset)
    end
end
addAssetsToLoad(workspace:GetDescendants())
addAssetsToLoad(game:GetService("ReplicatedStorage"):GetDescendants())
addAssetsToLoad(game:GetService("StarterPlayer"):GetDescendants())
addAssetsToLoad(game:GetService("StarterGui"):GetDescendants())

local assetAmount = #assets
local loaded = false
function updateProgress()
    if not loaded then
        Percent.Text = tostring(math.floor(#loadedAssets / assetAmount * 100 - 1)).."%"
        AmountTxt.Text = "("..tostring(#loadedAssets - 1).."/"..tostring(assetAmount)..")"
    end
end

local ContentProvider = game:GetService("ContentProvider")
for index, asset in assets do
    task.delay(index / (assetAmount / 0.75), function()
        ContentProvider:PreloadAsync({
            asset
        })
        table.insert(loadedAssets, asset)
        updateProgress()
    end)
end

local TweenService = game:GetService("TweenService")

local function Finish()
    if not game:GetService("RunService"):IsStudio() then
        --don't remove this or you'll get in trouble! salutations! (unless you mention the engine in the game's description)
        print(require(game:GetService("ReplicatedStorage").Modules.Utils).Credits)
    end

    task.spawn(function()
        require(game:GetService("ReplicatedStorage").Modules.Network):FireServerConnection("AddLoadedPlayer", "REMOTE_EVENT")
    end)

    loaded = true

    Percent.Text = "100%"
    AmountTxt.Text = "("..tostring(assetAmount).."/"..tostring(assetAmount)..")"

    TweenService:Create(CurrentlyInitting, TweenInfo.new(0.45, Enum.EasingStyle.Quint, Enum.EasingDirection.In), {
        TextTransparency = 1
    }):Play()
    TweenService:Create(Percent.UIStroke, TweenInfo.new(0.666), {
        Transparency = 1
    }):Play()
    TweenService:Create(Percent.UIStroke, TweenInfo.new(2), {
        Thickness = 20
    }):Play()
    TweenService:Create(AmountTxt.UIStroke, TweenInfo.new(0.666), {
        Transparency = 1
    }):Play()
    TweenService:Create(AmountTxt.UIStroke, TweenInfo.new(2), {
        Thickness = 20
    }):Play()
    TweenService:Create(Skip, TweenInfo.new(0.666), {
        TextTransparency = 1
    }):Play()
    task.wait(1)
    TweenService:Create(Percent, TweenInfo.new(1, Enum.EasingStyle.Quint, Enum.EasingDirection.In), {
        TextTransparency = 1
    }):Play()
    TweenService:Create(AmountTxt, TweenInfo.new(1, Enum.EasingStyle.Quint, Enum.EasingDirection.In), {
        TextTransparency = 1
    }):Play()
    task.wait(0.5)
    TweenService:Create(Main, TweenInfo.new(0.75), {
        BackgroundTransparency = 1
    }):Play()

    game:GetService("Debris"):AddItem(LoadingGui, 0.8)
    game:GetService("StarterGui"):SetCore("ResetButtonCallback", true)

    task.wait(0.8)

    workspace:SetAttribute("ClientLoaded", true)
end

task.delay(10, function()
    if not loaded then
        Skip.Visible = true
        TweenService:Create(Skip, TweenInfo.new(1), {
            TextTransparency = 0
        }):Play()
        Skip.MouseButton1Click:Once(function()
            loaded = true
            Finish()
        end)
    end
end)

while #loadedAssets < #assets - 100 or loaded do
    task.wait()
end

local function InitScripts(module: ModuleScript)
    local ModuleCode = require(module)
    task.spawn(function()
        ModuleCode:Init()
    end)

    while not ModuleCode.Loaded do
        if ModuleCode.CurrentlyInitting then
            CurrentlyInitting.Text = "Initializing "..ModuleCode.CurrentlyInitting.."..."
        else
            CurrentlyInitting.Text = ""
        end
        task.wait()
    end
end

InitScripts(game:GetService("ReplicatedStorage").Initter)
InitScripts(game:GetService("Players").LocalPlayer.PlayerScripts.PlayerInitter)

while not workspace:GetAttribute("ServerLoaded") do
    task.wait()
end

if loaded then return end

Finish()


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Types = require(ReplicatedStorage.Classes.Types)

type AchievementGroup = {
    Title: string,
    LayoutOrder: number,
    Achievements: {[string]: Types.Achievement}
}

return {
    General = {
        Title = "General",
        LayoutOrder = 1,
        Achievements = {
            Welcome = {
                Title = "Welcome To [INSERT GAME NAME]",
                Description = "Play [INSERT GAME NAME] for the first time",
                RewardType = "Currency",
                Amount = 300,
                LayoutOrder = 1,
            },
        },
    },
} :: {[string]: AchievementGroup}


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Emote = require(ReplicatedStorage.Classes.Emote)

return Emote.New({
    Config = {
        Name = "T-Pose",
        Quote = "be not afraid.",
    },
    AnimationIds = "rbxassetid://87300475457340",
    SoundIds = "rbxassetid://1839549655",
    SpeedMultiplier = 0.1,
})


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

local Utils = require(ReplicatedStorage.Modules.Utils)

local G = script:FindFirstChild("KillerNameToast")
local PlayerLabelPrefab = script:FindFirstChild("PlayerName")

return function(Parent: ScreenGui, plrNames: {string})
	local Group = G:Clone()
    Group.Parent = Parent

	for _, Item in Group:GetDescendants() do
		if Item:IsA("Frame") then
			Item.BackgroundTransparency = 1
		elseif Item:IsA("TextLabel") then
			Item.TextTransparency = 1
		end
	end

	local Frame = Group.Container.Frame

	for _, name in plrNames do
		local Char = workspace.Players:FindFirstChild(name)
		local CharName = Char:GetAttribute("CharacterName")
		local KillerName = require(Utils.Instance.GetCharacterModule("Killer", CharName)).Config.Name

		local PlayerLabel = PlayerLabelPrefab:Clone()
		PlayerLabel.Name = name
		PlayerLabel.KillerName.Text = KillerName
		PlayerLabel.PlayerName.Text = "("..name..")"
		PlayerLabel.Parent = Frame

		for _, Item in PlayerLabel:GetDescendants() do
			if Item:IsA("Frame") then
				Item.BackgroundTransparency = 1
			elseif Item:IsA("TextLabel") then
				Item.TextTransparency = 1
			end
		end
	end

	if #plrNames > 1 then
		Group.Container.Label.Text = "The killers are..."
	end

	task.wait(1)

	for _, Item in Group:GetDescendants() do
		if Item:HasTag("Transparent") then
			continue
		end
		if Item:IsA("Frame") then
			TweenService:Create(Item, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {BackgroundTransparency = 0}):Play()
		elseif Item:IsA("TextLabel") then
			TweenService:Create(Item, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {TextTransparency = 0}):Play()
		end
	end

	task.wait(3)

	for _, Item in Group:GetDescendants() do
		if Item:IsA("Frame") then
			TweenService:Create(Item, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.In), {BackgroundTransparency = 1}):Play()
		elseif Item:IsA("TextLabel") then
			TweenService:Create(Item, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.In), {TextTransparency = 1}):Play()
		end
	end

	task.wait(1.5)
	Group:Destroy()
end


return {
    Behaviour = function(_, Intro)
    	local TweenService = game:GetService("TweenService")

    	local Group = script:FindFirstChildWhichIsA("Frame"):Clone()
        Group.Parent = game:GetService("Players").LocalPlayer.PlayerGui:FindFirstChild("KillerIntros")
		table.insert(Intro.Disposables, Group)
    	local Frame = Group.Background
    	Frame.BackgroundTransparency = 0

    	local Text = Frame.TextLabel
    	Text.TextTransparency = 1
    	local InitialText = Text.Text

    	task.spawn(function()
    		while Frame and Text do
    			Text.Text = Text.Text.."."
    			if Text.Text == InitialText.."...." then
    				Text.Text = InitialText.."."
    			end
    			task.wait(0.35)
    		end
    	end)

    	Group:FindFirstChildWhichIsA("Sound"):Play()

    	TweenService:Create(Text, TweenInfo.new(0.5), {TextTransparency = 0}):Play()

    	task.wait(2.5)

    	for _, Item in Group:GetDescendants() do
    		-- if Item:IsA("Frame") then
    		-- 	TweenService:Create(Item, TweenInfo.new(1), {BackgroundTransparency = 1}):Play()
    		-- elseif Item:IsA("TextLabel") then
			if Item:IsA("TextLabel") then
    			TweenService:Create(Item, TweenInfo.new(0.5), {TextTransparency = 1}):Play()
    		end
    	end

    	task.wait(0.5)
    	Group:Destroy()
    end
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Cola = require(ReplicatedStorage.Assets.Items.Cola)
local PickableItem = require(ReplicatedStorage.Classes.Item.PickableItem)
local Utils = require(ReplicatedStorage.Modules.Utils)

return Utils.Type.CopyTable(PickableItem.New({
    ItemEquivalent = Cola,
    DisplayName = "Bloxy Cola",
    Model = script:FindFirstChild("Cola"),
}))


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Medkit = require(ReplicatedStorage.Assets.Items.Medkit)
local PickableItem = require(ReplicatedStorage.Classes.Item.PickableItem)
local Utils = require(ReplicatedStorage.Modules.Utils)

return Utils.Type.CopyTable(PickableItem.New({
    ItemEquivalent = Medkit,
    DisplayName = "Medkit",
    Model = script:FindFirstChild("Medkit"),
}))


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

local Character = require(ReplicatedStorage.Classes.Character)
local Types = require(ReplicatedStorage.Classes.Types)

local function OnCommanderInit(self: Types.Survivor, Char: Model)
    if not RunService:IsServer() then
        return
    end

    Char:FindFirstChildWhichIsA("Humanoid"):ApplyDescription(Players:GetHumanoidDescriptionFromUserIdAsync(self.Owner.UserId))
end

return Character.CreateSkin("Survivor", "Commander", {
    Config = {
        Name = "Yourself",
        Quote = "Hey, it's you!",
        Price = 99999999,

        Origin = {},
    },

    OnInit = OnCommanderInit,
})


--it was definitely not 3 am when writing this months ago (excusing the funny comments)

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Types = require(ReplicatedStorage.Classes.Types)
local Janitor = require(ReplicatedStorage.Packages.Janitor)

type Ability = {
    ModuleCode: Types.Ability,
    Keybind: TextLabel,
    Timer: TextLabel,
    Uses: TextLabel?,
    Interactor: ImageButton,
}

local AbilityGUI = {
    AbilityFramePositions = {
        Slash = UDim2.fromScale(0.675, 0.9),
        FirstAbility = UDim2.fromScale(0.745, 0.9),
        SecondAbility = UDim2.fromScale(0.815, 0.9),
        ThirdAbility = UDim2.fromScale(0.885, 0.9),
        FourthAbility = UDim2.fromScale(0.605, 0.9),
    },
    AbilityLayoutOrder = {
        Slash = 1,
        FirstAbility = 2,
        SecondAbility = 3,
        ThirdAbility = 4,
        FourthAbility = 5,
    },
}

local PrefabContainer = script:FindFirstChild("Prefabs")
local Prefabs = {
    Container = PrefabContainer:FindFirstChild("CharacterAbilitiesContainer"),
    AbilityContainer = PrefabContainer:FindFirstChild("AbilityContainer"),
    UsesLeftText = PrefabContainer:FindFirstChild("UsesLeft"),
}

--- Function used to init a character's ability GUI based on their ability table's info stored in `StarterCharacterScripts.PlayerAbilities.CharacterAbilities`.
function AbilityGUI.InitGUI()
    local InputManager = require(Players.LocalPlayer.PlayerScripts.InputManager)
    local Character = Players.LocalPlayer.Character

    --creating the entire gui
    local Container = Prefabs.Container:Clone()
    Container.Name = Character:GetAttribute("CharacterName")
    Container.Parent = Players.LocalPlayer.PlayerGui.CharacterGUI

    local Abilities: {Ability} = {}

    --this gets every single ability's properties and uses them
    --it's fucking genius!!
    for _, Ability in require(Players.LocalPlayer.Character.PlayerAbilities.CharacterAbilities).Abilities do
        if Ability.Passive ~= nil and Ability.Passive == true then
            continue
        end
        local AbilityT = {
            Module = Ability,
            RenderImage = Ability.RenderImage,
            UICorner = Ability.UICorner,
            Parent = Container,
        }
        table.insert(Abilities, AbilityGUI._SetupAbilityFrame(AbilityT))
    end

    local JanitorInstance = Janitor.new()
    JanitorInstance:LinkToInstances(Container, Character)

    JanitorInstance:Add(InputManager.SchemeChanged:Connect(function(CurrentScheme: string)
        for _, Ability: Ability in Abilities do
            AbilityGUI._UpdateAbilityFrameInput(Ability, InputManager, CurrentScheme)
        end
    end), "Disconnect")

    for _, Ability: Ability in Abilities do
        AbilityGUI._UpdateAbilityFrameInput(Ability, InputManager, InputManager.CurrentControlScheme)

        --it would be stupid to not add this (again). -Dyscarn
        -- uses label
        if Ability.Uses then
            JanitorInstance:Add(Ability.ModuleCode.Signals.ChargesChanged:Connect(function(amount: number)
                Ability.Uses.Text = tostring(amount)
            end), "Disconnect")
        end

        if Ability.Timer then
            Ability.Timer.TextTransparency = 1
        end

        JanitorInstance:Add(Ability.ModuleCode.Signals.CooldownSet:Connect(function(_cooldownSet: number)
            Ability.Timer.TextTransparency = 0
            while Ability.Timer and Ability.ModuleCode.CooldownTimer > 0 do
                -- failsafe if the ui gets destroyed for any reason
                if not Ability.Timer then
                    break
                end
                Ability.Timer.Text = tostring(math.ceil(Ability.ModuleCode.CooldownTimer * 10) / 10)
                task.wait(0.025)
            end
            -- failsafe if the ui gets destroyed for any reason
            if Ability.Timer then
                Ability.Timer.TextTransparency = 1
            end
        end), "Disconnect")

        JanitorInstance:Add(Ability.Interactor.MouseButton1Click:Connect(function()
            Ability.ModuleCode:AttemptUse()
        end), "Disconnect")
    end
end

--this is so messy wth
--- Function used to set up an entire ability's GUI for display in a character's GUI.
function AbilityGUI._SetupAbilityFrame(Ability): Ability
    --we'll have to put everything in a frame right????
    local AbilityContainer: Frame = Prefabs.AbilityContainer:Clone()
    AbilityContainer.Name = Ability.Module.Name
    AbilityContainer.Position = AbilityGUI.AbilityFramePositions[Ability.Module.InputName]
    AbilityContainer.LayoutOrder = AbilityGUI.AbilityLayoutOrder[Ability.Module.InputName]
    AbilityContainer.Parent = Ability.Parent

    --what if i put skibidi toilet in here -Quinn definetly not Dyscarn
    local RenderImage: ImageLabel = AbilityContainer:FindFirstChildWhichIsA("ImageLabel")
    RenderImage.Image = Ability.RenderImage
    if Ability.UICorner then
        local Corner = Instance.new("UICorner")
        Corner.CornerRadius = UDim.new(1, 0)
        Corner.Parent = RenderImage
    end

    --COOLDOWNS ARE IMPORTANT INFO YOU DIPSHIT!!!!
    local CooldownTimer: TextLabel = AbilityContainer:FindFirstChild("CooldownTime")
    CooldownTimer.Text = Ability.Module.Cooldown

    --KEYBINDS AS WELL!!! IF NOT, HOW ARE YOU GONNA KNOW WHAT YOU PRESS???????
    --(they WERE here but now they're dynamic TOODLES :D)

    --maybe the name not so much BUT YOU'LL HAVE TO REFERENCE IT IN A SPECIFIC WAY WON'T YOU?????
    AbilityContainer:FindFirstChild("Name").Text = Ability.Module.Name

    local AbilityT: Ability = {
        ModuleCode = Ability.Module,
        Keybind = AbilityContainer:FindFirstChild("Keybind"),
        Timer = CooldownTimer,
        Interactor = AbilityContainer:FindFirstChild("Interactor"),
    }

    --it would be stupid to not add this. -Dyscarn
    if Ability.Module.UseSettings.Limited then
        local UsesLeft: TextLabel = Prefabs.UsesLeftText:Clone()
        UsesLeft.Text = Ability.Module.UseSettings.InitialUses
        UsesLeft.Parent = AbilityContainer

        AbilityT.Uses = UsesLeft
    end

    return AbilityT
end

function AbilityGUI._UpdateAbilityFrameInput(Ability: Ability, InputManager, CurrentScheme: string)
    --I HATE MOBILE SUPPORT GNMDKIMBKBMSPMBPJBSG
    if CurrentScheme ~= "Touch" then
        Ability.Keybind.TextTransparency = 0

        --slash isn't built different now nvm :(
        local Path = "Default."..Ability.ModuleCode.InputName.."."
        if CurrentScheme ~= "Gamepad" then
            Path = Path.."Keyboard"
        else
            Path = Path.."Controller"
        end
        local Key = InputManager:GetInputBinding(Path) :: InputBinding

        --WOW THANKS DYSCARN FOR MAKING A COOL INPUT MANAGER THAT ACTUALLY WORKS AND SHOWS YOUR `preferred input device` // you're welcome Dyscarn
        Ability.Keybind.Text = Key.KeyCode.Name
        if CurrentScheme == "Gamepad" then
            Ability.Keybind.Text = Ability.Keybind.Text:gsub("Button", "")
        end

        return
    end
    --NEVER SHOW UP ON MY FACE AGAIN!!!!
    Ability.Keybind.TextTransparency = 1
end

return AbilityGUI


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

local Hitbox = require(ReplicatedStorage.Classes.Hitbox)
local Sounds = require(ReplicatedStorage.Modules.Sounds)
local Ability = require(ReplicatedStorage.Classes.Ability)
local Types = require(ReplicatedStorage.Classes.Types)
local Utils = require(ReplicatedStorage.Modules.Utils)

local SlashModule = {}

local function DefaultSlashBehaviour(self: Types.Ability)
	if RunService:IsServer() then
		Sounds.PlaySound(self.UseSound, {Parent = self.OwnerProperties.HRP})
		task.delay(self.Delay, function()
			Hitbox.New(self.Owner, {
				CFrameOffset = self.HitboxOffset,
				Size = self.HitboxSize,
				Time = self.Duration,
				Damage = self.Damage,
				Reason = "Slash Attack",
				ExecuteOnKill = true,
				Connections = self.Connections,
			})
		end)
	else
		self.OwnerProperties.TurnToMoveDirection:AddHeadPreventionFactor("Slash")
		self:AddConnection(task.delay(0.7, function()
			self.OwnerProperties.TurnToMoveDirection:RemoveHeadPreventionFactor("Slash")
		end))
	end
end
local function IncreaseDamage(self: Types.Ability, amount: number)
	self.Damage += amount
end

--- Creates a new Slash ability instance. Use this in every killer that you want to have a slash ability in.
function SlashModule.New(Props: Types.Ability?): Types.Ability

	-- Doesn't require you to unintuitively slot in an empty table for it to work at all– someone was confused about this a bit earlier, so I figured a fix was in order.
	Props = Props or {}
	local Final = Ability.New({
		Name = "Slash",
		InputName = "Slash",
		Cooldown = 2,
		Duration = 0.4,
		Damage = 20,
		RenderImage = "rbxassetid://11218451110",
		UseSound = "rbxassetid://12222200",
		UseAnimation = "rbxassetid://133504250023139",
		UICorner = true,
		Delay = 0.1,
		HitboxSize = Vector3.new(5, 6, 4.5),
		HitboxOffset = CFrame.new(0, 0, -2.5),
		Behaviour = DefaultSlashBehaviour,
		IncreaseDamage = IncreaseDamage,
	})

	Utils.Type.DeepTableOverwrite(Final, Props)

	return Final
end

return SlashModule


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Types = require(script.Parent.Types)
local Utils = require(ReplicatedStorage.Modules.Utils)

local EffectType = {}
EffectType.__index = EffectType

--- Effect preset for customization.
function EffectType.GetDefaultSettings(): Types.Effect
    return setmetatable({
        Name = "Effect",
        Description = "Does something",
        Duration = 1,
        Level = 1,

        TimeLeft = 1,
        ShowInGUI = true,
    }, EffectType)
end

--- ## DEPRECATED: use `Effect.GetDefaultSettings()` instead.
--- Effect preset for customization.
@deprecated
function EffectType.GetDefaultEffect(): Types.Effect
    return EffectType.GetDefaultSettings()
end

--- Creates an effect with the settings specified in `Props`.
--- Every property that has to be there has a fallback, so not all properties have to be written there if the default values are fine.
function EffectType.New(Props: Types.Effect): Types.Effect
    Props = Props or {}
    local Final = EffectType.GetDefaultSettings()

    Utils.Type.DeepTableOverwrite(Final, Props)

    return Final
end

--- ## DEPRECATED: use `Effect.New()` instead.
--- Creates an effect with the settings specified in `Props`.
--- Every property that has to be there has a fallback, so not all properties have to be written there if the default values are fine.
@deprecated
function EffectType.CreateEffect(Props: Types.Effect): Types.Effect
    return EffectType.New(Props)
end

function EffectType:Apply(level: number?, char: Model, duration: number)
    self.Level = level or self.Level
    self:ApplyEffect(level or self.Level, char, duration)
end

function EffectType:Remove(char: Model)
    self:RemoveEffect(char)
end

function EffectType:ApplyEffect(_level: number?, _char: Model, _duration: number)
end
function EffectType:RemoveEffect()
end

return EffectType


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

local TimeManager = RunService:IsServer() and require(ServerScriptService.Managers.TimeManager) or nil
local CommonFunctions = RunService:IsServer() and require(ServerScriptService.System.CommonFunctions) or nil
local Types = require(script.Parent.Types)
local Network = require(ReplicatedStorage.Modules.Network)
local Utils = require(ReplicatedStorage.Modules.Utils)
local Sounds = require(ReplicatedStorage.Modules.Sounds)

local HitboxFolder = workspace.TempObjectFolders.Hitboxes

local Hitbox = {
	DefaultConfig = {
		Size = Vector3.new(4, 7, 4),
		CFrameOffset = CFrame.new(),
		Damage = 20,
		Time = 0.2,
		Knockback = 0,
		HitMultiple = false,
		PredictVelocity = true,
		ExecuteOnKill = false,
		IsProjectile = false,
		FriendlyFire = false,
		Reason = "Slash Attack",
		Shape = Enum.PartType.Block,
		MinBlockingWallHeight = 3,
		IgnoreWalls = false,
		EffectsToApply = {},
		Connections = {},
	},
}

--- Creates a new hitbox, assigning it to a specific player.
function Hitbox.New(SourcePlayer: Player, Config: Types.HitboxSettings): Types.HitboxDetails
	-- Prevent this from normally running on the client
	--i love writing server shit and putting it in ReplicatedStorage :] -dys
	if not RunService:IsServer() then
		return {
			HumanoidsHit = {},
			Creator = SourcePlayer,
			Damage = 0,
			TimePast = 0,
			Cancelled = true,
			Connections = {},
			Cancel = function() end,
		}
	end

	-- Define the player in question for this hitbox, find their HumanoidRoot (or adjacent)
	local Char = SourcePlayer and SourcePlayer.Character -- "Oh, that's pretty clever, too. Huh!" – Itred
	local Root = Char and Char.PrimaryPart
	-- Prevent the hitbox from running if such is not found.
	if not Root then
		-- "Figured some feedback on this would be nice, I can see this accidentally being tripped with custom player models for a killer (maybe???)" – Itred
		warn("ReplicatedStorage/Classes/Hitbox: Notice – Hitbox creation halted, due to no primarypart being present in the character of player '"..SourcePlayer.Name.."'.")
		return {
			HumanoidsHit = {},
			Creator = SourcePlayer,
			Damage = 0,
			TimePast = 0,
			Cancelled = true,
			Connections = {},
			Cancel = function() end,
		}
	end

	-- Grab configuration to the pre-existing config table, or initialize some defaults for a new one if it doesn't exist.
	Utils.Type.DeepTableWrite(Config, Hitbox.DefaultConfig)

	-- Make a new hitbox filter
	local Over = OverlapParams.new()
	Over.FilterType = Enum.RaycastFilterType.Exclude
	Over.FilterDescendantsInstances = {Char}
	Over.CollisionGroup = "Attackable"

	-- Make a new raycast filter
	local RayParams = RaycastParams.new()
	RayParams.FilterType = Enum.RaycastFilterType.Exclude
	RayParams.FilterDescendantsInstances = {Char}

	local Effects = Char.Effects

	-- Initialize a property table for storing some important details about the hitbox that we'll want to fetch later; additionally, make a useful cancelling function, also for later.
	local Props: Types.HitboxDetails = {
		HumanoidsHit = {},
		Creator = SourcePlayer,
		Damage = Config.Damage,
		TimePast = 0,
		Cancelled = false,
		IsProjectile = Config.IsProjectile,
		Connections = {},
		Cancel = function(Props: Types.HitboxDetails)
			if Props.Cancelled then
				return
			end

			Props.Cancelled = true
			for _, Conn in Props.Connections do
				Conn:Disconnect()
			end
		end,
	}
	Props.Connections = {
		Effects.ChildAdded:Connect(function(newChild)
			if newChild.Name == "Stunned" then
				Props:Cancel()
			end
		end),

		Char.AncestryChanged:Connect(function(_, newParent)
			if newParent == nil then
				Props:Cancel()
			end
		end),

		Char.Destroying:Connect(function()
			Props:Cancel()
		end),

		Root.AncestryChanged:Connect(function(_, newParent)
			if newParent == nil then
				Props:Cancel()
			end
		end),

		Root.Destroying:Connect(function()
			Props:Cancel()
		end)
	}

	-- Start the process of looking for anything that collides with the hitbox.
	task.defer(function()
		if Effects:FindFirstChild("Stunned") then
			return
		end

		-- caching part and cloning it instead of creating it every time
		local Part = Instance.new("Part")
		Part.Name = SourcePlayer.Name.."Hitbox"
		Part.Transparency = 1
		Part.CanCollide = false
		Part.CanQuery = false
		Part.CanTouch = false
		Part.CastShadow = false
		Part.Anchored = true
		Part.Size = Config.Size
		Part.Shape = Config.Shape

		while not Props.Cancelled and Props.TimePast < Config.Time do
			if not (Config.HitMultiple or #Props.HumanoidsHit <= 0) then
				break
			end

			local CheckPart = Part:Clone()

			-- Change certain properties of the hitbox if they've been changed while execution of this Hitbox is still ongoing.
			for _, valueName in {"Size", "Shape"} do
				if CheckPart[valueName] ~= Config[valueName] then
					CheckPart[valueName] = Config[valueName]
				end
			end

			CheckPart.CFrame = (Config.CFrame and (typeof(Config.CFrame) == "function" and Config.CFrame() or Config.CFrame) or Root.CFrame) * Config.CFrameOffset
			CheckPart.Parent = HitboxFolder

			-- The hitbox itself lingers for a little while, even after parsing it, so it can be seen for those who have hitboxes enabled in their client settings(?).
			Debris:AddItem(CheckPart, 0.75)

			-- Some amount of ping compensation for placing the hitbox.
			if Config.PredictVelocity and not Config.IsProjectile then
				-- Utils.Misc.Print(SourcePlayer:GetNetworkPing())
				local PingDivider = 6.5 - math.clamp(SourcePlayer:GetNetworkPing(), 0, 3)
				local PingOffset = CheckPart.CFrame:VectorToObjectSpace(Root.AssemblyLinearVelocity) / PingDivider
				CheckPart.CFrame *= CFrame.new(PingOffset)
			end

			-- Finally parse through the hitbox itself.
			local Damaged = workspace:GetPartsInPart(CheckPart, Over)

			-- Only keep parts with HitboxPriority
			local FilteredDamaged = {}
			for _, part in Damaged do
				if part:GetAttribute("HitboxPriority") then
					table.insert(FilteredDamaged, part)
				end
			end
			Damaged = FilteredDamaged

			-- Flip through all of the parts hit, sort them by hitboxpriority for the next step.
			table.sort(Damaged, function(a, b)
				return (a:GetAttribute("HitboxPriority") or 1) > (b:GetAttribute("HitboxPriority") or 1)
			end)

			-- Make a new table for storing all players hit this loop.
			local Hit = {}

			-- Go through every hit player in the order mentioned beforehand.
			for _index, collidedpart in Damaged do

				-- Determine whether or not the part in question belongs to a character by searching for a "humanoid" object sibling.
				local Humanoid = Utils.Character.FindHumanoid(collidedpart)

				if not Humanoid or table.find(Props.HumanoidsHit, Humanoid) then
					continue
				end

				-- Proceed if the following conditions are met:
				-- If the humanoid exists,
				-- has more than 0 health,
				-- if the chracter tied to it is not invincible,
				-- is not executing (for killers),
				-- has not already been hit by this attack (so one doesn't take 300 damage from getting their arms, legs, and torso all within the same hitbox),
				-- and if they're either not on the same team as the user, or if the hitbox has friendly fire enabled.
				local TargetCharacter = Humanoid.Parent

				if not (
					Humanoid.Health > 0

						and not TargetCharacter:GetAttribute("Invincible")
						and not TargetCharacter:GetAttribute("Executing")

						and not Damaged[Humanoid]

						and (
							workspace:GetAttribute("FriendlyFire")
							or Config.FriendlyFire
							or (TargetCharacter:FindFirstChild("Role") and TargetCharacter.Role.Value or Char.Role.Value)
						) ~= Char.Role.Value) then
					continue
				end

				--[[
                -- If the hitbox has no preset anchor, the anchor its using is the HumanoidRootPart of the user– perform some sanity checks to ensure the target isn't getting hit through a wall or similar.
                if not Config.CFrame then

                    local index = 0
                    -- Starting from both the current facing position of the user, and the position of the user, facing towards the collided part, fire one more check forwards.
                    for _, o in {
                        Root.CFrame.LookVector,
                        CFrame.lookAt(Root.Position, collidedpart.Position).LookVector,
                    } do
                        -- Check in front of both of these starting points, moving out by the hitbox's length.
                        local cast = workspace:Raycast(Root.Position, o * CheckPart.Size.Z, RayParams)
                        local Inst = cast and cast.Instance

                        -- If the object hit is not a character (has no humanoid sibling), is not fully transparent, and is not a hitbox, then its a wall or something map-related– add one to an index counter.
                        if Inst and not Inst.Parent:FindFirstChildWhichIsA("Humanoid") and Inst.Transparency ~= 1 and not Inst:GetAttribute("Hitbox") then
                            index += 1
                        end
                    end

                    -- Only if both of the above checks hit something assumedly map-related, skip hitting this player.
                    if index >= 2 then
                        continue
                    end
                end
                ]]

				-- Check if there's a wall between player and target
				if not Config.IgnoreWalls then
					local HRoot = Humanoid.Parent:FindFirstChild("HumanoidRootPart")
					if HRoot then
						local function isBlockedByObstacle(): boolean
							local minHeight = Config.MinBlockingWallHeight

							local WallRayParams = RaycastParams.new()
							WallRayParams.FilterType = Enum.RaycastFilterType.Exclude
							WallRayParams.FilterDescendantsInstances = {Char, TargetCharacter}

							for _, offset in {-2, 0, 2} do
								local origin = Root.Position + Vector3.yAxis * offset
								local target = HRoot.Position + Vector3.yAxis * offset
								local cast = workspace:Raycast(origin, target - origin, WallRayParams)

								if cast
									and cast.Instance.Transparency ~= 1
									and not cast.Instance:GetAttribute("Hitbox")
									and cast.Normal.Y < 0.5
									and cast.Instance.Size.Y >= minHeight
								then
									local obstacleHumanoid = Utils.Character.FindHumanoid(cast.Instance)
									if obstacleHumanoid then
										continue
									end
									return true
								end
							end
							return false
						end

						if isBlockedByObstacle() then
							continue
						end
					end
				end

				-- Otherwise, add the player to two different tracking tables.
				table.insert(Props.HumanoidsHit, Humanoid)
				table.insert(Hit, Humanoid)

				-- Additionally, if the hitbox isn't set up to hit multiple players, stop it right here. Otherwise, let it keep going through any other player it may have hit this loop..
				if not Config.HitMultiple then
					break
				end
			end

			-- Change the hitbox part's color depending on whether or not it hit any targets.
			CheckPart.Color = #Hit > 0 and Color3.fromRGB(0, 255, 0) or Color3.fromRGB(255, 0, 0)

			-- Register a hit in every player detected in the previous step.
			for Index, Humanoid in Hit do

				Hitbox._RunCallback(Config.Connections, "PreHit", Config, Humanoid, Index == 1)

				-- Look for critical parts in the character in question, as well as the resistance effect, if applicable.
				local TargetCharacter = Humanoid.Parent
				local HRoot = TargetCharacter and TargetCharacter:IsA("Model") and Utils.Character.GetRootPart(TargetCharacter)
				local Resistance = TargetCharacter and TargetCharacter:FindFirstChild("Resistance") and TargetCharacter.Effects:FindFirstChild("Resistance") and TargetCharacter.Effects["Resistance"].Value or 0

				-- Configure damage to be affected by the Resistance effect.
				local Damage = Config.Damage * (1 - Resistance / 5)

				local function DamageHumanoid()
					CommonFunctions.DamagePlayer({
						SourcePlayer = SourcePlayer,
						TargetHumanoid = Humanoid,
						Damage = Damage,
						Reason = Config.Reason,
						KnockbackDirection = CheckPart.CFrame.LookVector,
						KnockbackMagnitude = Config.Knockback,
					})
				end

				-- If a HumanoidRootPart exists as a sibling to the humanoid object, proceed with registering a hit.
				if not HRoot then
					continue
				end

				-- Find out how many times this character has been hit so far(?).
				TargetCharacter:SetAttribute("TimesHit", (TargetCharacter:GetAttribute("TimesHit") or 0) + 1)

				-- If a survivor/killer is found tied to this character model, look for potential hit effects.
				if TargetCharacter:GetAttribute("CharacterName") ~= nil then

					-- Find the survivor/killer module tied to this character.
					local CharacterModule = require(TargetCharacter.PlayerAbilities.CharacterAbilities).CharacterModule

					-- If the survivor/killer has a function callback for being hit, and they nullify the hit for one reason or another, skip them and move on.
					if CharacterModule and CharacterModule.OnHit ~= nil then
						if CharacterModule:OnHit(Props, Damage) == "Ignored" then
							continue
						end
					end

				end

				-- If theres a function set to be executed on player hit for this hitbox, toss the humanoid to it.
				Hitbox._RunCallback(Config.Connections, "Hit", Config, Humanoid, Index == 1)

				-- Store the user as the last player whom hit the target, to give a readout of their killer if they are to die from a status effect or similar.
				if TargetCharacter:FindFirstChild("Killer") then
					TargetCharacter.Killer.Value = SourcePlayer
				end

				-- If the target has more than 0 health, but is killed by the hitbox:
				if Humanoid.Health > 0 and Humanoid.Health - Damage <= 0 then

					-- Find the killermodule for the user, who is assumedly on the "killer" team
					local Module = require(Utils.Instance.GetCharacterModule(Char.Role.Value, Char:GetAttribute("CharacterName"), Char:GetAttribute("CharacterSkinName")))

					-- Find out who the survivor they killed was, and what their skin was (if applicable)
					local KilledSurvivor = {
						Name = TargetCharacter:GetAttribute("CharacterName"),
						Skin = TargetCharacter:GetAttribute("CharacterSkinName"),
					}

					-- Play on-kill voicelines, only if the user is properly able to execute the target.
					if Module.Config.Voicelines.Kill then

						-- Fetch the voiceline, or if one isn't found, resort to a default.
						local Voiceline = KilledSurvivor.Skin and Module.Config.Voicelines.Kill[KilledSurvivor.Skin..KilledSurvivor.Name] or Module.Config.Voicelines.Kill[KilledSurvivor.Name]
						if not Voiceline then
							Voiceline = Module.Config.Voicelines.Kill["Default"]
						end

						-- Finally, play the voiceline.
						if Voiceline then
							Sounds.PlayVoiceline(Char, Voiceline)
						end
					end

					-- Play execution animations, if applicable
					if not (Char.Role.Value == "Killer" and Config.ExecuteOnKill and not TargetCharacter:GetAttribute("ExecutionsDisabled")) then
						-- If not, just inflict the damage as usual.
						DamageHumanoid()

						return
					end

					-- Fetch if the user has execution animations to use
					local ExecutionAnimsAvailable = Module.Config.AnimationIDs[Config.ExecutionAnimation or "Execution"]

					task.defer(function() -- Switched from task.spawn() to task.defer() to avoid task scheduler conflicts
						-- If the user doesn't have valid execution animations to use, just inflict damage.
						if not ExecutionAnimsAvailable then
							DamageHumanoid()

							return
						end

						-- If the user has valid execution animations to use, go through with running them.

						-- Set the user and the target to be in an executing state.
						Char:SetAttribute("Executing", true)
						Char:SetAttribute("Invincible", true)
						TargetCharacter:SetAttribute("Executing", true)

						-- Get the player that's about to be executed
						task.spawn(function()
							local TargetPlayer = Players:GetPlayerFromCharacter(TargetCharacter)
							if not TargetPlayer then
								return
							end

							-- Execute the same code as if a survivor was killed while in chase to make it more fair for the killer (it won't execute again when the character actually dies later)
							Network:FireClientConnection(SourcePlayer, "KilledPlayer", "REMOTE_EVENT", TargetPlayer)
							Network:FireConnection("KilledPlayer", SourcePlayer, TargetPlayer)

							CommonFunctions.UpdatePlayerStat(SourcePlayer, "KillerStats.Kills", 1)
							CommonFunctions.GrantRewardToPlayer(SourcePlayer, {Money = 20, EXP = 60, Reason = "killing a Survivor"})

							TimeManager.SetTime(TimeManager.CurrentTime + 35)
						end)

						-- Find out if theres a special execution animation to use for this specific survivor; if there isn't, use a default.
						local ExecutionAnim =
							(KilledSurvivor.Skin and
								ExecutionAnimsAvailable[KilledSurvivor.Skin..KilledSurvivor.Name]
								or ExecutionAnimsAvailable[KilledSurvivor.Name]
							)
							or ExecutionAnimsAvailable["Default"]

						-- If there are no execution animations to use, for one reason or another, resort to just killing the target and flinging it really far.
						if not ExecutionAnim then
							DamageHumanoid()

							return
						end

						-- If there is an animation to play, grab it.
						local RuntimeKillerModule = require(Char.PlayerAbilities.CharacterAbilities).CharacterModule
						local KillerExecAnims = RuntimeKillerModule.GameplayConfig.Cache.Animations[Config.ExecutionAnimation or "Execution"]
						local KillerAnim = ((KilledSurvivor.Skin and
							KillerExecAnims[KilledSurvivor.Skin..KilledSurvivor.Name]
							or KillerExecAnims[KilledSurvivor.Name]
							)
								or KillerExecAnims["Default"])
							.Killer

						-- If the survivor has a special death animation for this case, grab that, too.
						local SurvivorAnim = Utils.Character.LoadAnimationFromID(TargetCharacter, ExecutionAnim.Survivor)

						-- Grab the execution sound to play, if applicable; or use a default, if needed.
						local ExSound
						if Module.Config.Sounds.Execute then
							ExSound =
								(KilledSurvivor.Skin and
									Module.Config.Sounds.Execute[KilledSurvivor.Skin..KilledSurvivor.Name]
									or Module.Config.Sounds.Execute[KilledSurvivor.Name]
								)
								or Module.Config.Sounds.Execute["Default"]
						end

						-- Clear all of the target's effects (they won't need 'em where they're goin').
						TargetCharacter.Effects:ClearAllChildren()

						-- Anchor both the user and the target to play the animation
						Root.Anchored = true
						HRoot.Anchored = true

						-- Move in for the kill
						Char:PivotTo(CFrame.new(Root.Position, Vector3.new(HRoot.Position.X, Root.Position.Y, HRoot.Position.Z)))
						TargetCharacter:PivotTo(Root.CFrame * (Module.Config.ExecutionSurvivorCFrameOffset or CFrame.new(0, 0, -3)) * CFrame.fromEulerAnglesXYZ(0, math.rad(180), 0))

						-- Schedule some work for when the animation is over: to reset the execution and invincible state back to false for the user, and do some cleanup (including, y'know, actually killing the target the user just executed).
						task.delay(KillerAnim.Length, function()
							if Char then
								Char:SetAttribute("Invincible", false)
								Char:SetAttribute("Executing", false)
							end
							if Root then
								Root.Anchored = false
							end
							if HRoot then
								HRoot.Anchored = false
							end
							if Humanoid then
								Humanoid.Health = 0
							end
						end)

						-- Start playing the animation on both characters, emit the sound.
						KillerAnim:Play(0)
						SurvivorAnim:Play(0)
						if ExSound then
							Sounds.PlaySound(ExSound, {
								Parent = Root,
							})
						end

						-- Fire a signal for any on-execution abilities the killer may have.
						if RuntimeKillerModule.OnExecution then
							RuntimeKillerModule:OnExecution(TargetCharacter, Config.ExecutionAnimation or "Execution")
						end
					end)

				end

				-- Inflict the hitbox's damage if possible and do some knockback if existent.
				DamageHumanoid()

				-- If the hitbox applies effects, apply them.
				if Config.EffectsToApply then
					for _, Effect in Config.EffectsToApply do
						CommonFunctions.ApplyEffect({
							TargetHumanoid = Humanoid,
							EffectSettings = {
								Name = Effect.Name,
								Level = Effect.Level,
								Duration = Effect.Duration,
								Subfolder = Effect.Subfolder,
							},
							OverwriteExistingEffect = Effect.OverwriteExisting,
						})
					end
				end

				-- If the hitbox has a callback tied to it, fire it.
				Hitbox._RunCallback(Config.Connections, "PostHit", Config, Humanoid, Index == 1)
			end

			-- Repeat the hitbox parsing check once every 40th of a second.
			Props.TimePast += task.wait(1/40)
		end

		-- If the hitbox has a callback tied to it for when it ends, run that.
		Hitbox._RunCallback(Config.Connections, "Ended")
	end)

	return Props
end

-- Coordinate a callback for anything that would need to be listening to the hitbox's various states (ending, hitting something, etc.).
function Hitbox._RunCallback(ConnList: {[string]: (any?) -> ()}?, ID: string, ...)
	if ConnList and ConnList[ID] then
		ConnList[ID](...)
	end
end

return Hitbox


local HitmarkerCreator = {}

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

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

local AppearTweenInfo = TweenInfo.new(0.4, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out)
local DisappearTweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.In)

local FinalBillboardSize = UDim2.fromScale(0.7, 0.7)

local HitmarkerPrefab = Utils.Instance.FindFirstChild(script, "Hitmarker")
local HitmarkerFolder = workspace.TempObjectFolders.Hitmarkers

local SoundGroup: SoundGroup = SoundService.SoundGroups.Master.UI

function HitmarkerCreator.new(position: Vector3, dmgAmount: number, playSound: boolean?): Part
	playSound = playSound or true

	local Hitmarker = HitmarkerPrefab:Clone()
	Hitmarker.Position = position
	Hitmarker.BillboardGui.TextLabel.Text = tostring(math.floor(dmgAmount * 100) / 100)
	Hitmarker.Parent = HitmarkerFolder
	--
	if playSound then
		local SoundID = Utils.PlayerData.GetPlayerSetting(Players.LocalPlayer, "Miscellaneous.HitsoundID")
		if SoundID and #SoundID > 0 then
			task.spawn(function()
				Sounds.PlaySound(SoundID, {Volume = 0.5, SoundGroup = SoundGroup})
			end)
		end
	end
    Debris:AddItem(Hitmarker, 5)
	task.spawn(function()
		TweenService:Create(Hitmarker.BillboardGui.TextLabel, AppearTweenInfo, {Size = FinalBillboardSize}):Play()
		task.wait(4)
		if Hitmarker then
			TweenService:Create(Hitmarker.BillboardGui.TextLabel, DisappearTweenInfo, {TextTransparency = 1}):Play()
		end
	end)

	return Hitmarker
end

return HitmarkerCreator