local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local QRCode = require(Packages.QRCode)
local React = require(Packages.React)

local ErrorCorrection = require(script.Parent.ErrorCorrection)

export type Matrix = { [number]: { [number]: number } }

local function useQRCodeMatrix(text: string, level: ErrorCorrection.Level): Matrix
	return React.useMemo(function()
		local _, matrix = (QRCode :: any).qrcode(text, ErrorCorrection.toNumber(level))
		return matrix
	end, { text, level })
end

return useQRCodeMatrix


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactRoblox = require(Packages.ReactRoblox)

local ContextProviders = require(Plugin.Source.Common.ContextProviders)
local MockPlugin = require(Plugin.Source.Testing.MockPlugin)

local View = Foundation.View

local e = React.createElement

return {
	name = "Studio Rich Presence",
	storyRoots = {
		script.Parent,
	},
	mapStory = function(Story)
		return function(props)
			return e(ContextProviders, {
				plugin = MockPlugin.new(),
				overlayGui = props.container,
				_TESTING_themeOverride = props.theme,
			}, {
				Container = e(View, {
					tag = "size-full row align-x-center align-y-center padding-xxlarge",
				}, {
					Story = e(Story, props),
				}),
			})
		end
	end,
	packages = {
		React = React,
		ReactRoblox = ReactRoblox,
	},
}


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)
local Sift = require(Packages.Sift)

local LocalStorageStore = require(Plugin.Source.Plugin.LocalStorageStore)
local Telemetry = require(script.Parent.fireEventAsync)
local Types = require(script.Parent.Types)
local UserSettingsStore = require(Plugin.Source.UserSettings.UserSettingsStore)

local View = Foundation.View
local Dialog = Foundation.Dialog
local Checkbox = Foundation.Checkbox
local ButtonVariant = Foundation.Enums.ButtonVariant

local createNextOrder = ReactUtils.createNextOrder
local useCallback = React.useCallback
local useState = React.useState

type Props = {
	onClose: (result: any?) -> (),
}

local function WelcomeDialog(props: Props)
	local nextOrder = createNextOrder()

	local userSettingsStore = UserSettingsStore.get()
	local localStorageStore = LocalStorageStore.get()

	local isOptedIn, setIsOptedIn = useState(true)

	local onClose = useCallback(function()
		Telemetry.fireEvent({
			type = "onboardingCompleted",
			value = Types.OnboardingCompleted.new({ optedIntoTelemetry = isOptedIn }),
		})

		localStorageStore.setStorage(function(prev)
			return Sift.Dictionary.join(prev, {
				hasCompletedOnboarding = true,
			})
		end)

		userSettingsStore.setStorage(function(prev)
			return Sift.Dictionary.join(prev, {
				collectAnonymousUsageData = isOptedIn,
			})
		end)

		props.onClose()
	end, { localStorageStore.setStorage :: any, userSettingsStore.setStorage, isOptedIn, props.onClose })

	local onCancel = useCallback(function()
		localStorageStore.setStorage(function(prev)
			return Sift.Dictionary.join(prev, {
				hasCompletedOnboarding = true,
			})
		end)
		props.onClose()
	end, { localStorageStore.setStorage :: any, props.onClose })

	return React.createElement(Dialog.Root, {
		disablePortal = false,
		hasBackdrop = true,
		onClose = onCancel,
		onPressedOutside = onCancel,
	}, {
		Title = React.createElement(Dialog.Title, {
			text = "Welcome to Studio Activity",
		}),

		Content = React.createElement(Dialog.Content, {
			LayoutOrder = nextOrder(),
		}, {
			Wrapper = React.createElement(View, {
				tag = "auto-xy col gap-large",
			}, {
				WelcomeParagraph = React.createElement(Dialog.Text, {
					tag = "auto-xy text-wrap text-body-medium",
					Text = "Studio Activity lets you show what you're working on in Roblox Studio as your Discord status. Link your Discord account to get started!",
					LayoutOrder = nextOrder(),
				}),

				TelemetryParagraph = React.createElement(Dialog.Text, {
					tag = "auto-xy text-wrap text-body-medium",
					Text = "This is a plugin built by one person. Anonymous usage data helps me understand how many people use it and where things break. You can change this anytime in settings.",
					LayoutOrder = nextOrder(),
				}),

				Checkbox = React.createElement(Checkbox, {
					label = "Help out by sending anonymous usage data",
					isChecked = isOptedIn,
					onActivated = setIsOptedIn,
					LayoutOrder = nextOrder(),
				}),
			}),
		}),

		Actions = React.createElement(Dialog.Actions, {
			actions = {
				{
					text = "Get Started",
					variant = ButtonVariant.Emphasis,
					onActivated = onClose,
				},
			},
			LayoutOrder = nextOrder(),
		}),
	})
end

return WelcomeDialog


--[[
	Re-exports protobuf-generated telemetry event constructors.

	Callers use this module instead of importing from the codegen path directly:

		local Types = require(Telemetry.Types)
		local event = { type = "pluginLoaded", value = Types.PluginLoaded.new({ ... }) }
]]

local Plugin = script:FindFirstAncestor("StudioActivity")
local api = require(Plugin.Source.Api.Protos.api.v1.api)

export type AccountLinkFlow = api.AccountLinkFlow

export type TelemetryEvent =
	{ type: "pluginLoaded", value: api.PluginLoaded }
	| { type: "uiOpened", value: api.UiOpened }
	| { type: "onboardingCompleted", value: api.OnboardingCompleted }
	| { type: "accountLinkStarted", value: api.AccountLinkStarted }
	| { type: "accountLinked", value: api.AccountLinked }
	| { type: "deviceCodeFlowFailed", value: api.DeviceCodeFlowFailed }
	| { type: "browserFlowFailed", value: api.BrowserFlowFailed }
	| { type: "accountRemoved", value: api.AccountRemoved }
	| { type: "presenceToggled", value: api.PresenceToggled }
	| { type: "profileSelected", value: api.ProfileSelected }
	| { type: "sessionError", value: api.SessionError }
	| { type: "pluginUnloaded", value: api.PluginUnloaded }

return table.freeze({
	AccountLinkFlow = api.AccountLinkFlow,
	PluginLoaded = api.PluginLoaded,
	UiOpened = api.UiOpened,
	OnboardingCompleted = api.OnboardingCompleted,
	AccountLinkStarted = api.AccountLinkStarted,
	AccountLinked = api.AccountLinked,
	DeviceCodeFlowFailed = api.DeviceCodeFlowFailed,
	BrowserFlowFailed = api.BrowserFlowFailed,
	AccountRemoved = api.AccountRemoved,
	PresenceToggled = api.PresenceToggled,
	ProfileSelected = api.ProfileSelected,
	SessionError = api.SessionError,
	PluginUnloaded = api.PluginUnloaded,
})


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local sha256 = require(Packages.sha256)

local getLocalUserId = require(script.Parent.getLocalUserId)

--[[
	The events we send to the backend need some persistent identifier so we can
	group them by the user they come from. However, we do not collect PII so we
	first need to anonymize the UserId before sending it off. That's what this
	function is for.
]]
local function getAnonymizedUserId(): string
	local plainUserId = getLocalUserId()
	local hash = sha256(tostring(plainUserId))
	return hash
end

return getAnonymizedUserId


local MockPlugin = {}
MockPlugin.__index = MockPlugin

function MockPlugin.new(): Plugin
	local self = {
		_settings = {},
	}

	return setmetatable(self, MockPlugin) :: any
end

function MockPlugin:GetSetting(settingName: string)
	return self._settings[settingName]
end

function MockPlugin:SetSetting(settingName: string, newValue: any)
	self._settings[settingName] = newValue
end

function MockPlugin:GetMouse()
	return {
		Icon = "rbxassetid://-1",
	}
end

return MockPlugin


export type SettingType = "checkbox" | "dropdown" | "number"

local SettingType = {
	Checkbox = "checkbox" :: "checkbox",
	Dropdown = "dropdown" :: "dropdown",
	Number = "number" :: "number",
}

export type SettingGroup = "UI" | "Telemetry"

local SettingGroup = {
	UI = "UI" :: SettingGroup,
	Telemetry = "Telemetry" :: SettingGroup,
}

export type SettingChoice = {
	description: string,
	displayName: string,
	name: string,
}

type BaseSetting = {
	name: string,
	group: SettingGroup,
	displayName: string,
	description: string,
	settingType: SettingType,
}

type CheckboxSetting = BaseSetting & {
	settingType: "checkbox",
	value: boolean,
}

type DropdownSetting = BaseSetting & {
	settingType: "dropdown",
	choices: { SettingChoice },
}

type NumberSetting = BaseSetting & {
	settingType: "number",
	range: NumberRange?,
	value: number,
}

export type Setting = CheckboxSetting | DropdownSetting | NumberSetting

local theme: DropdownSetting = {
	name = "theme",
	group = SettingGroup.UI,
	displayName = "UI theme",
	description = "Select the UI theme to use. By default, Studio Activity will use the same theme as Studio",
	settingType = SettingType.Dropdown,
	choices = {
		{
			name = "system",
			displayName = "System",
			description = "Match the theme selected for Studio",
		},
		{
			name = "dark",
			displayName = "Dark",
			description = "Force the theme to use dark mode",
		},
		{
			name = "light",
			displayName = "Light",
			description = "Force the theme to use light mode",
		},
	},
}

local collectAnonymousUsageData: CheckboxSetting = {
	name = "collectAnonymousUsageData",
	group = SettingGroup.Telemetry,
	displayName = "Help improve Studio Activity by sending anonymous usage data",
	description = "Allow Studio Activity to collect anonymous usage data to help improve the product. No personal data is collected",
	settingType = SettingType.Checkbox,
	value = true,
}

local settings = {
	theme = theme,
	collectAnonymousUsageData = collectAnonymousUsageData,
}

export type Settings = typeof(settings)

return settings


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactCharm = require(Packages.ReactCharm)
local Sift = require(Packages.Sift)

local DefaultSettings = require(script.Parent.DefaultSettings)
local UserSettingsStore = require(script.Parent.UserSettingsStore)

local View = Foundation.View
local Checkbox = Foundation.Checkbox
local Dropdown = Foundation.Dropdown
local NumberInput = Foundation.NumberInput
local InputSize = Foundation.Enums.InputSize
local useTokens = Foundation.Hooks.useTokens

local useSignalState = ReactCharm.useSignalState

local e = React.createElement
local useCallback = React.useCallback
local useMemo = React.useMemo

type Setting = DefaultSettings.Setting
type SettingChoice = DefaultSettings.SettingChoice

type Props = {
	setting: Setting,
	LayoutOrder: number?,
}

local function SettingRow(props: Props)
	local tokens = useTokens()

	local userSettingsStore = UserSettingsStore.get()
	local userSettings = useSignalState(userSettingsStore.getStorage)

	local setSetting = useCallback(function(newValue: any)
		userSettingsStore.setStorage(function(prev)
			return Sift.Dictionary.join(prev, {
				[props.setting.name] = newValue,
			})
		end)
	end, { userSettingsStore.setStorage :: any, props.setting })

	local optionElement = useMemo(function(): React.Node
		local value = userSettings[props.setting.name]

		if props.setting.settingType == "checkbox" then
			return e(Checkbox, {
				label = props.setting.displayName,
				hint = props.setting.description,
				size = InputSize.Small,
				isChecked = value,
				onActivated = setSetting,
			})
		elseif props.setting.settingType == "dropdown" then
			local items = Sift.List.map(props.setting.choices, function(choice: SettingChoice): Foundation.DropdownItem
				return {
					id = choice.name,
					text = choice.displayName,
				}
			end)

			return e(Dropdown.Root, {
				label = props.setting.displayName,
				hint = props.setting.description,
				size = InputSize.Small,
				width = UDim.new(1, 0),
				value = value,
				items = items,
				onItemChanged = setSetting,
			})
		elseif props.setting.settingType == "number" then
			local range = props.setting.range

			return e(NumberInput, {
				value = value,
				label = props.setting.displayName,
				hint = props.setting.description,
				size = InputSize.Small,
				step = 10,
				maximum = if range then range.Max else nil,
				minimum = if range then range.Min else nil,
				onChanged = setSetting,
			})
		end
		error(`no handling for setting type {props.setting.settingType}`)
	end, { props.setting :: any, setSetting, userSettings })

	local hasBeenChanged = not userSettingsStore.isSettingDefault(props.setting.name)

	return e(View, {
		tag = "size-full-0 auto-y",
		LayoutOrder = props.LayoutOrder,
	}, {
		ChangedIndicator = if hasBeenChanged
			then e(View, {
				tag = "size-50-full",
				AnchorPoint = Vector2.new(1, 0),
				Position = UDim2.fromOffset(-8, 0),
				backgroundStyle = tokens.Color.System.Emphasis,
			})
			else nil,

		OptionWrapper = e(View, {
			tag = "size-full-0 auto-y",
		}, {
			Option = optionElement,
		}),
	})
end

return React.memo(SettingRow)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)

local SettingsView = require(script.Parent.SettingsView)

local Text = Foundation.Text
local Sheet = Foundation.Sheet
local DialogSize = Foundation.Enums.DialogSize

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement

type Props = {
	isOpen: boolean,
	onClose: (result: any?) -> (),
}

local function SettingsModal(props: Props)
	local nextOrder = createNextOrder()

	return e(Sheet.Root, {
		isOpen = props.isOpen,
		preferCenterSheet = true,
		size = DialogSize.Medium,
		onClose = props.onClose,
	}, {
		Header = e(Sheet.Header, {
			hasCloseAffordance = true,
		}, {
			Title = e(Text, {
				tag = "size-full-0 auto-y text-heading-small content-emphasis text-wrap text-align-x-left",
				Text = "Settings",
				LayoutOrder = nextOrder(),
			}),
		}),

		Content = props.isOpen and e(Sheet.Content, nil, {
			SettingsView = e(SettingsView),
		}),
	})
end

return SettingsModal


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)
local Sift = require(Packages.Sift)

local DefaultSettings = require(script.Parent.DefaultSettings)
local SettingRow = require(script.Parent.SettingRow)

local View = Foundation.View
local Text = Foundation.Text

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement
local useMemo = React.useMemo

type Setting = DefaultSettings.Setting
type SettingChoice = DefaultSettings.SettingChoice

type SettingsGroup = {
	name: string,
	settings: { Setting },
}

local function SettingsView()
	local nextOrder = createNextOrder()

	local settingsByGroup: { SettingsGroup } = useMemo(function()
		local settings = Sift.Dictionary.values(DefaultSettings)

		local sorted = Sift.Array.sort(settings, function(a: Setting, b: Setting)
			return a.name < b.name
		end)

		local groups = Sift.Array.reduce(sorted, function(accumulator: { [string]: SettingsGroup }, setting: Setting)
			local group: SettingsGroup
			if accumulator[setting.group] then
				group = accumulator[setting.group]
			else
				group = {
					name = setting.group,
					settings = {},
				}
				accumulator[setting.group] = group
			end

			table.insert(group.settings, setting)

			return accumulator
		end, {})

		return Sift.Array.sort(Sift.Dictionary.values(groups), function(a: SettingsGroup, b: SettingsGroup)
			return a.name < b.name
		end)
	end, { DefaultSettings })

	local children: { [string]: React.Node } = {}
	for index, group in settingsByGroup do
		local isFirst = index == 1
		children[group.name] = e(Text, {
			tag = {
				["auto-xy text-title-large padding-bottom-medium"] = true,
				["padding-top-medium"] = not isFirst,
			},
			Text = group.name,
			LayoutOrder = nextOrder(),
		})

		for _, setting in group.settings do
			children[setting.name] = e(SettingRow, {
				setting = setting,
				LayoutOrder = nextOrder(),
			})
		end
	end

	return e(View, {
		tag = "size-full-0 auto-y col padding-bottom-medium",
	}, children)
end

return React.memo(SettingsView)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)
local t = require(Packages.t)

local createPluginSettingsStore = require(Plugin.Source.Plugin.createPluginSettingsStore)
local defaultSettings = require(script.Parent.DefaultSettings)

export type UserSettings = {
	theme: string,
	collectAnonymousUsageData: boolean,
}

local defaultValue: UserSettings = {
	theme = defaultSettings.theme.choices[1].name,
	collectAnonymousUsageData = defaultSettings.collectAnonymousUsageData.value,
}

local validate = t.interface({
	theme = t.string,
	collectAnonymousUsageData = t.boolean,
})

-- defaultValue needs to be kept in sync with the keys in defaultSettings
for key in defaultSettings do
	assert(defaultValue[key] ~= nil, `setting with key {key} is missing from UserSettingsStore`)
end

return {
	get = Charm.computed(function()
		return createPluginSettingsStore("PresenceUserSettings", defaultValue, validate)
	end),
}


export type upsideEngine = {
	Version: string,
	Workspace: {},
	new: ((name: "PhysicalObject") -> PhysicalObject)
		& ((name: "StaticObject") -> StaticObject)
		& ((name: "Fluid") -> Fluid)
		& ((name: "Parallax") -> Parallax)
		& ((name: "Sprite") -> Sprite)
		& ((name: "Sound") -> Sound)
		& ((name: "Environment") -> Environment)
		& ((name: "Light") -> Light)
		& ((name: "Character") -> Character)
		& ((name: "Scene") -> Scene)
		& ((name: "Particle") -> Particle)
		& ((name: "Shader") -> Shader)
		& ((name: "TextTag") -> TextTag)
		& ((name: "ReactiveLabel") -> ReactiveLabel)
		& ((name: "ProximityPrompt2D") -> ProximityPrompt2D),

	GetService: ((name: "SceneManager") -> SceneManager)
		& ((name: "PluginSupportService") -> PluginSupportService)
		& ((name: "NetworkingService") -> NetworkingService)
		& ((name: "TextTagService") -> TextTagService)
		& ((name: "AuthorityService") -> AuthorityService)
		& ((name: "CrossPlatformService") -> CrossPlatformService),
}

return {}


-- THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.

export type Dictionary<k, v> = { [k]: v }
export type Array<v> = { [number]: v }
export type ChromaticAberrationConnection = {}
export type SubjectDestroyConnection = {}
export type CharacterDestroyConnection = {}
export type AuthorityType = "Server" | "Client"

export type Event = {
	Functions: { [number]: (any) -> () },
	Threads: { [number]: thread },
}
export type ShadingParams = {
	x: number,
	y: number,
	red: number,
	green: number,
	blue: number,
	opacity: number,
	rotate: (centre: Vector2 | Vector3, position: Vector2 | Vector3, degrees: number) -> (number, number),
	texture: (imageLabel: ImageLabel, position: Vector2 | Vector3) -> (),
}

export type RequestContent = {
	ClassName: string,
	ObjectId: string,
	Name: string,
	ObjectProperties: { [string]: any },
	Instance: { [string]: any },
}

export type Raycast2dResult = {
	Object: PhysicalObject,
	Position: UDim2,
	Distance: Vector2,
	Normal: UDim2,
}

export type Raycast2dParams = {
	FilterType: string,
	From: Vector2,
	List: Array<BaseObject>,
	To: Vector2,
}

export type UpsideEngineInput = {
	Action: string,
	Position: Vector2,
	KeyCode: Enum.KeyCode,
}

export type BaseClass = {
	Id: string,
	ClassName: string,
	Name: string,
	new: () -> BaseClass,
	SetName: (self: BaseClass, name: string) -> (),
	IsA: (self: BaseClass, className: string) -> boolean,
	Clone: (self: BaseClass, ...any) -> BaseClass,
	Destroy: (self: BaseClass) -> (),
}


interface upsideEngine {
	Version: string;
	Workspace: {};
	new: ((name: "PhysicalObject") => PhysicalObject)
		& ((name: "Sprite") => Sprite)
		& ((name: "StaticObject") => StaticObject)
		& ((name: "Fluid") => Fluid)
		& ((name: "Parallax") => Parallax)
		& ((name: "Sound") => Sound)
		& ((name: "Environment") => Environment)
		& ((name: "Light") => Light)
		& ((name: "Character") => Character)
		& ((name: "Scene") => Scene)
		& ((name: "Particle") => Particle)
		& ((name: "Shader") => Shader)
		& ((name: "TextTag") => TextTag)
		& ((name: "ReactiveLabel") => ReactiveLabel)
		& ((name: "ProximityPrompt2D") => ProximityPrompt2D),

	GetService: ((name: "SceneManager") => SceneManager)
		& ((name: "PluginSupportService") => PluginSupportService)
		& ((name: "NetworkingService") => NetworkingService)
		& ((name: "TextTagService") => TextTagService)
		& ((name: "AuthorityService") => AuthorityService)
		& ((name: "CrossPlatformService") => CrossPlatformService),
}

declare const UpsideEngine: upsideEngine;
export default UpsideEngine;


local replicatedStorage = game:GetService("ReplicatedStorage")
local upsideEngine = replicatedStorage:WaitForChild("UpsideEngine") :: ModuleScript
require(upsideEngine)


local TestEZ = require(script.Parent.DevPackages.TestEZ)
TestEZ.TestBootstrap:run({
    script.Parent.UpsideEngine,
})


local props = {
	NonReplicable = {
		-- BaseClass
		"ClassNameHistory",
		"ClassName",

		-- EventEmitter
		"EventsStorage",

		-- BaseObject
		"Instance",

		-- Particle
		"Units",
		"Clock",

		-- Physical Object
		"Collisions",
		"IsGrounded",

		-- Sprite
		"Active",

		-- Character
		"IsJumping",

		-- Scene
		"Camera",
		"Objects",
		"ParticleEnvironment",
		"SoundEnvironment",
		"ShaderEnvironment",

		-- Environment
		"Content",

		-- LightingEnvironment
		"__actrees",

		-- ProximityPrompt2d
		"Label",
		"HitboxButton",
		"IsClosest",
	},
	Instance = {
		"ZIndex",
		"Position",
		"ScaleType",
		"TileSize",
		"SliceScale",
		"ImageTransparency",
		"ImageRectSize",
		"ImageRectOffset",
		"Rotation",
		"ResampleMode",
		"ImageColor3",
		"Size",
		"Image",
		"SizeConstraint",
	},
}

export type InstanceProps = typeof(props.Instance)

return props


local runService = game:GetService("RunService")
local lib = script.Parent.Parent.Parent.Lib
local player = game:GetService("Players").LocalPlayer

local actorLoader = lib:WaitForChild("ActorLoader")
local util = require(lib.Util)

local copyTable = util.CopyTable
local isEqual = util.IsEqual

local isClient = runService:IsClient()
local actor = {}
actor.__index = actor

function actor.new(module: ModuleScript)
	local instance = Instance.new("Actor")
	instance.Parent = if isClient then player:FindFirstChild("PlayerScripts") else workspace

	local loader = actorLoader:Clone()
	loader.Parent = instance

	local ready = Instance.new("BindableEvent")
	ready.Name = "ready"
	ready.Parent = loader

	if not runService:IsRunMode() then
		ready.Event:Wait()
		ready:Destroy()
	end

	instance:SendMessage("init", module)

	return setmetatable({
		instance = instance,
		__cache = {},
	}, actor)
end

function actor:SetVariable(name: string, value: any, ignoreCache: boolean)
	if not ignoreCache then
		local equal = isEqual(self.__cache[name], value)
		if equal then
			self:UpdateVariable(name)
			return
		end
	end

	self.__cache[name] = if typeof(value) == "table" then copyTable(value) else value
	self.instance:SendMessage("setVariable", name, value)
end

function actor:SetRunMode(name)
	self.instance:SendMessage("setRunMode", name)
end

function actor:UpdateVariable(name)
	self.instance:SendMessage("updateVariable", name)
end

function actor:Execute(name: string)
	self.instance:SendMessage("execute", name)
end

function actor:ExecuteEveryStep(name: string)
	self.instance:SendMessage("executeEveryStep", name)
end

return actor


local isEqual = require(script.Parent.Parent.Parent.Lib.Util.Internal.IsEqual)
local actrees = {}
actrees.__index = actrees

function actrees.new()
	return setmetatable({
		__actors = {},
		__cache = {},
	}, actrees)
end

function actrees:AddActor(actor)
	table.insert(self.__actors, actor)
end

function actrees:SetVariable(name, value)
	local equal = isEqual(self.__cache[name], value)
	local method = if equal then "updateVariable" else "setVariable"
	local arg = if equal then nil else value

	for _, actor in self do
		actor.instance:SendMessage(method, name, arg)
	end
end

function actrees:__iter()
	return next, self.__actors
end

return actrees


local upsideEngine = script.Parent.Parent.Parent
local appData = upsideEngine.AppData

local copyTable = require(upsideEngine.Lib.Util.Internal.CopyTable)
local properties = require(appData.Properties)
local data = require(appData.Data)

local environments = {
	"SoundEnvironment",
	"LightingEnvironment",
	"ParticleEnvironment",
	"ShaderEnvironment",
	"Objects",
}

local baseClass = {}
baseClass.__index = baseClass

local function set(self, clon, index)
	clon[index] = self[index]
end

function baseClass.new(): BaseClass
	local id = tostring(math.random()):split(".")[2]
	local self = {
		Id = id,
		ClassName = script.Name,
		ClassNameHistory = "",
		Name = "unamed-" .. id,
	}

	data.workSpace[self.Name] = self
	data.objects[id] = self

	return setmetatable(self, baseClass) :: any
end

function baseClass:Clone()
	local clon = self.new()
	local id = clon.Id
	local instance = clon.Instance

	copyTable(self, clon)
	clon.Instance = instance
	clon.Id = id

	for _, index in properties.Instance do
		pcall(set, self.Instance, clon.Instance, index)
	end

	return clon
end

function baseClass:SetName(name: string)
	local instance = self.Instance
	if instance then
		instance.Name = name
	end

	data.workSpace[name] = self
	data.workSpace[self.Name] = nil
	self.Name = name
end

function baseClass:Destroy()
	local scene = data.objects[self.Scene]
	local id = self.Id

	if self.ClassNameHistory and self:IsA("EventEmitter") then
		self:Fire("Destroy")
	end

	if self.Instance then
		self.Instance:Destroy()
		data.workSpace[self.Name or ""] = nil
	end

	table.clear(self)
	if not scene then
		return
	end

	for _, environment in environments do
		local env = scene[environment]
		if not (env and env:HasOne(id)) then
			continue
		end

		env:RemoveOne(id)
	end
end

function baseClass:IsA(className: string)
	return self.ClassNameHistory:match(className) or self.ClassName == className
end

function baseClass:SetClassName(className)
	self.ClassName = className
	self.ClassNameHistory ..= className
end

return baseClass


local eventEmitter = require(script.Parent.EventEmitter)
local classEnvironments = {
	Sound = "SoundEnvironment",
	Light = "LightingEnvironment",
	Particle = "ParticleEnvironment",
}

local baseObject = {}
baseObject.__index = baseObject
--[[={
	@link BaseObject.md
}=]]

function baseObject.new(instanceType: string?): BaseObject
	local self = eventEmitter.new()
	self:SetClassName(script.Name)
	self.Instance = Instance.new(instanceType or "ImageLabel")
	self.Scene = "SceneId"
	self.Tags = {}

	return setmetatable(self, baseObject)
end

--[[={
	@desc Sets the object scene
	@link void.link
}=]]

function baseObject:SetScene(scene: Scene)
	local environment = classEnvironments[self.ClassName]
	scene.Objects:SetOne(self, self.Id)

	self.Scene = scene.Id
	self.Instance.Parent = scene.Instance:WaitForChild("GameFrame")

	if environment then
		environment = (scene :: any)[environment]
		environment:SetOne(self, self.Id)
	end
end

--[[={
	@desc Adds a tag to the object
	@link void.link
}=]]

function baseObject:AddTag(tag: string)
	self.Tags[tag] = true
end

--[[={
	@desc Checks if the object contain the specified tag
	@link boolean.link
}=]]

function baseObject:HasTag(tag: string): boolean
	return self.Tags[tag]
end

--[[={
	@desc Removes a tag from the object
	@link void.link
}=]]

function baseObject:RemoveTag(tag: string)
	self.Tags[tag] = nil
end

--[[={
	@desc This class is the base class of the majority of classes
	@about
		@Instance The object instance
		@Scene The object scene ID
		@InstanceType The class name of the object instance
		@Tags This table stores all the tags of the object
}=]]

return setmetatable(baseObject, eventEmitter)


local upsideEngine = script.Parent.Parent.Parent

local setSmoothPosition = require(script.Parent.Parent.Parent.Lib.Util.Math.SetSmoothPosition)
local character = require(script.Parent.Parent.Public.Character)
local eventEmitter = require(upsideEngine.Classes.Internal.EventEmitter)
local camera = {}
camera.__index = camera

local function disconnectSubject(self)
	if self.SubjectDestroyConnection then
		self.SubjectDestroyConnection:Disconnect()
		self.SubjectDestroyConnection = nil
	end
end

local function bindSubject(self, subject)
	disconnectSubject(self)
	self.Subject = subject

	if not subject then
		return
	end

	self.SubjectDestroyConnection = subject:On("Destroy", function()
		if self.Subject ~= subject then
			return
		end

		disconnectSubject(self)
		self.Subject = nil
	end)
end

--[[={
	@link Camera.md
}=]]

function camera.new(scene: Scene): Camera
	local self = eventEmitter.new()
	self:SetClassName(script.Name)
	self.OffsetPosition = UDim2.fromOffset(0, 0)
	self.LocalPosition = Vector2.zero
	self.Limits = Vector2.zero
	self.FollowSubject = true
	self.Scene = scene.Id
	self.Smoothness = 0.7
	self.Instance = scene.Instance:WaitForChild("GameFrame")
	local defaultSubject = character.new()
	self.Subject = defaultSubject
	self.SubjectDestroyConnection = nil
	bindSubject(self, defaultSubject)

	return setmetatable(self, camera)
end

--[[={
	@desc Gets the camera position
	@link udim2.link
}=]]

function camera:GetPosition(): UDim2
	return self.Instance.Position
end

--[[={
	@desc Sets the camera position
	@link void.link
}=]]

function camera:SetPosition(udim2: UDim2)
	local position = udim2 + self.OffsetPosition
	self.LocalPosition = Vector2.new(udim2.X.Offset, udim2.Y.Offset)
	setSmoothPosition(self, self.Smoothness, position)
end

--[[={
	@desc Sets the camera subject
	@link void.link
}=]]

function camera:SetSubject(subject: Character)
	bindSubject(self, subject)
end

--[[={
	@desc Position the camera in a centered location relative to the provided object
	@link void.link
}=]]

function camera:LookTo(object: StaticObject)
	assert(object:IsA("StaticObject"), ("StaticObject expected, got %s"):format(typeof(object)))
	local resolution = workspace.CurrentCamera.ViewportSize
	local center = resolution / 2

	local target = (object.Instance :: ImageLabel).Position
	target = Vector2.new(target.X.Offset, target.Y.Offset)

	local result = center + (center - target)
	self:SetPosition(UDim2.fromOffset(result.X, result.Y))
end

--[[={
	@desc This class is included on every scene, it's used to move you around the scene
	@about
		@Subject This property defines the object which is going to follow the camera
		@OffsetPosition This property serves to move as many pixels as you want the camera to adjust it to a desired position
		@LocalPosition This property is used to move the camera internally
		@Scene This is the Scene Id of the camera
		@Limits This property marks the limits to move the camera, for example, if you set `Vector2.new(0.5, 0.5)` the camera will move only when it reaches the limit
		@FollowSubject This property defines if the camera is going to follow the defined subject
		@Smoothness This property defines the smoothness with which the camera will move, it only works in a range between 0 and 1
}=]]

return setmetatable(camera, eventEmitter)


local baseClass = require(script.Parent.BaseClass)
local connection = {}
connection.__index = connection

local function removeThread(event, running)
	for index, thread in event.Threads do
		if thread == running then
			table.remove(event.Threads, index)
			break
		end
	end

	return running
end

local function resumeThread(event, running, ...)
	if coroutine.status(running) ~= "suspended" then
		return
	end

	removeThread(event, running)
	coroutine.resume(running, ...)
end

--[[={
	@link Connection.md
}=]]

function connection.new(eventId: number, event: Event)
	local self = baseClass.new()
	self:SetClassName(script.Name)
	self.Active = true
	self.EventId = eventId or 0
	self.Event = event or {}

	return setmetatable(self, connection)
end

--[[={
	@desc Wait until the event gets fired, if seconds were specified, once the specified seconds have elapsed, it will stop waiting
	@link thread.link
}=]]

function connection:Wait(seconds: number?): thread?
	local running = coroutine.running()
	if not self.Active then
		return
	end

	if seconds then
		task.delay(seconds, resumeThread, self.Event, running)
	end

	table.insert(self.Event.Threads, running)
	return coroutine.yield()
end

--[[={
	@desc Deletes the connection and the listener
	@link void.link
}=]]

function connection:Disconnect()
	self.Active = false
	self.Event.Functions[self.EventId] = nil
	self:Destroy()
end

--[[={
	@desc This class is used in the event emitter class, its used to manage a listener
	@about
		@Active Defines if the connection is active or not
		@EventId The id of the linked event
}=]]

return setmetatable(connection, baseClass)


local baseClass = require(script.Parent.BaseClass)
local connection = require(script.Parent.Connection)

local eventEmitter = {}
eventEmitter.__index = eventEmitter

local function getEvent(events, name)
	if not events[name] then
		events[name] = {
			Functions = {},
			Threads = {},
		}
	end

	return events[name]
end

local function cleanTrace(trace)
	local result = {}
	for line in trace:gmatch("[^\n]+") do
		if not line:find("Internal.EventEmitter") then
			table.insert(result, line)
		end
	end

	return table.concat(result, "\n")
end

local function errorHandler(err)
	local errData = err:split(": ")
	local trace = debug.traceback(errData[2])
	return cleanTrace(trace)
end

function eventEmitter.new(): EventEmitter
	local self = baseClass.new()
	self:SetClassName(script.Name)
	self.EventsStorage = {}

	return setmetatable(self, eventEmitter)
end

--[[={
	@desc Executes the provided function when the specified event gets fired
	@link Connection.md
}=]]

function eventEmitter:On(name: string, callback: () -> any): Connection
	local event = getEvent(self.EventsStorage, name)
	local id = #event.Functions + 1

	local function wrapper(...)
		local args = { ... }
		local sucess, err = xpcall(function()
			callback(unpack(args))
		end, errorHandler)

		if not sucess then
			warn(("Error: %s"):format(err))
		end
	end

	local cn = connection.new(id, event)
	event.Functions[id] = callback and wrapper or nil

	return cn
end

--[[={
	@desc Executes the provided function when the specified event gets fired, but only once
	@link Connection.md
}=]]
function eventEmitter:Once(name: string, callback: () -> any): Connection
	local event = getEvent(self.EventsStorage, name)
	local id = #event.Functions + 1

	local function wrapper(...)
		if callback then
			callback(...)
		end
		event.Functions[id] = nil
	end

	return self:On(name, wrapper)
end

--[[={
	@desc Triggers an event with the specified arguments
	@link void.link
}=]]

function eventEmitter:Fire(name: string, ...)
	local event = getEvent(self.EventsStorage, name)
	local functions = event.Functions

	if #functions > 0 then
		for _, func in functions do
			coroutine.wrap(func)(...)
		end
	end

	for _, thread in event.Threads do
		coroutine.resume(thread, ...)
	end

	table.clear(event.Threads)
end

--[[={
	@desc The event emitter is used to manage the events of a class
	@about
		@EventsStorage This table store all the events of the class
}=]]

return setmetatable(eventEmitter, baseClass)


local upsideEngine = script.Parent.Parent.Parent
local lightingSystem = upsideEngine.Systems.Lighting

local actor = require(script.Parent.Actor)
local actrees = require(script.Parent.Actrees)
local environment = require(upsideEngine.Classes.Public.Environment)

local lightingEnvironment = {}
lightingEnvironment.__index = lightingEnvironment

--[[={
	@link LightingEnvironment.md
}=]]

function lightingEnvironment.new(scene: Scene): LightingEnvironment
	local self = environment.new("Frame")
	self:SetClassName(script.Name)
	self.AmbientColor = Color3.fromRGB()
	self.AmbientTransparency = 0.2
	self.UpdateFrequency = 0.025
	self.LightIntensity = 1
	self.LightStyle = Enum.ResamplerMode.Pixelated
	self.ChunkColumns = 1
	self.ChunkResolution = Vector2.new(64, 64)
	self.__actrees = actrees.new()

	local gameFrame = scene.Instance:WaitForChild("GameFrame")
	local instance = self.Instance
	instance.Parent = gameFrame.Parent
	instance.Name = "Lighting"
	instance.Size = gameFrame.Size
	instance.Position = gameFrame.Position
	instance.AnchorPoint = gameFrame.AnchorPoint
	instance.BackgroundTransparency = 1

	return setmetatable(self, lightingEnvironment)
end

--[[={
	@desc Draws the pixels for the scene with the specified resolution
	@link void.link
}=]]

function lightingEnvironment:DrawPixels()
	local gameFrame = self.Instance.Parent:WaitForChild("GameFrame")
	local instance = self.Instance
	instance.Size = gameFrame.Size
	instance.Position = gameFrame.Position
	instance.AnchorPoint = gameFrame.AnchorPoint

	local chunkCols = self.ChunkColumns
	local chunkSize = self.Instance.AbsoluteSize.X / chunkCols

	local chunkX = math.clamp(self.ChunkResolution.X, 0, 128)
	local chunkY = math.clamp(self.ChunkResolution.Y, 0, 128)
	local chunkResolution = Vector2.new(chunkX, chunkY)

	local chunk = Instance.new("ImageLabel")
	chunk.Size = UDim2.fromOffset(chunkSize, chunkSize)
	chunk.BackgroundTransparency = 1
	chunk.BorderSizePixel = 0
	chunk.ZIndex = 1000
	chunk.ResampleMode = self.LightStyle

	for col = 0, chunkCols - 1 do
		local chkActor = actor.new(lightingSystem)
		self.__actrees:AddActor(chkActor)

		chkActor:SetVariable("pixels", {})
		chkActor:SetVariable("lights", {})
		chkActor:SetVariable("resolution", self.Instance.AbsoluteSize)
		chkActor:SetVariable("details", {
			col = col,
			chunk = chunk,
			chunkSize = chunkSize,
			chunkResolution = chunkResolution,
			screen = self.Instance,
			screenSize = self.Instance.AbsoluteSize,
		})
		chkActor:SetVariable("lighting", {
			ambientColor = self.AmbientColor,
			ambientTransparency = self.AmbientTransparency,
			updateFrequency = self.UpdateFrequency,
			lightIntensity = self.LightIntensity,
		})
	end

	task.defer(function()
		for _, chkActor in self.__actrees do
			chkActor:Execute("build")
			chkActor:ExecuteEveryStep("draw")
		end
	end)
end

--[[={
	@desc Updates the position, the transparency and the color of the ambient
	@link void.link
}=]]

function lightingEnvironment:UpdateScreen()
	local actree = self.__actrees
	actree:SetVariable("lighting", {
		ambientColor = self.AmbientColor,
		ambientTransparency = self.AmbientTransparency,
		updateFrequency = self.UpdateFrequency,
		lightIntensity = self.LightIntensity,
		lightStyle = self.LightStyle,
		chunkResolution = self.ChunkResolution,
	})

	for _, actor in self.__actrees do
		actor:Execute("updateScreen")
	end
end

--[[={
	@desc This class is one of the most important components of a scene, is used to manage everything related with the light
	@about
		@AmbientColor This is the color of every pixel of the ambient, we can say is the darkness color
		@AmbientTransparency This is the transparency of the ambient, we can say is the darkness transparency
		@UpdateFrequency This is the seconds between every update of the pixels in the screen
		@LightIntensity This is the intensity for all the lights in the scene
		@LightStyle Determines the light rendering mode, there are two modes Enum.ResamplerMode.Pixelated and Enum.ResamplerMode.Default, the default value is Enum.ResamplerMode.Pixelated
		@ChunkResolution Determines the resolution for every chunk of the screen
		@ChunkColumns Determines the number of columns for the chunks, the number of rows is calculated automatically, the default value is 1
}=]]

return setmetatable(lightingEnvironment, environment)


local upsideEngine = script.Parent.Parent.Parent
local environment = require(upsideEngine.Classes.Public.Environment)

local objectEnvironment = {}
objectEnvironment.__index = objectEnvironment
objectEnvironment.__iter = environment.__iter

function objectEnvironment.new(): ObjectEnvironment
	local self = environment.new()
	self:SetClassName(script.Name)

	return setmetatable(self, objectEnvironment)
end

return setmetatable(objectEnvironment, environment)


local upsideEngine = script.Parent.Parent.Parent
local environment = require(upsideEngine.Classes.Public.Environment)

local particleEnvironment = {}
particleEnvironment.__index = particleEnvironment

function particleEnvironment.new(): ParticleEnvironment
	local self = environment.new()
	self:SetClassName(script.Name)

	return setmetatable(self, particleEnvironment)
end

return setmetatable(particleEnvironment, environment)


local upsideEngine = script.Parent.Parent.Parent

local runService = game:GetService("RunService")
local players = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")

local copyTable = require(script.Parent.Parent.Parent.Lib.Util.Internal.CopyTable)
local ReplicationHandlers = require(script.Parent.Parent.Parent.Lib.Util.Replication.Handlers)
local eventEmitter = require(upsideEngine.Classes.Internal.EventEmitter)

local classes = {}
local networking

local isServer = runService:IsServer()
local socket

if isServer then
	socket = replicatedStorage:FindFirstChild("UpsideEngineSocket") or Instance.new("RemoteEvent")
	socket.Name = "UpsideEngineSocket"
	socket.Parent = replicatedStorage
else
	socket = replicatedStorage:WaitForChild("UpsideEngineSocket")
end

--[[={
	@link Request.md
}=]]

local request = {}
request.__index = request

local function setProperty(object, property, value)
	object[property] = value
end

local function applyProperties(object, properties)
	for index, value in properties do
		pcall(setProperty, object, index, value)
	end
end

function request.new(clientId: number?, content: RequestContent?): Request
	local data = if content then content else {}
	local playerId = if clientId ~= nil
		then clientId
		elseif players.LocalPlayer ~= nil then players.LocalPlayer.UserId
		else 0

	local self = eventEmitter.new()
	self:SetClassName(script.Name)

	self.Content = data
	self.ClientId = playerId

	return setmetatable(self, request)
end

--[[={
	@desc Sends the request to the server
	@link void.link
}=]]

function request:Send(content: Dictionary<string, any>)
	local id = content.ObjectId
	networking.Pending[id] = {
		ClientId = self.ClientId,
		Content = content,
	}
end

--[[={
	@desc The request is approved and its replicated all the clients
	@link void.link
}=]]

function request:Approve()
	if not isServer then
		return
	end

	self:Send(self.Content)
end

--[[={
	@desc Accepts the request and builds the object to be replicated
	@link BaseObject.md
}=]]

function request:Accept(): BaseObject
	local content = self.Content
	local class = classes[content.ClassName]

	local playerData = networking.PlayersData[self.ClientId] or {}
	local exists = playerData[content.ObjectId] ~= nil

	local clon = playerData[content.ObjectId] or class.new()
	local instance = clon.Instance

	if not exists and isServer then
		local clientRequests = networking.RequestsCache[self.ClientId] or {}
		networking.RequestsCache[self.ClientId] = clientRequests
		local currentCache = clientRequests[content.ObjectId] or {}
		clientRequests[content.ObjectId] = copyTable(currentCache, content)
	end

	clon.Id = content.ObjectId
	applyProperties(instance, content.Instance)
	copyTable(content.ObjectProperties, clon)

	for className, handler in ReplicationHandlers do
		if clon:IsA(className) then
			handler(clon)
		end
	end

	local this = networking.ReplicationTarget[clon] or {}
	local position = content.Instance.Position or this.Position

	this.Position = position
	networking.ReplicationTarget[clon] = this

	if exists then
		return clon
	end

	playerData[content.ObjectId] = clon
	clon:SetName(content.Name)
	networking:Fire("Build", clon)

	return clon
end

--[[={
	@desc Returns the player which sent the request
	@link player.link
}=]]

function request:GetClient(): Player
	return players:GetPlayerByUserId(self.ClientId)
end

task.defer(function()
	for _, module in upsideEngine.Classes.Public:GetChildren() do
		if module.Name:match(".src") then
			continue
		end

		classes[module.Name] = require(module)
	end

	upsideEngine = require(upsideEngine.UpsideEngine)
	networking = upsideEngine.GetService("NetworkingService")
end)

--[[={
	@desc This class is used for the client replication
	@about
		@Content The content of the request
		@ClientId The UserId of the client which sent the request if exists
	@events
		@Build Params -> [BaseObject](BaseObject.md)
		Fired when a request is accepted and the object to be replicated is being built
}=]]

return setmetatable(request, eventEmitter)


local upsideEngine = script.Parent.Parent.Parent
local shadingSystem = script.Parent.Parent.Parent.Systems.Shading
local runService = game:GetService("RunService")
local actor = require(script.Parent.Actor)
local environment = require(upsideEngine.Classes.Public.Environment)

local shaderEnvironment = {}
shaderEnvironment.__index = shaderEnvironment
shaderEnvironment.__iter = environment.__iter

function shaderEnvironment.new(): ObjectEnvironment
	local self = environment.new()
	self:SetClassName(script.Name)

	if runService:IsRunning() and runService:IsClient() and not runService:IsRunMode() then
		self.__actor = actor.new(shadingSystem)
		self.__actor:SetRunMode("ByTasks")
		self.__actor:SetVariable("shadable", {})
		self.__actor:ExecuteEveryStep("draw")
	end

	return setmetatable(self, shaderEnvironment)
end

return setmetatable(shaderEnvironment, environment)


local upsideEngine = script.Parent.Parent.Parent
local environment = require(upsideEngine.Classes.Public.Environment)
local soundEnvironment = {}
soundEnvironment.__index = soundEnvironment

--[[={
	@link SoundEnvironment.md
}=]]

function soundEnvironment.new(): SoundEnvironment
	local self = environment.new("SoundGroup")
	self.Instance.Parent = game.SoundService
	self:SetClassName(script.Name)

	return setmetatable(self, soundEnvironment)
end

return setmetatable(soundEnvironment, environment)


local upsideEngine = script.Parent.Parent.Parent
local sprite = require(upsideEngine.Classes.Public.Sprite)
local character = {}
character.__index = character

local function check(greater, axePos, targetAxe)
	return if greater then axePos >= targetAxe else axePos <= targetAxe
end

local function getAxeData(self, target, axe)
	local absPos = self.Instance.Position
	absPos = Vector2.new(absPos.X.Offset, absPos.Y.Offset)

	local greater = target[axe] > absPos[axe]
	local direction = greater and 1 or -1
	local isX = axe == "X"

	local speed = direction * self.WalkSpeed * 10
	local velocity = Vector2.new(isX and speed, not isX and speed)
	self.Velocity += velocity

	return {
		active = true,
		greater = greater,
		velocity = velocity,
		name = axe,
	}
end

local function moveAxe(self, axe, target)
	local instance = self.Instance
	local velocity = self.Velocity

	local name = axe.name
	local axeVal = instance.Position
	axeVal = axeVal[name].Offset

	local isEnabled = check(axe.greater, target, axeVal)
	if isEnabled then
		isEnabled = self.Velocity[name] == velocity[name] and axe.active
	end

	if not isEnabled and axe.active then
		self.Velocity -= axe.velocity
		axe.active = false
	end

	return isEnabled
end

local function moveTo(self, target)
	local enabled = true
	self.Velocity = Vector2.zero

	local x = getAxeData(self, target, "X")
	local y = getAxeData(self, target, "Y")

	while enabled do
		local xActive = moveAxe(self, x, target.X)
		local yActive = moveAxe(self, y, target.Y)
		enabled = xActive or yActive
		task.wait()
	end
end

function character.new(): Character
	local self = sprite.new()
	self.Anchored = false
	self.Health = 100
	self.MaxHealth = 100
	self.WalkSpeed = 150
	self.JumpPower = 150
	self.IsJumping = false

	self:SetClassName(script.Name)
	return setmetatable(self, character)
end

--[[={
	@desc Updates the amount of health of the character
	@link void.link
}=]]

function character:UpdateHealth(Health: number)
	assert(typeof(Health) == "number", ("Number expected got %s"):format(typeof(Health)))
	assert(Health <= self.MaxHealth, "The health cannot exceed maximum health")

	local oldHealth = self.Health
	self.Health = Health

	if oldHealth > 0 and Health <= 0 then
		self:Fire("Died")
	else
		self:Fire("HealthChanged")
	end
end

--[[={
	@desc The character jumps with the provided jump power, if none is provided it will use the JumpPower property as value
	@link void.link
}=]]

local JUMP_SCALE = 10

function character:Jump(jumpPower: number?)
	if not self.IsGrounded then
		return
	end

	jumpPower = jumpPower or self.JumpPower
	self.Velocity -= Vector2.new(0, jumpPower * JUMP_SCALE)
	self.IsGrounded = false
	self.IsJumping = true

	task.defer(function()
		self:On("Collision"):Wait()
		self.IsJumping = false
	end)
end

--[[={
	@desc The character walks directly to the provided position
	@link void.link
}=]]

function character:MoveTo(target: Vector2)
	coroutine.wrap(moveTo)(self, target)
end

--[[={
	@desc This class is used for the player character and for npcs
	@about
		@Health The amount of health of the character
		@MaxHealth The maximum amount of health of the character
		@WalkSpeed The walk speed of the character
		@JumpPower The jump power of the character
		@IsJumping Set to true when the character is jumping
}=]]

return setmetatable(character, sprite)


local upsideEngine = script.Parent.Parent.Parent
local baseObject = require(upsideEngine.Classes.Internal.BaseObject)

local function setProperty(obj, p, v)
	obj[p] = v
end

local environment = {}
environment.__index = environment

function environment.new(instanceType: string?): Environment
	local self = baseObject.new(instanceType or "Folder")
	self:SetClassName(script.Name)
	self.Content = {}
	self.Count = 0

	return setmetatable(self, environment)
end

function environment:__iter()
	return next, self.Content
end

--[[={
	@desc Gets the object with the specified index
	@link any.link
}=]]

function environment:Get(index: string): any
	return self.Content[index]
end

--[[={
	@desc Adds an object with the specified index, if no parameter is specified it will use the next number of the count property as index
	@link any.link
}=]]

function environment:SetOne(value: any, index: string)
	if not self.Content[index] and value then
		self.Count += 1
	elseif self.Content[index] and not value then
		self.Count -= 1
	end

	self.Content[index] = value
end

--[[={
	@desc Adds an object with the specified index, if no parameter is specified it will use the next number of the count property as index
	@link any.link
}=]]

function environment:AddOne(value: any, index: string)
	assert(self.Content[index] == nil, "This value already exist!")

	self.Count += 1
	self.Content[index or self.Count] = value
end

--[[={
	@desc Removes the object with the specified index
	@link void.link
}=]]

function environment:RemoveOne(index: string)
	assert(self.Content[index], "This value does not exist!")
	self.Content[index or self.Count] = nil
	self.Count -= 1
end

--[[={
	@desc Checks if the environment has a value with the specified index
	@link boolean.link
}=]]

function environment:HasOne(index: string): boolean
	return self.Content[index] ~= nil
end

--[[={
	@desc Checks if the environment contains every specified index and returns a boolean and a dictionary with boolean values, example:
	```lua
	local hasAll, dictionary = treeEnv:Has({
		"Tree1",
		"Tree2",
		"Tree4"
	})

	print(hasAll, dictionary) -- output: false, { Tree1 = true, Tree2 = true, Tree4 = false }
	```
	@tsreturns LuaTuple<[boolean, Dictionary<any, boolean>]>
	@link boolean.link
}=]]

function environment:Has(objects: Array<string>): (boolean, Dictionary<string, boolean>)
	local hasAll = true
	local result = {}

	for _, index in objects do
		local has = self:HasOne(index)
		result[index] = has

		if has == false then
			hasAll = false
		end
	end

	return hasAll, result
end

--[[={
	@desc Adds objects with the specified index
	```lua
	treeEnv:Add({
		"Tree1" = tree.new(),
		"Tree2" = tree.new(),
		"Tree3" = tree.new()
	})

	print(treeEnv:Get("Tree2")) -- output: Tree2
	```
	@link void.link
}=]]

function environment:Add(objects: Dictionary<string, any>)
	for index, object in objects do
		self:AddOne(object, index)
	end
end

--[[={
	@desc Removes the objects with the specified index, example:
	```lua
	treeEnv:Remove({ "Tree1", "Tree2", "Tree3" })
	print(treeEnv:Get("Tree2")) -- output: nil
	```
	@link void.link
}=]]

function environment:Remove(objects: Array<string>)
	for _, index in objects do
		self:RemoveOne(index)
	end
end

--[[={
	@desc Updates the objects with the specified index, example:
	```lua
	treeEnv:Update({
		Tree1 = treeEnv:Get("Tree2"),
		Tree2 = treeEnv:Get("Tree1"),
	})
	```
	@link void.link
}=]]

function environment:Update(objects: Dictionary<string, any>)
	for index, object in objects do
		self.Content[index] = object
	end
end

--[[={
	@desc Finds an object by his name
	@link any.link
}=]]

function environment:FindByName(name: string): any
	for _, object in self.Content do
		if object.Name ~= name then
			continue
		end

		return object
	end

	return
end

--[[={
	@desc Sets the specified property in every object in the environment
	@link void.link
}=]]

function environment:SetProperty(property: string, value: any)
	for _, object in self.Content do
		pcall(setProperty, object, property, value)
	end
end

--[[={
	@desc Executes the specified methods in every object in the environment with the specified parameters
	@link void.link
}=]]

function environment:Run(method: string, ...)
	for _, object in self.Content do
		if typeof(object) == "table" then
			coroutine.wrap(pcall)(object[method] or warn, object, ...)
		end
	end
end

--[[={
	@desc This class is used to storage objects and interact with them in an easier way
	@about
		@Count The amount of objects in this environment
}=]]

return setmetatable(environment, baseObject)


local physicalObject = require(script.Parent.PhysicalObject)

local fluid = {}
fluid.__index = fluid

function fluid.new()
	local self = physicalObject.new()
	self:SetClassName(script.Name)
	self.Density = 2
	self.Viscosity = 5
	self.WavesAmplitude = 1
	self.WavesSpeed = 0.1

	return setmetatable(self, fluid)
end

--[[={
	@desc This class is used to simulate fluids
	@about
		@Density Indicates how much dense the fluid should be. Higher values indicate a denser fluid,
        affecting buoyancy and movement of objects within the fluid
		@Viscosity Determines the flow resistance of the fluid. Higher values indicate a thicker fluid,
        affecting the speed at which objects can move through it
		@WavesAmplitude Specifies the magnitude of the fluid waves. These waves are not visible but influence buoyancy and object stability.
		@WavesSpeed Determines how fast the waves should move
}=]]

return setmetatable(fluid, physicalObject)


local staticObject = require(script.Parent.StaticObject)
local light = {}
light.__index = light

--[[={
	@link Light.md
}=]]

function light.new(): Light
	local self = staticObject.new()
	self:SetClassName(script.Name)
	self.Shape = "PointLight"
	self.Rotation = 0
	self.Angle = 90
	self.Color = Color3.fromRGB(255, 255, 255)
	self.Range = 50
	self.Brightness = 0
	self.Inverted = false

	return setmetatable(self, light)
end

--[[={
	@desc This class is used to illuminate areas in the darkness
	@about
		@TrackCollisions Its a property of PhysicalObject, in this class is marked as readonly and is set false to optimizate
		@Range Is the range of the light
		@Shape Is how the light should be shown, there are two modes "PointLight" and "SpotLight"
		@Brightness Is the brightness of the light
		@Color The color you want the light to have
		@Rotation
		!!!warning
			Only works on Spotlights

		Determines how many degrees the Light is rotated
		@Angle
		!!!warning
			Only works on Spotlights

		Determines the light area size
		@Inverted
		!!!warning
			Only works on Pointlights

		Inverts the light source
		@Track If this property is set to true the light is tracked, so it appears on the screen, if it is disabled and it was tracked at some point,
		it will appear on the screen but if at any time another light enters in the range of this light, this light will start to modify the shape of the initial light, that's because we can say that when we disable the
		the shape of the initial light, that is because it could be said that when we disable the Track property what remains on the screen is the trace of the light.
}=]]

return setmetatable(light, staticObject)


local baseObject = require(script.Parent.Parent.Internal.BaseObject)
local toUDim2 = require(script.Parent.Parent.Parent.Lib.Util.DataType.ToUDim2)
local parallax = {}
parallax.__index = parallax

function parallax.new()
	local self = baseObject.new("Frame")
	self:SetClassName(script.Name)
	self.CanvasSize = Vector2.new(1, 1)
	self.Offset = Vector2.zero
	self.Track = true
	self.LockToCamera = false

	local instance = self.Instance
	instance.BackgroundTransparency = 1
	instance.ClipsDescendants = true
	instance.AnchorPoint = Vector2.new(0.5, 0.5)

	local content = Instance.new("ImageLabel")
	content.Name = "Content"
	content.BackgroundTransparency = 1
	content.AnchorPoint = Vector2.new(0.5, 0.5)
	content.Position = UDim2.fromScale(0.5, 0.5)
	content.Size = UDim2.fromScale(3, 3)
	content.TileSize = UDim2.fromScale(1, 1)
	content.ScaleType = Enum.ScaleType.Tile
	content.ResampleMode = Enum.ResamplerMode.Pixelated
	content.Parent = self.Instance

	return setmetatable(self, parallax)
end

--[[={
	@desc Sets the tile texture
	@link void.link
}=]]

function parallax:SetTexture(texture: string)
	self.Instance.Content.Image = texture
end

--[[={
	@desc Updates the parallax properties (position, canvasSize, etc.), its automatically called when Track is enabled
	@link void.link
}=]]

function parallax:UpdateTiles()
	local instance = self.Instance :: ImageLabel
	local content = instance.Content :: ImageLabel
	local absoluteSize = instance.AbsoluteSize

	local canvasSize = absoluteSize * self.CanvasSize
	local position = self.Offset / absoluteSize
	local finalPosition = {
		X = 0,
		Y = 0,
	} :: any

	for axe in finalPosition do
		local value = tostring(position[axe])
		value = value:split(".")[2] or "0"
		value = tonumber("0." .. value)

		finalPosition[axe] = position[axe] < 0 and value or -value
	end

	finalPosition = Vector2.new(finalPosition.X, finalPosition.Y)
	content.TileSize = toUDim2(canvasSize)
	content.Position = toUDim2(finalPosition * absoluteSize) + UDim2.fromScale(0.5, 0.5)
end

--[[={
	@desc This class is used to create inmersive backgrounds
	@about
		@CanvasSize Defines the image zoom, if the value is 1X, 1Y it will see the whole image as expected, if you set it to 2X, 2Y you only will be able see half of the image
		@Offset Defines the scroll position of the parallax object
		@Track Defines if Update method should be called automatically
		@LockToCamera Locks the Parallax object to the camera position
}=]]

return setmetatable(parallax, baseObject)


local tweenService = game:GetService("TweenService")
local upsideEngine = script.Parent.Parent.Parent
local sceneManager = require(upsideEngine.Services).SceneManager

local baseObject = require(upsideEngine.Classes.Internal.BaseObject)
local character = require(script.Parent.Character)
local staticObject = require(script.Parent.StaticObject)

local particle = {}
particle.__index = particle

function particle.new(): Particle
	local self = baseObject.new()
	self:SetClassName(script.Name)

	self.Angle = Vector2.new(90, 90)
	self.Info = TweenInfo.new(0.5)
	self.Enabled = true

	self.Rotation = 0
	self.MaxRate = 100
	self.Units = 0
	self.Range = 5
	self.LifeTime = 1
	self.Clock = 0
	self.Rate = 55

	self.Subject = character.new()
	self.Properties = {
		Image = "http://www.roblox.com/asset/?id=6904405933",
	}
	self.Goals = {
		Size = UDim2.fromScale(0.05, 0.1),
		ImageTransparency = 1,
	}

	return setmetatable(self, particle)
end

--[[={
    @desc Sets the subject property
}=]]
function particle:SetSubject(subject: Character)
	assert(subject ~= nil, "Subject expected")
	local newScene = sceneManager:Get(subject.Scene)
	assert(newScene ~= nil, "The subject must be in a scene!")

	local id = (self.Subject and self.Subject.Scene) or self.Scene
	local oldScene = sceneManager:Get(id)
	if self.Subject and oldScene then
		oldScene.ParticleEnvironment:RemoveOne(self.Id)
	end

	self.Subject = subject

	if not newScene.ParticleEnvironment:HasOne(self.Id) then
		newScene.ParticleEnvironment:AddOne(self, self.Id)
	end
end

--[[={
    @desc Sets the angle property of the particle
}=]]
function particle:SetAngle(Angle: Vector2)
	assert(Angle and Angle.X > 0 and Angle.Y > 0, "The angle components must be greater than 0!")
	self.Angle = Angle
end

--[[={
    @desc Sets the maximum amount of particles that can exist at the same time
}=]]
function particle:SetMaxRate(maxRate: number)
	assert(type(maxRate) == "number" and maxRate > 0, "The maxRate must be greater than 0!")
	assert(maxRate <= 1500, "The maxRate limit is 1500!")
	self.MaxRate = maxRate
end

function particle:SetRotation(rotation: number)
	assert(type(rotation) == "number", "Rotation must be a number (degrees)")
	self.Rotation = rotation
end

--[[={
    @desc Emits the specified amount of particles, if none is provided then will use as amount the Rate property
    @link void.link
}=]]
function particle:Emit(rate: number?)
	if not self.Enabled then
		return
	end

	local emitCount = rate or self.Rate
	if type(emitCount) ~= "number" or emitCount <= 0 then
		return
	end

	local id = (self.Subject and self.Subject.Scene) or self.Scene
	local scene = sceneManager:Get(id)
	if not scene or not scene.Instance or not scene.Instance.GameFrame then
		return
	end

	local available = math.max(self.MaxRate - self.Units, 0)
	if available <= 0 then
		return
	end

	emitCount = math.min(emitCount, available)
	for _ = 1, emitCount do
		self.Units += 1

		local unit = staticObject.new()
		unit.Instance.ZIndex = self.Instance.ZIndex

		local subjectInstance = (self.Subject and self.Subject.Instance) or unit.Instance
		local instance = unit.Instance
		instance.Parent = scene.Instance.GameFrame
		instance.Position = subjectInstance.Position

		for property, value in self.Properties do
			instance[property] = value
		end

		local halfAngleDeg = (self.Angle and self.Angle.X or 0) / 2

		local rotationOffset = tonumber(self.Rotation) or 0
		local thetaDeg = rotationOffset + ((math.random() * (2 * halfAngleDeg)) - halfAngleDeg)
		local theta = math.rad(thetaDeg)
		local radius = math.sqrt(math.random()) * self.Range

		local dx = math.cos(theta) * radius
		local dy = math.sin(theta) * radius
		if self.Angle and self.Angle.X and self.Angle.X ~= 0 then
			dy = dy * (self.Angle.Y / math.max(self.Angle.X, 1))
		end

		local targetPosition = subjectInstance.Position + UDim2.fromOffset(dx, dy)
		local info = TweenInfo.new(
			self.LifeTime,
			self.Info.EasingStyle,
			self.Info.EasingDirection,
			self.Info.RepeatCount,
			self.Info.Reverses,
			self.Info.DelayTime
		)

		local goalsTween = tweenService:Create(instance, info, self.Goals)
		local positionTween = tweenService:Create(instance, self.Info, {
			Position = targetPosition,
		})

		positionTween:Play()
		goalsTween:Play()

		local conn
		conn = goalsTween.Completed:Connect(function()
			if conn then
				conn:Disconnect()
			end
			pcall(function()
				goalsTween:Destroy()
				positionTween:Destroy()
				if unit and unit.Destroy then
					unit:Destroy()
				end
			end)
			self.Units -= 1
		end)

		task.wait(math.max(self.LifeTime / math.max(self.Rate, 1), 0.001))
	end
end

--[[={
	@desc
		!!! warning
			Currently it's recommended to use sprites to make particles/vfx, this class is in experimental state and can change a lot

		_____
		This class is used for vfx

	@about
		@Angle Depending on the value the particles will be more dispersed
		@Subject The subject which is going to be the center of emission of the particle, a character, sprite, etc...
		@MaxRate Is the maximum amount of particles that can exist at the same time
		@Units Is the amount of particles that are existing at this moment
		@Properties Is a table with the initial properties of the particle which is going to be generated
		@Goals Is a table with the goals of the particles
		@Info Is the tween info of the tween which is going to be used to move the particles
		@Enabled When its enabled new particles can be emitted
		@Range Is the distance that can be traveled by each particle
		@LifeTime The number of seconds the particle will be active before being destroyed
		@Rate Is the amount of particles which is going to be generated
		@Clock Used for internal purposes
		@Rotation Rotation in degrees applied to the emission direction (offset for theta)
}=]]

return setmetatable(particle, baseObject)


local ProximityPrompt2D = require(script.Parent.ProximityPrompt2D)
local Scene = require(script.Parent.Scene)

return function()
	describe("ProximityPrompt2D.new", function()
		it("should create an instance with default properties", function()
			local prompt = ProximityPrompt2D.new()
			expect(prompt.Range).to.equal(100)
			expect(prompt.ActionName).to.equal("Interact")
			expect(prompt.Enabled).to.equal(true)
			expect(typeof(prompt.Fire)).to.equal("function")
		end)
	end)

	describe(":Attach", function()
		it("should set the prompt's Instance.Parent to the object's Instance", function()
			local prompt = ProximityPrompt2D.new()
			local dummyObject = { Instance = Instance.new("Folder") }
			prompt:Attach(dummyObject)
			expect(prompt.Instance.Parent).to.equal(dummyObject.Instance)
		end)
	end)

	describe(":Detach", function()
		it("should set the prompt's Instance.Parent to the scene's Instance", function()
            local scene = Scene.new()
            local prompt = ProximityPrompt2D.new()
            local dummyObject = { Instance = Instance.new("Folder") }
            prompt:SetScene(scene)
            prompt:Attach(dummyObject)
            prompt:Detach()
            expect(prompt.Instance.Parent).to.equal(scene.Instance.GameFrame)
		end)
	end)
end


local UserInputService = game:GetService("UserInputService")
local upsideEngine = script.Parent.Parent.Parent
local CrossPlatformService = require(script.Parent.Parent.Parent.Services.CrossPlatformService)
local SceneManager = require(upsideEngine.Services.SceneManager)
local Sprite = require(upsideEngine.Classes.Public.Sprite)

local proximityPrompt2d = {}
proximityPrompt2d.__index = proximityPrompt2d

function proximityPrompt2d.new(): ProximityPrompt2D
	local self = Sprite.new()
	self:SetClassName(script.Name)
	self.Range = 100
	self.ActionName = "Interact"
	self.Enabled = true
	self.IsClosest = false
	self.InRange = false
	self.ShowIfClosest = true
	self.FadeDuration = 0.1
	self.Label = Instance.new("TextLabel")
	self.HitboxButton = Instance.new("TextButton")
	self.SecondsPerFrame = 0.5
	self.Instance.Name = "ProximityPrompt2D"
	self.LabelPositions = {
		UDim2.new(0.5, 0, 0.45, 0),
		UDim2.new(0.5, 0, 0.45, -20),
	}

	local hitbox = self.HitboxButton
	hitbox.Name = "Hitbox"
	hitbox.Text = ""
	hitbox.BackgroundTransparency = 1
	hitbox.Size = UDim2.new(1, 0, 1, 0)
	hitbox.Position = UDim2.new(0.5, 0, 0.5, 0)
	hitbox.AnchorPoint = Vector2.new(0.5, 0.5)
	hitbox.Parent = self.Instance

	local label = self.Label
	label.Name = "Label"
	label.Text = ""
	label.BackgroundTransparency = 1
	label.Size = UDim2.new(0.4, 0, 0.4, 0)
	label.Position = UDim2.new(0.5, 0, 0.45, 0)
	label.AnchorPoint = Vector2.new(0.5, 0.5)
	label.TextScaled = true
	label.Font = Enum.Font.Arcade
	label.TextColor3 = Color3.fromRGB(255, 255, 255)
	label.Parent = self.Instance

	self.HitboxButton.Activated:Connect(function()
		self:Fire("Triggered")
	end)

	self.HitboxButton.MouseButton1Up:Connect(function()
		self:Fire("TriggerEnded")
	end)

	self:On("FrameChanging", function(currentFrame)
		local labelPositions = self.LabelPositions
		local actionName = self.ActionName
		local labelPosition = labelPositions[currentFrame.X + 1] or Vector2.new(0, 0)
		label.Position = labelPosition
		label.Text = CrossPlatformService:GetTargetActionKey(actionName) or ""
		hitbox.Visible = UserInputService.TouchEnabled
	end)

	local instance = self.Instance
	instance.ImageRectSize = Vector2.new(19, 21)
	instance.Size = UDim2.new(0, 100, 0, 100)
	self:SetSpriteSheet("idle", "http://www.roblox.com/asset/?id=128407262269576", Vector2.new(2, 0))

	return setmetatable(self, proximityPrompt2d)
end

function proximityPrompt2d:Attach(object: StaticObject)
	self.Instance.Parent = object.Instance
end

function proximityPrompt2d:Detach()
	local scene = SceneManager:Get(self.Scene)
	if scene then
		self.Instance.Parent = scene.Instance.GameFrame
	end
end

--[[={
	@desc The proximity prompt is a UI element that will show a prompt when the player is close to it, this is used to interact with objects in the game, it will show a label and a hitbox that will be used to detect when the player is close to it
	@about
		@Label The text label of the prompt, this will be shown when the prompt is active
        @Range The range of the prompt, this will be used to detect when the prompt should be shown
        @Enabled If the prompt is enabled or not, if it is not enabled the prompt will not be shown
		@InRange True if the proximity prompt is visible on the player's screen. Useful for detecting when the prompt is within interaction range
        @IsClosest True if the proximity prompt is the closest one to the player
		@FadeDuration The duration of the fade in and out of the prompt, this will be used to make the prompt appear and disappear smoothly
		@LabelPositions A table containing the positions(as UDim2) of the label, this will be used to adjust the text to the proximity prompt sprites, position 0 will be the position on the first frame, position 1 will be the position on the second frame, etc.
		@HitboxButton The button that will be used to trigger the proximity prompt on mobile devices
		@ActionName The action name of the prompt, this will be used to detect when the
        prompt is triggered, if you change the default action name you should change or
        add a new action in the CrossPlatformService, 'Interact' by default
		@ShowIfClosest Determines whether this proximity prompt should only be shown when it is the closest one to the player.
		If set to true, the prompt will only appear if it is the nearest among all prompts within interaction range.
		If set to false, the prompt will be shown as long as it is within range, regardless of other nearby prompts.
		The property is enabled by default
	@events
		@Triggered Fired when the proximity prompt is triggered. This occurs when the player begins interacting with the prompt.
		@TriggerEnded Fired when the proximity prompt interaction ends. This occurs when the player releases the interaction.
}=]]

return setmetatable(proximityPrompt2d, Sprite)


local SoundService = game:GetService("SoundService")
local upsideEngine = script.Parent.Parent.Parent
local TextTagService = require(upsideEngine.Services.TextTagService)
local TextTag = require(script.Parent.TextTag)
local baseObject = require(upsideEngine.Classes.Internal.BaseObject)

local reactiveLabel = {}
reactiveLabel.__index = reactiveLabel

local defaultTypingSoundId = TextTag.new().TypingSoundId
local typingSound = Instance.new("Sound")
typingSound.Parent = SoundService

local function createUIListLayout(self: Frame, horizontalAlignment, verticalAlignment)
	local uiListLayout = Instance.new("UIListLayout")
	uiListLayout.Parent = self
	uiListLayout.HorizontalAlignment = horizontalAlignment
	uiListLayout.VerticalAlignment = verticalAlignment
	uiListLayout.FillDirection = Enum.FillDirection.Horizontal
	uiListLayout.Wraps = true
	uiListLayout.Padding = UDim.new(0, 0)
	uiListLayout.SortOrder = Enum.SortOrder.LayoutOrder
end

local function trim(s: string): string | nil
	return s:match("^%s*(.-)%s*$")
end

local function parseTags(s: string, currentTags: { any }?): { [any]: any }
	currentTags = currentTags or {}
	local results = {}
	local i = 1

	while i <= #s do
		local j, k, tag, argsStr = s:find("<(%w+)(.-)>", i)
		if j then
			local preText = s:sub(i, j - 1)
			local trimmed = trim(preText)
			if trimmed ~= "" then
				table.insert(results, { text = trimmed, tags = currentTags })
			end

			local closeTag = "</" .. tag .. ">"
			local closeStart, closeEnd = s:find(closeTag, k + 1)
			if not closeStart then
				break
			end

			local argTable = {}
			if argsStr and argsStr ~= "" then
				for key, value in argsStr:gmatch("(%w+)%s*=%s*[\"'](.-)[\"']") do
					argTable[key] = value
				end

				for key, value in argsStr:gmatch("(%w+)%s*=%s*([%d%.%-]+)") do
					if not argTable[key] then
						argTable[key] = tonumber(value) or value
					end
				end
			end

			local inner = s:sub(k + 1, closeStart - 1)
			local newTags = currentTags and { table.unpack(currentTags) } or {}
			table.insert(newTags, { name = tag, args = argTable })

			local innerResults = parseTags(inner, newTags)
			for _, elem in innerResults do
				table.insert(results, elem)
			end

			i = closeEnd + 1
		else
			local remainder = s:sub(i)
			local trimmed = trim(remainder)
			if trimmed ~= "" then
				table.insert(results, { text = trimmed, tags = currentTags })
			end
			break
		end
	end

	return results
end

local function validateTags(s: string): boolean
	local stack = {}
	for slash, tagName in s:gmatch("<(/?)(%w+)(.-)>") do
		if slash == "" then
			table.insert(stack, tagName)
		else
			local last = table.remove(stack)
			if last ~= tagName then
				return false
			end
		end
	end
	return #stack == 0
end

function reactiveLabel.new(): Sound
	local self = baseObject.new("Frame")
	self:SetClassName(script.Name)
	self.Font = Enum.Font.Arial
	self.FontSize = 32
	self.Text = ""
	self.HorizontalAlignment = Enum.HorizontalAlignment.Left
	self.VerticalAlignment = Enum.VerticalAlignment.Top
	self.TextTransparency = 0
	self.TextColor3 = Color3.new(1, 1, 1)
	self.TextScaled = false
	self.Animated = true
	self.TextStrokeColor3 = Color3.new(0, 0, 0)
	self.TextStrokeTransparency = 1
	self.TextStrokeThickness = 0
	self.CharactersPerSecond = 20
	self.TypingSoundId = "rbxassetid://92143495194538"
	self.TypingSoundVolume = 1
	self.StopSoundOnFinish = true
	self.PauseDelays = {
		[","] = 0.25,
		["."] = 0.5,
		["?"] = 0.5,
	}
	self.Instance.Size = UDim2.new(0, 400, 0, 400)
	self.Instance.BackgroundTransparency = 1

	createUIListLayout(self.Instance, self.HorizontalAlignment, self.VerticalAlignment)
	return setmetatable(self, reactiveLabel)
end

--[[={
	@desc Renders the label text with a typewriter effect, yielding during animations.
	@link void.link
}=]]

function reactiveLabel:Render()
	if not validateTags(self.Text) then
		error("Invalid tag syntax in text")
	end

	self.Instance:ClearAllChildren()
	createUIListLayout(self.Instance, self.HorizontalAlignment, self.VerticalAlignment)

	local elements = parseTags(self.Text, {})

	local allWords = {}
	local charactersDelayGlobal = 1 / math.max(1, self.CharactersPerSecond)

	for _, data in elements do
		for word in data.text:gmatch("%S+") do
			local wordFrame = Instance.new("Frame")
			wordFrame.BackgroundTransparency = 1
			wordFrame.Size = UDim2.new(0, 0, 0, self.FontSize)
			wordFrame.AutomaticSize = Enum.AutomaticSize.X
			wordFrame.Parent = self.Instance
			createUIListLayout(wordFrame, self.HorizontalAlignment, self.VerticalAlignment)

			local letters = {}
			local displayMode = "letter"
			local typingSoundId = self.TypingSoundId
			local typingSoundVolume = self.TypingSoundVolume
			local wordsPerSecond = 0

			local localWordsPerSecond = 0
			local useLocalCharsPerSecond = false

			for _, tag in data.tags do
				local args = tag.args or {}
				local textTag = TextTagService.TextTags[tag.name] or {}
				typingSoundId = if textTag.TypingSoundId ~= defaultTypingSoundId and textTag.TypingSoundId
					then textTag.TypingSoundId
					else typingSoundId
				typingSoundVolume = textTag.TypingSoundVolume or typingSoundVolume
				wordsPerSecond = if textTag.WordsPerSecond and textTag.WordsPerSecond > 0
					then textTag.WordsPerSecond
					else wordsPerSecond

				if args.wordsPerSecond and args.wordsPerSecond > localWordsPerSecond then
					localWordsPerSecond = args.wordsPerSecond
					useLocalCharsPerSecond = true
				end

				if textTag.displayMode == "word" then
					displayMode = "word"
				end
			end

			if useLocalCharsPerSecond then
				wordsPerSecond = localWordsPerSecond
			end

			for i = 1, #word do
				local letter = word:sub(i, i)
				local wrappedLetter = letter
				for _, tag in data.tags do
					local argsStr = ""
					for key, value in (tag.args or {}) do
						argsStr ..= (" %s='%s'"):format(key, tostring(value))
					end

					wrappedLetter = ("<%s%s>%s</%s>"):format(tag.name, argsStr, wrappedLetter, tag.name)
				end

				local letterFrame = Instance.new("Frame")
				letterFrame.BackgroundTransparency = 1
				letterFrame.Size = UDim2.new(0, 0, 0, self.FontSize)
				letterFrame.AutomaticSize = Enum.AutomaticSize.X
				letterFrame.ZIndex = self.Instance.ZIndex
				letterFrame.Parent = wordFrame

				local letterLabel = Instance.new("TextLabel")
				letterLabel.BackgroundTransparency = 1
				letterLabel.Name = letter
				letterLabel.Text = if i == #word then wrappedLetter .. " " else wrappedLetter
				letterLabel.RichText = true
				letterLabel.Font = self.Font
				letterLabel.TextSize = self.FontSize
				letterLabel.TextColor3 = self.TextColor3
				letterLabel.TextTransparency = 1
				letterLabel.TextScaled = self.TextScaled
				letterLabel.AutomaticSize = Enum.AutomaticSize.X
				letterLabel.Size = UDim2.new(0, 0, 0, self.FontSize)
				letterLabel.ZIndex = self.Instance.ZIndex
				letterLabel.Parent = letterFrame

				local stroke = Instance.new("UIStroke")
				stroke.Parent = letterLabel
				stroke.Color = self.TextStrokeColor3
				stroke.Transparency = 1
				stroke.Thickness = self.TextStrokeThickness

				table.insert(letters, {
					label = letterLabel,
					stroke = stroke,
					letter = letter,
					tags = data.tags,
					typingSoundId = typingSoundId,
					typingSoundVolume = typingSoundVolume,
					wordsPerSecond = wordsPerSecond,
				})

				letterFrame.Size = UDim2.fromOffset(0, self.FontSize)
				letterFrame.AutomaticSize = Enum.AutomaticSize.X
			end

			table.insert(allWords, {
				letters = letters,
				displayMode = displayMode,
				wordsPerSecond = wordsPerSecond,
			})
		end

		local maxIdleTime = 0
		for _, tag in data.tags do
			local idle = tonumber(tag.args and tag.args.idleTime) or 0
			if idle > maxIdleTime then
				maxIdleTime = idle
			end
		end
		if maxIdleTime > 0 and #allWords > 0 then
			allWords[#allWords].idleTime = maxIdleTime
		end
	end

	for _, wordObj in allWords do
		local letters = wordObj.letters
		local displayMode = wordObj.displayMode
		local wordsPerSecond = wordObj.wordsPerSecond or 0

		if displayMode == "letter" and self.Animated then
			for i, lettersData in letters do
				for _, tag in lettersData.tags do
					local textTag = TextTagService.TextTags[tag.name]
					local args = tag.args or {}
					if textTag and textTag.renderFunction then
						task.defer(textTag.renderFunction, lettersData.label, i, args)
					end
				end

				lettersData.label.TextTransparency = self.TextTransparency
				lettersData.stroke.Transparency = self.TextStrokeTransparency

				typingSound.SoundId = lettersData.typingSoundId
				typingSound.Volume = lettersData.typingSoundVolume
				typingSound:Play()

				task.wait(charactersDelayGlobal)
				local pauseDelay = self.PauseDelays[lettersData.letter]
				if pauseDelay then
					task.wait(pauseDelay)
				end
			end
		else
			for i, lettersData in letters do
				for _, tag in lettersData.tags do
					local textTag = TextTagService.TextTags[tag.name]
					local args = tag.args or {}
					if textTag and textTag.renderFunction then
						task.defer(textTag.renderFunction, lettersData.label, i, args)
					end
				end
				lettersData.label.TextTransparency = self.TextTransparency
				lettersData.stroke.Transparency = self.TextStrokeTransparency
			end

			if self.Animated and #letters > 0 then
				typingSound.SoundId = letters[1].typingSoundId
				typingSound.Volume = letters[1].typingSoundVolume
				typingSound:Play()
				task.wait(charactersDelayGlobal)
			end
		end

		if wordsPerSecond > 0 and self.Animated then
			task.wait(1 / wordsPerSecond)
		end

		if self.StopSoundOnFinish then
			typingSound:Stop()
		end

		if wordObj.idleTime and wordObj.idleTime > 0 then
			task.wait(wordObj.idleTime)
		end
	end
end

--[[={
	@desc UI element that renders text with a typewriter effect, text animations and supports roblox rich text.
	@about
		@Text The text content to be rendered.
		@Font The font used for displaying text.
		@FontSize The font size of the text.
		@TextColor3 The color of the text.
		@TextScaled Determines if the text scales automatically.
		@Animated If true, renders text progressively like a typewriter.
		@CharactersPerSecond  The number of characters rendered per second.
		@TypingSoundId The sound asset ID played during typing.
		@TypingSoundVolume The volume of the typing sound.
		@StopSoundOnFinish If true, stops the typing sound once rendering is complete.
		@TextStrokeColor3 The stroke (outline) color for the text.
		@TextStrokeTransparency Transparency of the text stroke.
		@TextStrokeThickness Thickness of the text stroke.
		@HorizontalAlignment Controls how child elements are laid out horizontally within the label. Enum type: Enum.HorizontalAlignment
        @VerticalAlignment Controls how child elements are laid out vertically within the label. Enum type: Enum.VerticalAlignment
		@PauseDelays A table defining additional pause durations after certain characters (e.g., comma, period).
		Example of PauseDelays usage:
		```
		{
			[","] = 0.25, -- pause 0.25 seconds after a comma
			["."] = 0.5,  -- pause 0.5 seconds after a period
			["?"] = 0.5,  -- pause 0.5 seconds after a question mark
		}
		```
}=]]

return setmetatable(reactiveLabel, baseObject)


local upsideEngine = script.Parent.Parent.Parent

local objectEnvironment = require(upsideEngine.Classes.Internal.ObjectEnvironment)
local shaderEnvironment = require(upsideEngine.Classes.Internal.ShaderEnvironment)
local camera = require(upsideEngine.Classes.Internal.Camera)
local baseObject = require(upsideEngine.Classes.Internal.BaseObject)

local soundEnvironment = require(upsideEngine.Classes.Internal.SoundEnvironment)
local lightingEnvironment = require(upsideEngine.Classes.Internal.LightingEnvironment)
local particleEnvironment = require(upsideEngine.Classes.Internal.ParticleEnvironment)

local sceneManager = require(upsideEngine.Services).SceneManager
local util = require(upsideEngine.Lib.Util)

local scene = {}
scene.__index = scene

local function intersect(a, b, c, d)
	local r, s = (b - a), (d - c)
	d = r.X * s.Y - r.Y * s.X

	local u = ((c.X - a.X) * r.Y - (c.Y - a.Y) * r.X) / d
	local t = ((c.X - a.X) * s.Y - (c.Y - a.Y) * s.X) / d

	return (0 <= u and u <= 1 and 0 <= t and t <= 1) and a + t * r
end

local function intersectRayCircle(a, b, center, radius)
	local eps = 1e-6
	local d = b - a
	local f = a - center

	local a_coef = d.X * d.X + d.Y * d.Y
	local b_coef = 2 * (f.X * d.X + f.Y * d.Y)
	local c_coef = f.X * f.X + f.Y * f.Y - radius * radius

	if a_coef <= eps then
		return nil
	end

	local disc = b_coef * b_coef - 4 * a_coef * c_coef
	if disc < 0 then
		return nil
	end

	local sqrtDisc = math.sqrt(disc)
	local t1 = (-b_coef - sqrtDisc) / (2 * a_coef)
	local t2 = (-b_coef + sqrtDisc) / (2 * a_coef)

	local t = math.huge
	if t1 >= 0 and t1 <= 1 and t1 < t then
		t = t1
	end
	if t2 >= 0 and t2 <= 1 and t2 < t then
		t = t2
	end

	if t == math.huge then
		return nil
	end

	return a + d * t
end

--[[={
	@link Scene.md
}=]]

function scene.new(): Scene
	local self = baseObject.new("Folder")
	self:SetClassName(script.Name)

	local instance = self.Instance
	local gameFrame = Instance.new("Frame")
	gameFrame.Name = "GameFrame"
	gameFrame.Parent = instance
	gameFrame.BorderSizePixel = 0
	gameFrame.Size = UDim2.fromScale(1, 1)
	gameFrame.Position = UDim2.fromScale(0.5, 0.5)
	gameFrame.AnchorPoint = Vector2.new(0.5, 0.5)
	gameFrame.BackgroundTransparency = 1

	self.Gravity = Vector2.new(0, 9.81 * 4)
	self.OnlyTrackVisible = false
	self.Camera = camera.new(self)
	self.LightingEnvironment = lightingEnvironment.new(self)
	self.ParticleEnvironment = particleEnvironment.new()
	self.SoundEnvironment = soundEnvironment.new()
	self.ShaderEnvironment = shaderEnvironment.new()
	self.Objects = objectEnvironment.new()

	sceneManager.Scenes[self.Id] = self
	self:On("Destroy", function()
		sceneManager.Scenes[self.Id] = nil
		sceneManager.ActiveScenes[self.Id] = nil
	end)

	return setmetatable(self, scene)
end

--[[={
	@desc Enables the scene, which will make it process physics, lights, sounds and sprites
	@link array.link
}=]]

function scene:Enable()
	local instance = self.Instance
	instance.GameFrame.Visible = true
	instance.Lighting.Visible = true

	sceneManager.ActiveScenes[self.Id] = self
	sceneManager:Fire("SceneLoaded", self)
end

--[[={
	@desc Disables the scene
	@link array.link
}=]]

function scene:Disable()
	local instance = self.Instance
	instance.GameFrame.Visible = false
	instance.Lighting.Visible = false

	sceneManager.ActiveScenes[self.Id] = nil
	sceneManager:Fire("SceneUnloaded", self)
end

--[[={
	@desc Gets all the objects with the provided tags in the scene
	@link array.link
}=]]

function scene:GetTaggedObjects(tag: string): Array<string>
	local Taggeds = {}

	for _, object in self.Objects do
		if object:HasTag(tag) then
			table.insert(Taggeds, object)
		end
	end

	return Taggeds
end

--[[={
	@desc Casts a ray using the Raycast2dParams. If it finds an elegible object a Raycast2dResult is returned containing the results of the operations
	@link /documentation/datatypes/Raycast2DResult.html
}=]]

function scene:Raycast(info: Raycast2dParams): Raycast2dResult
	local isBlackList = info.FilterType == "Blacklist"
	local intersections = {}

	local from, to = info.From, info.To
	info.List = info.List or {}

	local blacklistSet
	if isBlackList then
		blacklistSet = {}
		for key, value in info.List do
			if type(key) == "number" then
				blacklistSet[value.Id] = true
			else
				blacklistSet[key] = true
			end
		end
	end

	local targetList = isBlackList and self.Objects or info.List

	from = Vector3.new(from.X, from.Y)
	to = Vector3.new(to.X, to.Y)

	local function pushIntersection(normal, position, object)
		table.insert(intersections, {
			Object = object,
			Position = UDim2.fromOffset(position.X, position.Y),
			Distance = (from - position).Magnitude,
			Normal = UDim2.fromOffset(normal.X, normal.Y),
		})
	end

	for _, object in targetList do
		if isBlackList and blacklistSet[object.Id] or not object:IsA("PhysicalObject") then
			continue
		end

		local shape = object.Shape:lower()
		local instance = object.Instance
		local absPos = instance.AbsolutePosition
		local absSize = instance.AbsoluteSize

		if shape == "circle" then
			local centre = absPos + (absSize / 2)
			local radius = ((absSize.X + absSize.Y) / 2) * object.HitboxScale
			centre = Vector3.new(centre.X, centre.Y, 0)

			local position = intersectRayCircle(from, to, centre, radius)
			if not position then
				continue
			end

			local normal = (position - centre).Unit
			pushIntersection(normal, position, object)
		else
			local corners = util.GetCorners(object)
			local n = #corners

			for i = 1, n do
				local current = corners[i]
				local nextCorner = corners[i % n + 1]
				local position = intersect(current, nextCorner, from, to)

				if not position then
					continue
				end

				local edge = nextCorner - current
				local normal = edge.Unit
				pushIntersection(normal, position, object)
			end
		end
	end

	table.sort(intersections, function(a, b)
		return a.Distance < b.Distance
	end)

	return intersections[1]
end

--[[={
	@desc This class is useful to have a workspace and manage your project more easily
	@about
		@OnlyTrackVisible This property defines whether physics should be calculated only for visible objects
		@Objects This environment stores all the objects in the scene
		@Camera The scene camera
		@Gravity The scene gravity
		@ParticleEnvironment This is the ParticleEnvironment of the scene
		@LightingEnvironment This is the LightingEnvironment of the scene
		@SoundEnvironment This is the SoundEnvironment of the scene
		@ShaderEnvironment This is the ShaderEnvironment of the scene
}=]]

return setmetatable(scene, baseObject)


local baseClass = require(script.Parent.Parent.Internal.BaseObject)
local path = require(script.Parent.Parent.Parent.Lib.Util.Internal.Path)

local source = nil
local shader = {}
shader.__index = shader

function shader.new(): Shader
	local self = baseClass.new("Folder")
	self:SetClassName(script.Name)
	self.Enabled = true
	self.PreloadSprites = false
	self.MirrorEnabled = false
	self.MirrorSource = source
	self.Precision = 0.6
	self.Path = ""

	return setmetatable(self, shader)
end

--[[={
	@desc Sets the mirror source object for rendering the shader. When a mirror source is specified,
	the shader uses its `ImageRectOffset` and `ImageRectSize` to render. All objects to which this shader
	is applied will display the exact same result, mirroring the appearance of the source object.
	If no mirror source is set, each object will be rendered independently,
	which may lead to decreased performance.
	@link void.md
}=]]

function shader:SetMirrorSource(object: StaticObject)
	self.MirrorSource = object.Instance
end

--[[={
	@desc Sets the module script which is going to be used as shader source
	@link void.md
}=]]

function shader:SetSource(file: ModuleScript)
	self.Path = path.getRoute(file)
end

--[[={
	@desc
		!!! warning
			Shaders are very cpu-intensive, so it is not recommended to use them with high resolution images
			and not to use too many shaders.

			If you need to scale an image, do it in the studio, instead of doing it in the editing program,
			this will lighten a lot the work for the cpu.

		_____
		This class is used to modify how an object is renderized
	@about
		@Path This property stores the shader path
        @Enabled This property determines if the shader is enabled
		@PreloadSprites When this property is marked as true every animation of a sprite will be preloaded,
		if not the animations will be loaded dynamically
		@MirrorSource The current mirror source, nil by default ImageLabel if an object is set using SetMirrorSource()
		@MirrorEnabled This property determines if the shader will use the mirror source or not, false by default
        @Precision Sometimes, modifying the position of pixels in an image can create gaps.
		Upside Engine automatically fills these gaps, but this can occasionally
		lead to unwanted effects. Therefore, adjust this property according to your needs,
		the value of this property ranges between 0 and 1.
}=]]

return setmetatable(shader, baseClass)


local upsideEngine = script.Parent.Parent.Parent
local objects = require(upsideEngine.AppData.Data).objects

local character = require(script.Parent.Character)
local baseObject = require(upsideEngine.Classes.Internal.BaseObject)
local sound = {}
sound.__index = sound

local function getSoundEnvironment(scene: string): SoundEnvironment
	return objects[scene].SoundEnvironment
end

local function disconnectSubject(self)
	if self.SubjectDestroyConnection then
		self.SubjectDestroyConnection:Disconnect()
		self.SubjectDestroyConnection = nil
	end
end

local function removeFromSoundEnvironment(self)
	local sceneId = self.Subject and self.Subject.Scene or nil
	if self.Subject and objects[sceneId] then
		local soundEnvironment = getSoundEnvironment(sceneId)
		soundEnvironment.Content[self.Id] = nil
	end
end

local function bindSubject(self, subject)
	disconnectSubject(self)
	self.Subject = subject

	if not subject then
		return
	end

	self.SubjectDestroyConnection = subject:On("Destroy", function()
		if self.Subject ~= subject then
			return
		end

		removeFromSoundEnvironment(self)
		disconnectSubject(self)
		self.Subject = nil
	end)
end

function sound.new(): Sound
	local self = baseObject.new("Sound")
	self:SetClassName(script.Name)
	self.Range = 1000
	self.MaxVolume = 1
	self.DistanceFading = true
	local defaultSubject = character.new()
	self.Subject = defaultSubject
	self.SubjectDestroyConnection = nil
	bindSubject(self, defaultSubject)

	return setmetatable(self, sound)
end

--[[={
	@desc Sets the provided character as subject, if is provided a sound group, this will be the new sound group of the sound
	@link void.link
}=]]

function sound:SetSubject(subject: Character, useSceneSoundGroup: boolean?)
	assert(objects[subject.Scene] ~= nil, "The subject must be in a scene!")

	removeFromSoundEnvironment(self)

	local SoundEnvironment = getSoundEnvironment(subject.Scene)
	SoundEnvironment.Content[self.Id] = self

	bindSubject(self, subject)
	if useSceneSoundGroup then
		self.Instance.SoundGroup = SoundEnvironment.Instance
	end
end

--[[={
	@desc This class is used to play sounds
	@about
		@Subject This table stores all the objects in the scene
		@Range This is the SoundEnvironment of the scene
		@MaxVolume This is the ParticleEnvironment of the scene
		@DistanceFading The volume will depend on the player distance
}=]]

return setmetatable(sound, baseObject)


local contentProvider = game:GetService("ContentProvider")
local physicalObject = require(script.Parent.PhysicalObject)

local sprite = {}
sprite.__index = sprite

--[[={
	@desc The second parameter is the size of a cell of every frame of an sprite sheet, this can be changed using the .ImageRectSize property of the object Instance
	@link Sprite.md
}=]]

function sprite.new(): Sprite
	local self = physicalObject.new()
	self:SetClassName(script.Name)
	self.IsPlaying = false
	self.SecondsPerFrame = 0.05
	self.Sprites = {}
	self.Active = {
		Name = "",
		CurrentFrame = Vector2.zero,
		SecondsPerFrame = 0,
		NextStepReady = true,
	}

	return setmetatable(self, sprite)
end

--[[={
	@desc Adds a new sprite sheet to the object, the sprite will be played using as reference the amount of cells in the X axe and the Y axe, this must be provided in the third paramether as a Vector2
	@link void.link
}=]]

function sprite:SetSpriteSheet(name: string, url: string, frames: Vector2)
	task.defer(contentProvider.PreloadAsync, contentProvider, { url })
	self.Sprites[name] = {
		Url = url,
		Frames = frames - Vector2.one,
	}
end

--[[={
	@desc Adds a new sprite list to the object, it will play the animation using the order in the provided list, this list must contain the urls of every frame
	@link void.link
}=]]

function sprite:SetSpriteList(name: string, list: Array<string>)
	task.defer(contentProvider.PreloadAsync, contentProvider, list)
	self.Sprites[name] = {
		List = list,
	}
end

--[[={
	@desc Plays the specified sprite if exists, if secondsEveryFrame is specified it will be used instead of .SecondsPerFrame
	@link void.link
}=]]

function sprite:Play(name: string, secondsPerFrame: number?)
	self.IsPlaying = true
	self.Active = {
		Name = name,
		SecondsPerFrame = secondsPerFrame or self.SecondsPerFrame,
		NextStepReady = true,
		CurrentFrame = Vector2.zero,
	}
end

--[[={
	@desc Stops the reproduction of the current playing sprite sheet or sprite list
	@link void.link
}=]]

function sprite:Stop()
	self.IsPlaying = false
end

--[[={
	@desc This class is used to play sounds
	@about
		@IsPlaying Indicates if the sprite is playing
		@Sprites This table stores all sprite sheets and sprite lists
		@SecondsPerFrame This is the time to wait between frame and frame
		@Active This table stores information about the active spritesheet
	@events
		@FrameChanging This event is fired before the current frame changes, it will provide the new frame position as a Vector2
}=]]

return setmetatable(sprite, physicalObject)