--!strict

local RunService = game:GetService("RunService")

export type Trove = {
	Extend: (self: Trove) -> Trove,
	Clone: <T>(self: Trove, instance: T & Instance) -> T,
	Construct: <T, A...>(self: Trove, class: Constructable<T, A...>, A...) -> T,
	Connect: (self: Trove, signal: SignalLike | RBXScriptSignal, fn: (...any) -> ...any) -> ConnectionLike,
	BindToRenderStep: (self: Trove, name: string, priority: number, fn: (dt: number) -> ()) -> (),
	AddPromise: <T>(self: Trove, promise: T & PromiseLike) -> T,
	Add: <T>(self: Trove, object: T & Trackable, cleanupMethod: string?) -> T,
	Remove: <T>(self: Trove, object: T & Trackable) -> boolean,
	Clean: (self: Trove) -> (),
	AttachToInstance: (self: Trove, instance: Instance) -> RBXScriptConnection,
	Destroy: (self: Trove) -> (),
}

type TroveInternal = Trove & {
	_objects: { any },
	_cleaning: boolean,
	_findAndRemoveFromObjects: (self: TroveInternal, object: any, cleanup: boolean) -> boolean,
	_cleanupObject: (self: TroveInternal, object: any, cleanupMethod: string?) -> (),
}

--[=[
	@within Trove
	@type Trackable Instance | ConnectionLike | PromiseLike | thread | ((...any) -> ...any) | Destroyable | DestroyableLowercase | Disconnectable | DisconnectableLowercase
	Represents all trackable objects by Trove.
]=]
export type Trackable =
	Instance
	| ConnectionLike
	| PromiseLike
	| thread
	| ((...any) -> ...any)
	| Destroyable
	| DestroyableLowercase
	| Disconnectable
	| DisconnectableLowercase

--[=[
	@within Trove
	@interface ConnectionLike
	.Connected boolean
	.Disconnect (self) -> ()
]=]
type ConnectionLike = {
	Connected: boolean,
	Disconnect: (self: ConnectionLike) -> (),
}

--[=[
	@within Trove
	@interface SignalLike
	.Connect (self, callback: (...any) -> ...any) -> ConnectionLike
	.Once (self, callback: (...any) -> ...any) -> ConnectionLike
]=]
type SignalLike = {
	Connect: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike,
	Once: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike,
}

--[=[
	@within Trove
	@interface PromiseLike
	.getStatus (self) -> string
	.finally (self, callback: (...any) -> ...any) -> PromiseLike
	.cancel (self) -> ()
]=]
type PromiseLike = {
	getStatus: (self: PromiseLike) -> string,
	finally: (self: PromiseLike, callback: (...any) -> ...any) -> PromiseLike,
	cancel: (self: PromiseLike) -> (),
}

--[=[
	@within Trove
	@type Constructable { new: (A...) -> T } | (A...) -> T
]=]
type Constructable<T, A...> = { new: (A...) -> T } | (A...) -> T

--[=[
	@within Trove
	@interface Destroyable
	.disconnect (self) -> ()
]=]
type Destroyable = {
	Destroy: (self: Destroyable) -> (),
}

--[=[
	@within Trove
	@interface DestroyableLowercase
	.disconnect (self) -> ()
]=]
type DestroyableLowercase = {
	destroy: (self: DestroyableLowercase) -> (),
}

--[=[
	@within Trove
	@interface Disconnectable
	.disconnect (self) -> ()
]=]
type Disconnectable = {
	Disconnect: (self: Disconnectable) -> (),
}

--[=[
	@within Trove
	@interface DisconnectableLowercase
	.disconnect (self) -> ()
]=]
type DisconnectableLowercase = {
	disconnect: (self: DisconnectableLowercase) -> (),
}

local FN_MARKER = newproxy()
local THREAD_MARKER = newproxy()
local GENERIC_OBJECT_CLEANUP_METHODS = table.freeze({ "Destroy", "Disconnect", "destroy", "disconnect" })

local function GetObjectCleanupFunction(object: any, cleanupMethod: string?)
	local t = typeof(object)

	if t == "function" then
		return FN_MARKER
	elseif t == "thread" then
		return THREAD_MARKER
	end

	if cleanupMethod then
		return cleanupMethod
	end

	if t == "Instance" then
		return "Destroy"
	elseif t == "RBXScriptConnection" then
		return "Disconnect"
	elseif t == "table" then
		for _, genericCleanupMethod in GENERIC_OBJECT_CLEANUP_METHODS do
			if typeof(object[genericCleanupMethod]) == "function" then
				return genericCleanupMethod
			end
		end
	end

	error(`failed to get cleanup function for object {t}: {object}`, 3)
end

local function AssertPromiseLike(object: any)
	if
		typeof(object) ~= "table"
		or typeof(object.getStatus) ~= "function"
		or typeof(object.finally) ~= "function"
		or typeof(object.cancel) ~= "function"
	then
		error("did not receive a promise as an argument", 3)
	end
end

--[=[
	@class Trove
	@tag Vendor

	A Trove is helpful for tracking any sort of object during
	runtime that needs to get cleaned up at some point.
]=]
local Trove = {}
Trove.__index = Trove

--[=[
	@return Trove
	Constructs a Trove object.

	```lua
	local trove = Trove.new()
	```
]=]
function Trove.new(): Trove
	local self = setmetatable({}, Trove)

	self._objects = {}
	self._cleaning = false

	return (self :: any) :: Trove
end

--[=[
	@method Add
	@within Trove
	@param object any -- Object to track
	@param cleanupMethod string? -- Optional cleanup name override
	@return object: any
	Adds an object to the trove. Once the trove is cleaned or
	destroyed, the object will also be cleaned up.

	The following types are accepted (e.g. `typeof(object)`):

	| Type | Cleanup |
	| ---- | ------- |
	| `Instance` | `object:Destroy()` |
	| `RBXScriptConnection` | `object:Disconnect()` |
	| `function` | `object()` |
	| `thread` | `task.cancel(object)` |
	| `table` | `object:Destroy()` _or_ `object:Disconnect()` _or_ `object:destroy()` _or_ `object:disconnect()` |
	| `table` with `cleanupMethod` | `object:<cleanupMethod>()` |

	Returns the object added.

	```lua
	-- Add a part to the trove, then destroy the trove,
	-- which will also destroy the part:
	local part = Instance.new("Part")
	trove:Add(part)
	trove:Destroy()

	-- Add a function to the trove:
	trove:Add(function()
		print("Cleanup!")
	end)
	trove:Destroy()

	-- Standard cleanup from table:
	local tbl = {}
	function tbl:Destroy()
		print("Cleanup")
	end
	trove:Add(tbl)

	-- Custom cleanup from table:
	local tbl = {}
	function tbl:DoSomething()
		print("Do something on cleanup")
	end
	trove:Add(tbl, "DoSomething")
	```
]=]
function Trove.Add(self: TroveInternal, object: Trackable, cleanupMethod: string?): any
	if self._cleaning then
		error("cannot call trove:Add() while cleaning", 2)
	end

	local cleanup = GetObjectCleanupFunction(object, cleanupMethod)
	table.insert(self._objects, { object, cleanup })

	return object
end

--[=[
	@method Clone
	@within Trove
	@return Instance
	Clones the given instance and adds it to the trove. Shorthand for
	`trove:Add(instance:Clone())`.

	```lua
	local clonedPart = trove:Clone(somePart)
	```
]=]
function Trove.Clone(self: TroveInternal, instance: Instance): Instance
	if self._cleaning then
		error("cannot call trove:Clone() while cleaning", 2)
	end

	return self:Add(instance:Clone())
end

--[=[
	@method Construct
	@within Trove
	@param class { new(Args...) -> T } | (Args...) -> T
	@param ... Args...
	@return T
	Constructs a new object from either the
	table or function given.

	If a table is given, the table's `new`
	function will be called with the given
	arguments.

	If a function is given, the function will
	be called with the given arguments.

	The result from either of the two options
	will be added to the trove.

	This is shorthand for `trove:Add(SomeClass.new(...))`
	and `trove:Add(SomeFunction(...))`.

	```lua
	local Signal = require(somewhere.Signal)

	-- All of these are identical:
	local s = trove:Construct(Signal)
	local s = trove:Construct(Signal.new)
	local s = trove:Construct(function() return Signal.new() end)
	local s = trove:Add(Signal.new())

	-- Even Roblox instances can be created:
	local part = trove:Construct(Instance, "Part")
	```
]=]
function Trove.Construct<T, A...>(self: TroveInternal, class: Constructable<T, A...>, ...: A...)
	if self._cleaning then
		error("Cannot call trove:Construct() while cleaning", 2)
	end

	local object = nil
	local t = type(class)
	if t == "table" then
		object = (class :: any).new(...)
	elseif t == "function" then
		object = (class :: any)(...)
	end

	return self:Add(object)
end

--[=[
	@method Connect
	@within Trove
	@param signal RBXScriptSignal
	@param fn (...: any) -> ()
	@return RBXScriptConnection
	Connects the function to the signal, adds the connection
	to the trove, and then returns the connection.

	This is shorthand for `trove:Add(signal:Connect(fn))`.

	```lua
	trove:Connect(workspace.ChildAdded, function(instance)
		print(instance.Name .. " added to workspace")
	end)
	```
]=]
function Trove.Connect(self: TroveInternal, signal: SignalLike, fn: (...any) -> ...any)
	if self._cleaning then
		error("Cannot call trove:Connect() while cleaning", 2)
	end

	return self:Add(signal:Connect(fn))
end

--[=[
	@method BindToRenderStep
	@within Trove
	@param name string
	@param priority number
	@param fn (dt: number) -> ()
	Calls `RunService:BindToRenderStep` and registers a function in the
	trove that will call `RunService:UnbindFromRenderStep` on cleanup.

	```lua
	trove:BindToRenderStep("Test", Enum.RenderPriority.Last.Value, function(dt)
		-- Do something
	end)
	```
]=]
function Trove.BindToRenderStep(self: TroveInternal, name: string, priority: number, fn: (dt: number) -> ())
	if self._cleaning then
		error("cannot call trove:BindToRenderStep() while cleaning", 2)
	end

	RunService:BindToRenderStep(name, priority, fn)

	self:Add(function()
		RunService:UnbindFromRenderStep(name)
	end)
end

--[=[
	@method AddPromise
	@within Trove
	@param promise Promise
	@return Promise
	Gives the promise to the trove, which will cancel the promise if the trove is cleaned up or if the promise
	is removed. The exact promise is returned, thus allowing chaining.

	```lua
	trove:AddPromise(doSomethingThatReturnsAPromise())
		:andThen(function()
			print("Done")
		end)
	-- Will cancel the above promise (assuming it didn't resolve immediately)
	trove:Clean()

	local p = trove:AddPromise(doSomethingThatReturnsAPromise())
	-- Will also cancel the promise
	trove:Remove(p)
	```

	:::caution Promise v4 Only
	This is only compatible with the [roblox-lua-promise](https://eryn.io/roblox-lua-promise/) library, version 4.
	:::
]=]
function Trove.AddPromise(self: TroveInternal, promise: PromiseLike)
	if self._cleaning then
		error("cannot call trove:AddPromise() while cleaning", 2)
	end
	AssertPromiseLike(promise)

	if promise:getStatus() == "Started" then
		promise:finally(function()
			if self._cleaning then
				return
			end
			self:_findAndRemoveFromObjects(promise, false)
		end)

		self:Add(promise, "cancel")
	end

	return promise
end

--[=[
	@method Remove
	@within Trove
	@param object any
	Removes the object from the Trove and cleans it up.

	```lua
	local part = Instance.new("Part")
	trove:Add(part)
	trove:Remove(part)
	```
]=]
function Trove.Remove(self: TroveInternal, object: Trackable): boolean
	if self._cleaning then
		error("cannot call trove:Remove() while cleaning", 2)
	end

	return self:_findAndRemoveFromObjects(object, true)
end

--[=[
	@method Extend
	@within Trove
	@return Trove
	Creates and adds another trove to itself. This is just shorthand
	for `trove:Construct(Trove)`. This is useful for contexts where
	the trove object is present, but the class itself isn't.

	:::note
	This does _not_ clone the trove. In other words, the objects in the
	trove are not given to the new constructed trove. This is simply to
	construct a new Trove and add it as an object to track.
	:::

	```lua
	local trove = Trove.new()
	local subTrove = trove:Extend()

	trove:Clean() -- Cleans up the subTrove too
	```
]=]
function Trove.Extend(self: TroveInternal)
	if self._cleaning then
		error("cannot call trove:Extend() while cleaning", 2)
	end

	return self:Construct(Trove)
end

--[=[
	@method Clean
	@within Trove
	Cleans up all objects in the trove. This is
	similar to calling `Remove` on each object
	within the trove. The ordering of the objects
	removed is _not_ guaranteed.

	```lua
	trove:Clean()
	```
]=]
function Trove.Clean(self: TroveInternal)
	if self._cleaning then
		return
	end

	self._cleaning = true

	for _, obj in self._objects do
		self:_cleanupObject(obj[1], obj[2])
	end

	table.clear(self._objects)
	self._cleaning = false
end

function Trove._findAndRemoveFromObjects(self: TroveInternal, object: any, cleanup: boolean): boolean
	local objects = self._objects

	for i, obj in ipairs(objects) do
		if obj[1] == object then
			local n = #objects
			objects[i] = objects[n]
			objects[n] = nil

			if cleanup then
				self:_cleanupObject(obj[1], obj[2])
			end

			return true
		end
	end

	return false
end

function Trove._cleanupObject(_self: TroveInternal, object: any, cleanupMethod: string?)
	if cleanupMethod == FN_MARKER then
		object()
	elseif cleanupMethod == THREAD_MARKER then
		pcall(task.cancel, object)
	else
		object[cleanupMethod](object)
	end
end

--[=[
	@method AttachToInstance
	@within Trove
	@param instance Instance
	@return RBXScriptConnection
	Attaches the trove to a Roblox instance. Once this
	instance is removed from the game (parent or ancestor's
	parent set to `nil`), the trove will automatically
	clean up.

	This inverses the ownership of the Trove object, and should
	only be used when necessary. In other words, the attached
	instance dictates when the trove is cleaned up, rather than
	the trove dictating the cleanup of the instance.

	:::caution
	Will throw an error if `instance` is not a descendant
	of the game hierarchy.
	:::

	```lua
	trove:AttachToInstance(somePart)
	trove:Add(function()
		print("Cleaned")
	end)

	-- Destroying the part will cause the trove to clean up, thus "Cleaned" printed:
	somePart:Destroy()
	```
]=]
function Trove.AttachToInstance(self: TroveInternal, instance: Instance)
	if self._cleaning then
		error("cannot call trove:AttachToInstance() while cleaning", 2)
	elseif not instance:IsDescendantOf(game) then
		error("instance is not a descendant of the game hierarchy", 2)
	end

	return self:Connect(instance.Destroying, function()
		self:Destroy()
	end)
end

--[=[
	@method Destroy
	@within Trove
	Alias for `trove:Clean()`.

	```lua
	trove:Destroy()
	```
]=]
function Trove.Destroy(self: TroveInternal)
	self:Clean()
end

return {
	new = Trove.new,
}


local RunService = game:GetService("RunService")

local Client = require(script.Client)
local Server = require(script.Server)

if RunService:IsClient() then -- No Server object on the client
    script.Server:Destroy()
    Server = nil
else
    Client = nil
end

return setmetatable({
    Client = Client,
    Server = Server,
}, {
    __index = function(_, key)
        if key == "Server" then
            error("Cannot access RoQuest.Server on the client! Make sure to require(RoQuest).Client instead!")
        elseif key == "Client" then
            error("Cannot access RoQuest.Client on the server! Make sure to require(RoQuest).Server instead!")
        else
            error("RoQuest has no member named '" .. tostring(key) .. "'")
        end
    end
})


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Client

type QuestLifeCycle = RoQuest.QuestLifeCycle

local QuestLifeCycle = RoQuest.QuestLifeCycle

local AppleQuest = QuestLifeCycle {
    Name = "AppleQuest",
}

function AppleQuest:OnStart()
   -- print("Apple On Start")
end

function AppleQuest:OnComplete()
   -- print("Apple On Complete")
end

function AppleQuest:OnObjectiveChange(...)
   -- print(...)
end

function AppleQuest:OnDeliver()
   -- print("Apple On Deliver")
end

function AppleQuest:OnDestroy()
    --print("Apple On Destroy")
end

return AppleQuest


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server

type QuestLifeCycle = RoQuest.QuestLifeCycle

local QuestLifeCycle = RoQuest.QuestLifeCycle

local AppleQuest = QuestLifeCycle {
    Name = "AppleQuest",
}

function AppleQuest:OnStart()
    --print("Apple On Start")
end

function AppleQuest:OnComplete()
    --print("Apple On Complete")
end

function AppleQuest:OnObjectiveChange(...)
    --print(...)
end

function AppleQuest:OnDeliver()
    --print("Apple On Deliver")
end

function AppleQuest:OnDestroy()
    --print("Apple On Destroy")
end

return AppleQuest


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server

type QuestLifeCycle = RoQuest.QuestLifeCycle

local QuestLifeCycle = RoQuest.QuestLifeCycle

local CornQuest: QuestLifeCycle = QuestLifeCycle {
    Name = "CornQuest",
}

return CornQuest


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server

local ObjectiveInfo = RoQuest.ObjectiveInfo

return ObjectiveInfo.new {
    Description = "%s/%s apples collected",
    Name = "Collect Apples",
    ObjectiveId = "Apple",
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server

local ObjectiveInfo = RoQuest.ObjectiveInfo

return ObjectiveInfo.new {
    Description = "%s/%s corn collected",
    Name = "Collect Corn",
    ObjectiveId = "Corn",
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server

local ObjectiveInfo = RoQuest.ObjectiveInfo

return ObjectiveInfo.new {
    Description = "%s/%s flowers collected",
    Name = "Collect Flowers",
    ObjectiveId = "Flower",
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server

local ObjectiveInfo = RoQuest.ObjectiveInfo

return ObjectiveInfo.new {
    Description = "%s/%s marbles collected",
    Name = "Collect Marbles",
    ObjectiveId = "Marble",
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server

local ObjectiveInfo = RoQuest.ObjectiveInfo

return ObjectiveInfo.new {
    Description = "%s/%s zombies killed",
    Name = "Kill Zombies",
    ObjectiveId = "KillZombie",
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server
local appleObjective = require(ReplicatedStorage.ObjectiveInfos.Apple)

local Quest = RoQuest.Quest

return Quest {
    Name = "Collect Apples", -- The name of our quest
    Description = "Collect 2 apples", -- The description that we will display to our user
    QuestId = "AppleCollection", -- A unique identifier to our quest
    QuestAcceptType = RoQuest.QuestAcceptType.Automatic, -- If the quest automatically gets accepted or rquires manual work
    QuestDeliverType = RoQuest.QuestDeliverType.Automatic, -- If the quest automatically gets delivered or requires manual work
    QuestRepeatableType = RoQuest.QuestRepeatableType.NonRepeatable, -- If the quest can be repeated or not
    QuestStart = -1, -- UTC time to define when the quest should become available (specially useful for event quests)
    QuestEnd = -1, -- UTC time to define when the quest should no longer be available (specially useful for event quests)
    RequiredQuests = {}, -- A list of quests that are required to be delivered before this quest can be started
    LifeCycles = {"AppleQuest"}, -- The lifecycles that will manage this quest's behavior
    QuestObjectives = {
        appleObjective:NewObjective(2)
    },
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server
local appleObjective = require(ReplicatedStorage.ObjectiveInfos.Apple)

local Quest = RoQuest.Quest

return Quest {
    Name = "Collect Apples", -- The name of our quest
    Description = "Collect 3 apples", -- The description that we will display to our user
    QuestId = "AppleCollection2", -- A unique identifier to our quest
    QuestAcceptType = RoQuest.QuestAcceptType.Automatic, -- If the quest automatically gets accepted or rquires manual work
    QuestDeliverType = RoQuest.QuestDeliverType.Automatic, -- If the quest automatically gets delivered or requires manual work
    QuestRepeatableType = RoQuest.QuestRepeatableType.NonRepeatable, -- If the quest can be repeated or not
    QuestStart = -1, -- UTC time to define when the quest should become available (specially useful for event quests)
    QuestEnd = -1, -- UTC time to define when the quest should no longer be available (specially useful for event quests)
    RequiredQuests = {"AppleCollection"}, -- A list of quests that are required to be delivered before this quest can be started
    LifeCycles = {"AppleQuest"}, -- The lifecycles that will manage this quest's behavior
    QuestObjectives = {
        appleObjective:NewObjective(3)
    },
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server
local cornObjective = require(ReplicatedStorage.ObjectiveInfos.Corn)

local Quest = RoQuest.Quest

return Quest {
    Name = "Corn Curse!", -- The name of our quest
    Description = "Collect 3 corn", -- The description that we will display to our user
    QuestId = "CornCurse", -- A unique identifier to our quest
    QuestAcceptType = RoQuest.QuestAcceptType.Automatic, -- If the quest automatically gets accepted or rquires manual work
    QuestDeliverType = RoQuest.QuestDeliverType.Manual, -- If the quest automatically gets delivered or requires manual work
    QuestRepeatableType = RoQuest.QuestRepeatableType.Infinite, -- If the quest can be repeated or not
    QuestStart = -1, -- UTC time to define when the quest should become available (specially useful for event quests)
    QuestEnd = -1, -- UTC time to define when the quest should no longer be available (specially useful for event quests)
    RequiredQuests = {}, -- A list of quests that are required to be delivered before this quest can be started
    LifeCycles = {"CornQuest"}, -- The lifecycles that will manage this quest's behavior
    QuestObjectives = {
        cornObjective:NewObjective(3)
    },
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server
local flowerObjective = require(ReplicatedStorage.ObjectiveInfos.Flower)

local Quest = RoQuest.Quest

return Quest {
    Name = "Collect Flowers", -- The name of our quest
    Disabled = true,
    Description = "Collect 3 flowers", -- The description that we will display to our user
    QuestId = "FlowerEvent", -- A unique identifier to our quest
    QuestAcceptType = RoQuest.QuestAcceptType.Automatic, -- If the quest automatically gets accepted or rquires manual work
    QuestDeliverType = RoQuest.QuestDeliverType.Automatic, -- If the quest automatically gets delivered or requires manual work
    QuestRepeatableType = RoQuest.QuestRepeatableType.NonRepeatable, -- If the quest can be repeated or not
    QuestStart = os.time() + 10, -- UTC time to define when the quest should become available (specially useful for event quests)
    QuestEnd = os.time() + 15, -- UTC time to define when the quest should no longer be available (specially useful for event quests)
    RequiredQuests = {}, -- A list of quests that are required to be delivered before this quest can be started
    LifeCycles = {"FlowerQuest"}, -- The lifecycles that will manage this quest's behavior
    QuestObjectives = {
        flowerObjective:NewObjective(3)
    },
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server
local flowerObjective = require(ReplicatedStorage.ObjectiveInfos.Flower)

local Quest = RoQuest.Quest

return Quest {
    Name = "Collect Flowers", -- The name of our quest
    Description = "Collect 4 flowers", -- The description that we will display to our user
    QuestId = "FlowerCollection", -- A unique identifier to our quest
    QuestAcceptType = RoQuest.QuestAcceptType.Manual, -- If the quest automatically gets accepted or rquires manual work
    QuestDeliverType = RoQuest.QuestDeliverType.Automatic, -- If the quest automatically gets delivered or requires manual work
    QuestRepeatableType = RoQuest.QuestRepeatableType.Daily, -- If the quest can be repeated or not
    QuestStart = -1, -- UTC time to define when the quest should become available (specially useful for event quests)
    QuestEnd = -1, -- UTC time to define when the quest should no longer be available (specially useful for event quests)
    RequiredQuests = {}, -- A list of quests that are required to be delivered before this quest can be started
    LifeCycles = {"FlowerQuest"}, -- The lifecycles that will manage this quest's behavior
    QuestObjectives = {
        flowerObjective:NewObjective(4)
    },
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server
local cornObjective = require(ReplicatedStorage.ObjectiveInfos.Corn)

local Quest = RoQuest.Quest

return Quest {
    Name = "Corn Trouble!", -- The name of our quest
    Description = "Collect 1 corn", -- The description that we will display to our user
    QuestId = "InfiniteCorn", -- A unique identifier to our quest
    QuestAcceptType = RoQuest.QuestAcceptType.Manual, -- If the quest automatically gets accepted or rquires manual work
    QuestDeliverType = RoQuest.QuestDeliverType.Manual, -- If the quest automatically gets delivered or requires manual work
    QuestRepeatableType = RoQuest.QuestRepeatableType.Infinite, -- If the quest can be repeated or not
    QuestStart = -1, -- UTC time to define when the quest should become available (specially useful for event quests)
    QuestEnd = -1, -- UTC time to define when the quest should no longer be available (specially useful for event quests)
    RequiredQuests = {}, -- A list of quests that are required to be delivered before this quest can be started
    LifeCycles = {}, -- The lifecycles that will manage this quest's behavior
    QuestObjectives = {
        cornObjective:NewObjective(1)
    },
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server
local marbleObjective = require(ReplicatedStorage.ObjectiveInfos.Marble)

local Quest = RoQuest.Quest

return Quest {
    Name = "Collect Shiny Marble", -- The name of our quest
    Description = "Collect 1 marble", -- The description that we will display to our user
    QuestId = "MarbleCollection", -- A unique identifier to our quest
    QuestAcceptType = RoQuest.QuestAcceptType.Manual, -- If the quest automatically gets accepted or rquires manual work
    QuestDeliverType = RoQuest.QuestDeliverType.Manual, -- If the quest automatically gets delivered or requires manual work
    QuestRepeatableType = RoQuest.QuestRepeatableType.Infinite, -- If the quest can be repeated or not
    QuestStart = -1, -- UTC time to define when the quest should become available (specially useful for event quests)
    QuestEnd = -1, -- UTC time to define when the quest should no longer be available (specially useful for event quests)
    RequiredQuests = {}, -- A list of quests that are required to be delivered before this quest can be started
    LifeCycles = {}, -- The lifecycles that will manage this quest's behavior
    QuestObjectives = {
        marbleObjective:NewObjective(1)
    },
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server
local zombieObjective = require(ReplicatedStorage.ObjectiveInfos.Zombie)

local Quest = RoQuest.Quest

return Quest {
    Name = "An undead problem!", -- The name of our quest
    Description = "Kill 1 Zombie", -- The description that we will display to our user
    QuestId = "KillZombie1", -- A unique identifier to our quest
    QuestAcceptType = RoQuest.QuestAcceptType.Manual, -- If the quest automatically gets accepted or rquires manual work
    QuestDeliverType = RoQuest.QuestDeliverType.Manual, -- If the quest automatically gets delivered or requires manual work
    QuestRepeatableType = RoQuest.QuestRepeatableType.NonRepeatable, -- If the quest can be repeated or not
    QuestStart = -1, -- UTC time to define when the quest should become available (specially useful for event quests)
    QuestEnd = -1, -- UTC time to define when the quest should no longer be available (specially useful for event quests)
    RequiredQuests = {}, -- A list of quests that are required to be delivered before this quest can be started
    LifeCycles = {"ZombieQuest"}, -- The lifecycles that will manage this quest's behavior
    QuestObjectives = {
        zombieObjective:NewObjective(1)
    },
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Server
local Red = require(ReplicatedStorage.RoQuest.Vendor.Red).Server

RoQuest.OnStart():andThen(function()
    local Net = Red "QuestManager"

    Net:On("AcceptQuest", function(player: Player, questId: string)
        RoQuest:GiveQuest(player, questId)
    end)

    Net:On("DeliverQuest", function(player: Player, questId: string)
        RoQuest:DeliverQuest(player, questId)
    end)

    Net:On("CancelQuest", function(player: Player, questId: string)
        RoQuest:CancelQuest(player, questId)
    end)
end)


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

local RoQuest = require(ReplicatedStorage.RoQuest).Server

local questsStore = DataStoreService:GetDataStore("PlayerQuests")
local PlayerQuestData = RoQuest.PlayerQuestData

RoQuest:Init(
    RoQuest:LoadDirectory(ReplicatedStorage.Quests),
    RoQuest:LoadDirectory(ReplicatedStorage.LifeCycles.Server)
)

RoQuest.OnUnAvailableQuestChanged:Connect(function(...)
    print("Unavailable Changed", ...)
end)
RoQuest.OnAvailableQuestChanged:Connect(function(...)
    print("Available Changed", ...)
end)
RoQuest.OnCompletedQuestChanged:Connect(function(...)
    print("Completed Changed", ...)
end)
RoQuest.OnDeliveredQuestChanged:Connect(function(...)
    print("Delivered Changed", ...)
end)
RoQuest.OnInProgressQuestChanged:Connect(function(...)
    print("In Progress Changed", ...)
end)

RoQuest.OnStart():andThen(function()
    local function playerAdded(player: Player)
        local _success, playerData = pcall(function()
            return questsStore:GetAsync("player_"..player.UserId)
        end)

        if playerData then -- Set the data on RoQuest
            task.delay(2, function()
                print("[test] Before Set", RoQuest:GetPlayerData(player))
                RoQuest:SetPlayerData(player, playerData)
                print("[test] After Set", RoQuest:GetPlayerData(player))
            end)
        end
    end

    local function playerRemoved(player: Player)
        local success, errorMessage = pcall(function()
            questsStore:SetAsync("player_"..player.UserId, RoQuest:GetPlayerData(player))
        end)

        if not success then
            print(errorMessage)
        end
    end

    Players.PlayerAdded:Connect(playerAdded)

	for _, player: Player in Players:GetPlayers() do
		playerAdded(player)
	end

	Players.PlayerRemoving:Connect(playerRemoved)

    RoQuest.OnQuestDelivered:Connect(function(player)
        print(RoQuest:GetPlayerData(player))
    end)
end)


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

local RoQuest = require(ReplicatedStorage.RoQuest).Client
local Hud = require(script.Parent.Parent.Interface.Hud)
local Prompt = require(script.Parent.Parent.Interface.Prompt)
local QuestStatus = RoQuest.QuestStatus

type Quest = RoQuest.Quest

local TAG: string = "QuestGiver"
local ICONS = {
    [QuestStatus.Completed] = "?",
    [QuestStatus.Delivered] = "",
    [QuestStatus.InProgress] = "..."
}

local function questGiverAdded(instance: Instance)
    local questId: string = instance:GetAttribute("QuestId")
    local clickDetector: ClickDetector = Instance.new("ClickDetector")
    local highlight: Highlight = Instance.new("Highlight")
    highlight.FillColor = Color3.fromRGB(255, 255, 0)
    highlight.FillTransparency = 0.85
    highlight.Adornee = instance
    highlight.Enabled = false

    local iconTest: TextLabel = instance:WaitForChild("HumanoidRootPart"):WaitForChild("Quest"):WaitForChild("Text")

    clickDetector.MouseHoverEnter:Connect(function()
        highlight.Enabled = true
    end)

    clickDetector.MouseHoverLeave:Connect(function()
        highlight.Enabled = false
    end)

    clickDetector.MouseClick:Connect(function()
        if
            RoQuest:GetQuestStatus(questId) == QuestStatus.Delivered and
            not RoQuest:CanGiveQuest(questId)
        then
            return
        end

        Prompt:SetQuest(questId)
        Hud:EnableScreen("QuestPrompt")
    end)

    local function questStatusChanged(_questId: string)
        if _questId ~= questId then
            return
        end

        local quest: Quest? = RoQuest:GetQuest(questId)
        local displayIcon: string = ""

        if RoQuest:CanGiveQuest(questId) then
            displayIcon = "!"
        elseif quest then
            displayIcon = ICONS[quest:GetQuestStatus()]
        end

        iconTest.Text = displayIcon
    end

    RoQuest.OnQuestCompleted:Connect(questStatusChanged)
    RoQuest.OnQuestStarted:Connect(questStatusChanged)
    RoQuest.OnQuestDelivered:Connect(questStatusChanged)
    RoQuest.OnQuestAvailable:Connect(questStatusChanged)
    questStatusChanged(questId)

    clickDetector.Parent = instance
    highlight.Parent = instance
end

RoQuest.OnStart():andThen(function()
    for _, instance: Instance in CollectionService:GetTagged(TAG) do
        questGiverAdded(instance)
    end

    CollectionService:GetInstanceAddedSignal(TAG):Connect(function(instance: Instance)
        questGiverAdded(instance)
    end)
end)


--!strict
local Players = game:GetService("Players")

local localPlayer: Player = Players.LocalPlayer
local playerGui: PlayerGui = localPlayer:WaitForChild("PlayerGui")
local questLog: ScreenGui = playerGui:WaitForChild("QuestLog")
local prompts: ScreenGui = playerGui:WaitForChild("Prompts")
local hud: ScreenGui = playerGui:WaitForChild("HUD")

local screens = {
    QuestLog = questLog.Container,
    QuestPrompt = prompts.QuestPrompt,
}

local Hud = {}
Hud.currentScreen = ""

function Hud:DisableScreen(screenName: string)
    if not screens[screenName] then
        return
    end

    screens[screenName].Visible = false
    self.currentScreen = ""
end

function Hud:EnableScreen(screenName: string)
    local currentScreen = self.currentScreen
    self:DisableScreen(self.currentScreen)

    if screenName == currentScreen then
        return
    end

    screens[screenName].Visible = true
    self.currentScreen = screenName
end

function Hud:Init()
    for _, instance: Instance in hud.Container:GetChildren() do
        if not instance:IsA("ImageButton") then
            continue
        end

        (instance :: ImageButton).Activated:Connect(function()
            self:EnableScreen(instance.Name)
        end)
    end
end

return Hud


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

local RoQuest = require(ReplicatedStorage.RoQuest).Client

type Quest = RoQuest.Quest

local localPlayer: Player = Players.LocalPlayer
local playerGui: PlayerGui = localPlayer:WaitForChild("PlayerGui")
local popups: ScreenGui = playerGui:WaitForChild("Popups")
local questTemplate = popups:WaitForChild("QuestCompleted"):Clone()
popups.QuestCompleted:Destroy()

local Popup = {}

function Popup:Completed(quest: Quest)
    SoundService:WaitForChild("QuestComplete"):Play()

    local newQuest: Frame = questTemplate:Clone()
    local questNamePosition: UDim2 = newQuest.QuestName.Position
    local initialSize: UDim2 = newQuest.Size
    newQuest.Size = UDim2.fromScale(0, 0)
    newQuest.QuestName.Text = quest.Name
    newQuest.QuestName.Position = UDim2.fromScale(0, 1)

    newQuest.Parent = popups

    local frameTween: Tween = TweenService:Create(newQuest, TweenInfo.new(0.5), {Size = initialSize})
    frameTween:Play()
    frameTween.Completed:Wait()

    local textTween: Tween = TweenService:Create(newQuest.QuestName, TweenInfo.new(0.75), {Position = questNamePosition})
    textTween:Play()
    textTween.Completed:Wait()

    task.wait(2)

    local frameTween2: Tween = TweenService:Create(newQuest, TweenInfo.new(0.5), {Size = UDim2.fromScale(0, 0)})
    frameTween2:Play()
    frameTween2.Completed:Wait()

    newQuest:Destroy()
end

function Popup:Init()
    RoQuest.OnQuestCompleted:Connect(function(questId: string)
        self:Completed(RoQuest:GetStaticQuest(questId))
    end)

    RoQuest.OnQuestObjectiveChanged:Connect(function()
        SoundService:WaitForChild("Collect"):Play()
    end)
end

return Popup


--!strict
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Client
local Red = require(ReplicatedStorage.RoQuest.Vendor.Red).Client
local Hud = require(script.Parent.Hud)

type Quest = RoQuest.Quest
type QuestObjective = RoQuest.QuestObjective
type QuestStatus = RoQuest.QuestStatus

local localPlayer: Player = Players.LocalPlayer
local playerGui: PlayerGui = localPlayer:WaitForChild("PlayerGui")
local prompts: ScreenGui = playerGui:WaitForChild("Prompts")
local questPrompt: Frame = prompts.QuestPrompt
local objectivesFrame: Frame = questPrompt.Objectives
local objectiveTemplate: TextLabel = objectivesFrame.Template:Clone()
local buttons: Frame = questPrompt.Buttons
local QuestStatus = RoQuest.QuestStatus

objectivesFrame.Template:Destroy()

local Prompt = {}
Prompt._CurrentQuestId = ""

function Prompt:SetQuest(questId: string)
    local staticQuest: Quest? = RoQuest:GetStaticQuest(questId)

    if not staticQuest then
        return
    end

    questPrompt.Title.Text = staticQuest.Name
    questPrompt.Description.Description.Text = staticQuest.Description

    for _, child: Instance in objectivesFrame:GetChildren() do
        if child:IsA("TextLabel") then
            child:Destroy()
        end
    end

    for _, questObjective: QuestObjective in staticQuest:GetQuestObjectives() do
        local newTemplate: TextLabel = objectiveTemplate:Clone()
        newTemplate.Text = string.format(questObjective:GetDescription(), "0", questObjective:GetTargetProgress())
        newTemplate.Parent = objectivesFrame
    end

    local questStatus: QuestStatus = RoQuest:GetQuestStatus(questId)

    buttons.Accept.Visible = RoQuest:CanGiveQuest(questId)
    buttons.Deliver.Visible = questStatus == QuestStatus.Completed

    self._CurrentQuestId = questId
end

function Prompt:Init()
    local Net = Red "QuestManager"

    buttons.Accept.Activated:Connect(function()
        Net:Fire("AcceptQuest", self._CurrentQuestId)
        Hud:DisableScreen("QuestPrompt")
    end)

    buttons.Decline.Activated:Connect(function()
        Hud:DisableScreen("QuestPrompt")
    end)

    buttons.Deliver.Activated:Connect(function()
        Net:Fire("DeliverQuest", self._CurrentQuestId)
        Hud:DisableScreen("QuestPrompt")
    end)
end

return Prompt


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

local RoQuest = require(ReplicatedStorage.RoQuest).Client
local Red = require(ReplicatedStorage.RoQuest.Vendor.Red).Client

type Quest = RoQuest.Quest

local DEFAULT_COLOR: Color3 = Color3.fromRGB(140, 140, 140)
local ACTIVE_COLOR: Color3 = Color3.fromRGB(255, 255, 127)

local localPlayer: Player = Players.LocalPlayer
local playerGui: PlayerGui = localPlayer:WaitForChild("PlayerGui")
local questLog: ScreenGui = playerGui:WaitForChild("QuestLog")
local template: Frame = questLog.Container.Unavailable.Template:Clone()
questLog.Container.Unavailable.Template:Destroy() -- Delete the template after we cached a clone

local QuestLog = {}
QuestLog.screens = {} :: {[string]: ScrollingFrame}
QuestLog.buttons = {} :: {[string]: GuiButton}
QuestLog.currentScreen = "" :: string

function QuestLog:UpdateButtonColor()
    for buttonName: string, button: GuiButton in self.buttons do
        button.BackgroundColor3 = buttonName == self.currentScreen and ACTIVE_COLOR or DEFAULT_COLOR
    end
end

function QuestLog:SetScreen(name: string)
    if self.currentScreen == name then
        return
    end

    local currentScrollingFrame: ScrollingFrame? = self.screens[self.currentScreen]
    local newScrollingFrame: ScrollingFrame? = self.screens[name]

    if currentScrollingFrame then
        currentScrollingFrame.Visible = false
    end

    if newScrollingFrame then
        newScrollingFrame.Visible = true
    end

    self.currentScreen = name
    self:UpdateButtonColor()
end

function QuestLog:SetupButtons()
    for _, button: Instance in questLog.Container.Buttons:GetChildren() do
        if not button:IsA("GuiButton") then
            continue
        end

        (button :: GuiButton).Activated:Connect(function()
            self:SetScreen(button.Name)
        end)

        self.buttons[button.Name] = button
    end
end

function QuestLog:SetupWindows()
    for _, child: Instance in questLog.Container:GetChildren() do
        if not child:IsA("ScrollingFrame") then
            continue
        end

        self.screens[child.Name] = child
    end
end

function QuestLog:DestroyQuest(scrollingFrame: ScrollingFrame, questId: string)
    local frame: Frame? = scrollingFrame:FindFirstChild(questId)

    if frame then
        frame:Destroy()
    end
end

function QuestLog:CreateQuest(scrollingFrame: ScrollingFrame, quest: Quest)
    local Net = Red "QuestManager"

    local frame: Frame = scrollingFrame:FindFirstChild(quest.QuestId) or template:Clone()
    frame.Name = quest.QuestId
    frame.Title.Text = quest.Name
    frame.Description.Text = quest.Description
    frame.Buttons.Cancel.Visible = quest:GetQuestStatus() == RoQuest.QuestStatus.InProgress
    frame.Buttons.Cancel.Activated:Connect(function()
        Net:Fire("CancelQuest", quest.QuestId)
    end)

    frame.Visible = true
    frame.Parent = scrollingFrame
end

function QuestLog:PopulateScreen(scrollingFrame: ScrollingFrame, quests: {[string]: Quest})
    for _, child: Instance in scrollingFrame:GetChildren() do -- Remove quests no longer in use
        if not child:IsA("Frame") then
           continue
        end

        if not quests[child.Name] then
            child:Destroy()
        end
    end

    for _, quest: Quest in quests do
        self:CreateQuest(scrollingFrame, quest)
    end
end

function QuestLog:SetAllScreens()
    self:PopulateScreen(self.screens["InProgress"], RoQuest:GetInProgressQuests())
    self:PopulateScreen(self.screens["Available"], RoQuest:GetAvailableQuests())
    self:PopulateScreen(self.screens["Completed"], RoQuest:GetCompletedQuests())
    self:PopulateScreen(self.screens["Delivered"], RoQuest:GetDeliveredQuests())
    self:PopulateScreen(self.screens["Unavailable"], RoQuest:GetUnAvailableQuests())
end

function QuestLog:Init()
    self:SetupWindows() -- Caching our windows
    self:SetupButtons() -- Caching our buttons
    self:SetScreen("InProgress") -- Setting the initial active screen
    self:SetAllScreens() -- Populating all screens

    RoQuest.OnUnAvailableQuestChanged:Connect(function()
        self:PopulateScreen(self.screens["Unavailable"], RoQuest:GetUnAvailableQuests())
    end)

    RoQuest.OnAvailableQuestChanged:Connect(function()
        self:PopulateScreen(self.screens["Available"], RoQuest:GetAvailableQuests())
    end)

    RoQuest.OnCompletedQuestChanged:Connect(function()
        self:PopulateScreen(self.screens["Completed"], RoQuest:GetCompletedQuests())
    end)

    RoQuest.OnDeliveredQuestChanged:Connect(function()
        self:PopulateScreen(self.screens["Delivered"], RoQuest:GetDeliveredQuests())
    end)

    RoQuest.OnInProgressQuestChanged:Connect(function()
        self:PopulateScreen(self.screens["InProgress"], RoQuest:GetInProgressQuests())
    end)

    RoQuest.OnPlayerDataChanged:Connect(function()
        self:SetAllScreens()
    end) -- Hard reset our screens
end

return QuestLog


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

local RoQuest = require(ReplicatedStorage.RoQuest).Client

type Quest = RoQuest.Quest
type QuestObjective = RoQuest.QuestObjective

local QuestStatus = RoQuest.QuestStatus

local localPlayer: Player = Players.LocalPlayer
local playerGui: PlayerGui = localPlayer:WaitForChild("PlayerGui")
local scrollingFrame: ScrollingFrame = playerGui:WaitForChild("Quests"):WaitForChild("Main"):WaitForChild("ScrollingFrame")
local template: Frame = scrollingFrame:WaitForChild("Template")
local clone = template:Clone()
template:Destroy()
template = clone

local Quests = {}

function Quests:UpdateObjective(quest: Quest, objectiveId: string, newValue: number)
    local existingFrame: Frame? = scrollingFrame:FindFirstChild(quest.QuestId)

    if not existingFrame then
        return
    end

    local objectivesWindow: Frame = existingFrame.TextContainer.Objectives
    local objectiveText: TextLabel? = objectivesWindow:FindFirstChild(objectiveId)

    if not objectiveText then
        return
    end

    local questObjective: QuestObjective = quest:GetQuestObjective(objectiveId)

    objectiveText.Text = string.format(questObjective:GetDescription(), newValue, questObjective:GetTargetProgress())
end

function Quests:UpdateQuestFrame(quest: Quest, frame: Frame)
    local textContainer: Frame = frame:WaitForChild("TextContainer")

    if quest:GetQuestStatus() == QuestStatus.Completed then
        textContainer.Description.Text = "Read to deliver!"
    elseif quest:GetQuestStatus() == QuestStatus.Delivered then
        frame:Destroy()
    else
        textContainer.Title.Text = quest.Name
        textContainer.Description.Text = quest.Description
    end
end

function Quests:UpdateInterface()
    local quests: {[string]: Quest} = RoQuest:GetInProgressQuests()

    for _, instance: Instance in scrollingFrame:GetChildren() do -- Delete quests that dont exit
        if instance:IsA("Frame") and not quests[instance.Name] then
            instance:Destroy()
        end
    end

    for questId: string, quest: Quest in quests do
        local existingFrame: Frame = scrollingFrame:FindFirstChild(questId)

        if existingFrame then
            existingFrame:Destroy()
        end

        local newTemplate: Frame = template:Clone()
        newTemplate.Name = questId
        local textContainer: Frame = newTemplate:WaitForChild("TextContainer")
        textContainer.Title.Text = quest.Name
        textContainer.Description.Text = quest.Description
        local objectives: Frame = textContainer:WaitForChild("Objectives")

        for _, questObjective: QuestObjective in quest:GetQuestObjectives() do
            local objectiveTemplate: TextLabel = textContainer.Description:Clone()
            objectiveTemplate.Name = questObjective:GetObjectiveId()
            objectiveTemplate.TextSize = 14
            objectiveTemplate.Text = string.format(questObjective:GetDescription(), questObjective:Get(), questObjective:GetTargetProgress())
            objectiveTemplate.Parent = objectives
        end

        newTemplate.Parent = scrollingFrame
        self:UpdateQuestFrame(quest, newTemplate)
    end
end

function Quests:Init()
    RoQuest.OnPlayerDataChanged:Connect(function()
        self:UpdateInterface()
    end)

    RoQuest.OnInProgressQuestChanged:Connect(function()
        self:UpdateInterface()
    end)

    RoQuest.OnQuestObjectiveChanged:Connect(function(questId: string, objectiveId: string, newValue: number)
        self:UpdateObjective(RoQuest:GetQuest(questId), objectiveId, newValue)
    end)

    self:UpdateInterface()
end

return Quests


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Client

RoQuest.OnStart():andThen(function()
    for _, moduleScript: ModuleScript in script:GetChildren() do
        require(moduleScript):Init()
    end
end)


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RoQuest = require(ReplicatedStorage.RoQuest).Client

RoQuest:Init(RoQuest:LoadDirectory(ReplicatedStorage.LifeCycles.Client))


--!strict
--@author: crusherfire
--@date: 5/8/24
--[[@description:
	Should be placed as a child script of the actor.
]]
-----------------------------
-- DEPENDENCIES --
-----------------------------
local ParallelModuleLoader = require(script.Parent:WaitForChild("ParallelModuleLoader"))

-----------------------------
-- VARIABLES --
-----------------------------
local actor: Actor = script.Parent

-----------------------------
-- HANDLERS --
-----------------------------
actor:BindToMessage("RequireModule", function(module: ModuleScript)
	ParallelModuleLoader.onRequireModule(script, module)
end)

actor:BindToMessage("InitModule", function()
	ParallelModuleLoader.onInitModule(script)
end)

actor:BindToMessage("StartModule", function()
	ParallelModuleLoader.onStartModule(script)
end)

-----------------------------
-- MAIN --
-----------------------------
script:SetAttribute("Loaded", true)


--!strict
--@author: crusherfire
--@date: 5/8/24
--[[@description:
	General code for parallel scripts.
]]
-- Since this module will be required by scripts in separate actors, these variables won't be shared!
local require = require
local requiredModule
local function onRequireModule(script: BaseScript, module: ModuleScript)
	local success, result = pcall(function()
		return require(module)
	end)

	if not success then
		warn("Parallel module errored!\n", result)
		script:SetAttribute("Errored", true)
		script:SetAttribute("Required", true)
		return
	end
	requiredModule = result
	script:SetAttribute("Required", true)
end

local function onInitModule(script: BaseScript)
	if script:GetAttribute("Errored") then
		warn("Unable to init errored module!")
		script:SetAttribute("Initialized", true)
		return
	end
	if not requiredModule then
		warn("Told to load module that does not exist!")
		script:SetAttribute("Initialized", true)
		return
	end
	if not requiredModule.Init then
		script:SetAttribute("Initialized", true)
		return
	end

	local success, result = pcall(function()
		requiredModule:Init()
	end)

	if not success then
		warn("Parallel module errored!\n", result)
		script:SetAttribute("Errored", true)
		script:SetAttribute("Initialized", true)
		return
	end
	script:SetAttribute("Initialized", true)
end

local function onStartModule(script: BaseScript)
	if script:GetAttribute("Errored") then
		warn("Unable to start errored module!")
		script:SetAttribute("Started", true)
		return
	end
	if not requiredModule then
		warn("Told to start module that does not exist!")
		script:SetAttribute("Started", true)
		return
	end
	if not requiredModule.Start then
		script:SetAttribute("Started", true)
		return
	end

	local success, result = pcall(function()
		requiredModule:Start()
	end)

	if not success then
		warn("Parallel module errored!\n", result)
		script:SetAttribute("Errored", true)
		script:SetAttribute("Started", true)
		return
	end
	script:SetAttribute("Started", true)
end

return { onRequireModule = onRequireModule, onInitModule = onInitModule, onStartModule = onStartModule }


--!strict
local ServerScriptService = game:GetService("ServerScriptService")

local RELOCATED_FOLDER = ServerScriptService:FindFirstChild("RELOCATED_MODULES")
assert(RELOCATED_FOLDER, "ServerScriptService missing 'RELOCATED_MODULES' folder")

local module = RELOCATED_FOLDER:FindFirstChild(script.Name)
assert(module, `RELOCATED_MODULES folder missing module '{script.Name}'`)

return require(module) :: any


--!strict
--!nolint LocalShadow

--> Helpers ----------------------------------------------------------------------------
type function assert_is_msg(input: type, input_type: string, label: string, is: string)
    local error_msg_start = `\n'{label}' should be of type {is}`

    local error_msg_end =
        if is_primitive(input_type) then `but is instead of type '{input_type}'!`
        else `but is instead of type '{input_type}': {stringify_preview(input)}`

    return `{error_msg_start} {error_msg_end}`
end

-- Asserts if the input is of the required types - with a nice formatted error message.
type function assert_is(input: type, label: string, input_tag: string, ...: string)
    local input_type = if input_tag == "singleton" then type(input:value()) else input_tag

    if select("#", ...) == 1 then
        local is = ...
        if input_type ~= is then
            error(assert_is_msg(input, input_type, label, `'{is}'`))
        end

    else
        local allowed_types = table.pack(...)

        local assert_failed = false
        for _, allowed_type in allowed_types do
            if input_type ~= allowed_type then
                assert_failed = true
                break
            end
        end

        if assert_failed then
            local allowed_types_len = #allowed_types
            local allowed_types_str = `'{allowed_types[1]}'`
            for idx = 2, allowed_types_len - 1 do
                allowed_types_str ..= `, '{allowed_types[idx]}'`
            end
            allowed_types_str ..= ` or '{allowed_types[allowed_types_len]}'`

            error(assert_is_msg(input, label, input_type, allowed_types_str))
        end
    end
end

type function is_primitive(input_tag: string)
    return
        input_tag == "unknown" or
        input_tag == "never" or
        input_tag == "any" or
        input_tag == "boolean" or
        input_tag == "number" or
        input_tag == "string"
end

-- The equality operator `==` is really strict with unions and intersections,
-- where the order of their components needs to be the same.
type function sort_type(input: type)
    local input_tag = input.tag

    if input_tag == "union" or input_tag == "intersection" then
        local components = input:components()

        table.sort(components, function(a: type, b: type): boolean
            local a_tag, b_tag = a.tag, b.tag

            return
                if a_tag ~= b_tag then a_tag < b_tag
                elseif a_tag == "table" or a_tag == "union" or a_tag == "intersection" then stringify(a) > stringify(b)
                elseif a_tag ~= "singleton" then false
                else (a:value() :: any) < (b:value() :: any)
        end)

        for idx, component in components do
            components[idx] = sort_type(component)
        end

        return (if input_tag == "union" then union_from_components else intersection_from_components)(components)

    elseif input_tag == "function" then
        local params = input:parameters()
        local params_head, params_tail = params.head, params.tail

        if params_head then
            for idx, value in params_head do
                params_head[idx] = sort_type(value)
            end
        end

        if params_tail then
            params_tail = sort_type(params_tail)
        end

        local returns = input:returns()
        local returns_head, returns_tail = returns.head, returns.tail

        if returns_head then
            for idx, value in returns_head do
                returns_head[idx] = sort_type(value)
            end
        end

        if returns_tail then
            returns_tail = sort_type(returns_tail)
        end

        return types.newfunction(
            { head = params_head, tail = params_tail },
            { head = returns_head, tail = returns_tail }
        )

    elseif input_tag == "table" then
        for key, value in input:properties() do
            local value_read, value_write = value.read, value.write

            if value_read == value_write then
                input:setproperty(key, sort_type(value_read))
            else
                if value_read then
                    input:setreadproperty(key, sort_type(value_read))
                end
                if value_write then
                    input:setwriteproperty(key, sort_type(value_write))
                end
            end
        end

        -- TODO: add support for read and write indexers when available.
        local indexer = input:indexer()
        if indexer then
            input:setindexer(sort_type(indexer.index), sort_type(indexer.readresult))
        end

        return input

    elseif input_tag == "negation" then
        return types.negationof(sort_type(input:inner()))

    else
        return input
    end
end

type function is_event_schema(input: type)
    if input.tag ~= "table" then return false end

    local input_properties = input:properties()

    local count = 0
    for _ in input_properties do
        count += 1
    end

    -- Events only have 4 fields.
    if count ~= 4 then return false end

    for key in input_properties do
        if key.tag ~= "singleton" then return false end

        local key = key:value()

        if type(key) ~= "string" then return false end

        if not (key ~= "Connect" or key ~= "ConnectParallel" or key ~= "Once" or key ~= "Wait") then
            return false
        end
    end

    return true
end

type function read_write_map(input: { read: type?, write: type? }, callback: (input: type?) -> type?)
    local input_read, input_write = input.read, input.write

    if input_read == input_write then
        local input_read = callback(input.read)

        input.read = input_read
        input.write = input_read

    else
        input.read = callback(input_read)
        input.write = callback(input_write)
    end
end

type function read_write_find_first(input: { read: type?, write: type? }, callback: (input: type) -> boolean): type?
    local input_read, input_write = input.read, input.write

    if input_read then
        local found = type_find_first(input_read, callback)
        if found then return found end
    end

    if input_write then
        local found = type_find_first(input_write, callback)
        if found then return found end
    end

    return nil
end

type function unions_and_intersections_flatten(input: type)
    local input_tag = input.tag

    if input_tag == "union" or input_tag == "intersection" then
        local components: { type } = input:components()

        for idx = #components, 1, -1 do
            local component = components[idx]
            local component_tag = component.tag
            if component_tag ~= "union" and component_tag ~= "intersection" then continue end

            local component = unions_and_intersections_flatten(component)
            if component.tag == input_tag then
                local sub_components: { type } = component:components()

                components[idx] = sub_components[1]
                for idx = 2, #sub_components do
                    table.insert(components, sub_components[idx])
                end

            else
                components[idx] = component
            end
        end

        return (if input_tag == "union" then union_from_components(components) else intersection_from_components(components)) or types.never

    else
        return input
    end
end

type function type_find_first(input: type, callback: (input: type) -> boolean): type?
    local input_tag = input.tag

    if input_tag == "union"  or input_tag == "intersection" then
        for _, component in unions_and_intersections_flatten(input):components() do
            if callback(component) then return input end
        end

    elseif input_tag == "negation" then
        if callback(input:inner()) then return input end
    end

    return if callback(input) then input else nil
end

type function type_has_tag(
    input: type,
    tags: {
        [
            | "any"
            | "boolean"
            | "buffer"
            | "extern"
            | "function"
            | "generic"
            | "intersection"
            | "negation"
            | "never"
            | "nil"
            | "number"
            | "singleton"
            | "string"
            | "table"
            | "thread"
            | "union"
            | "unknown"
        ]: true
    }
): type?
    local input_tag = input.tag

    if input_tag == "union"  or input_tag == "intersection" then
        for _, component in unions_and_intersections_flatten(input):components() do
            if tags[component.tag] then return input end
        end

    elseif input_tag == "negation" then
        if tags[input.tag] then return input end
    end

    return if tags[input.tag] then input else nil
end
----------------------------------------------------------------------------------------

--> Stringify Helpers ------------------------------------------------------------------
type function stringify_preview(input: type)
    return `\n> {string.gsub(stringify(input), "\n", "\n> ")}`
end

type function stringify_table(
    input: type, nestedness: number,
    pre_padding: string?, post_padding: string?
)
    local properties = input:properties()
    local indexer = input:indexer()

    if table_value_is_empty(properties, indexer) then return "{ }" end

    local next_nestedness = nestedness + 1

    local outer_padding = string.rep("   ", nestedness - 1)
    local padding = outer_padding .. "    "

    local stringified = `{pre_padding or outer_padding}\{`

    -- Sorts the table keys so its stringified representation
    -- to ensure stringification is deterministic.
    local keys: { type } = {}
    for key in properties do table.insert(keys, key) end
    table.sort(keys, function(a, b) return tostring(a) > tostring(b) end)

    for _, key in keys do
        local value = properties[key]

        local key_value = if key.tag == "singleton" then key:value() else input

        local stringified_key =
            if type(key_value) == "string" then key_value
            else `[{stringify_main(key, next_nestedness, "", padding)}]`

        local value_read, value_write = value.read, value.write

        if value_read == value_write then
            stringified ..= `\n{padding}{stringified_key}: {stringify_main(value_read, next_nestedness)}`

        else
            if value_read then
                stringified ..= `\n{padding}read {stringified_key}: {stringify_main(value_read, next_nestedness)}`
            end
            if value_write then
                stringified ..= `\n{padding}write {stringified_key}: {stringify_main(value_write, next_nestedness)}`
            end
        end
    end

    -- TODO: add support for read and write indexers when available.
    if indexer then
        local indexer_index = indexer.index
        local indexer_index_value = if indexer_index.tag == "singleton" then indexer_index:value() else input

        local stringified_indexer_index =
            if type(indexer_index_value) == "string" then indexer_index_value
            else `[{stringify_main(indexer_index, next_nestedness, "", padding)}]`

        stringified ..= `\n{padding}{stringified_indexer_index}: {stringify_main(indexer.readresult, next_nestedness)}`
    end

    return stringified .. `\n{post_padding or outer_padding}}`
end

type function stringify_components(
    components: { type },
    concatenator: string
)
    for idx, component in components do
        local component_tag = component.tag

        if component_tag == "union" then
            components[idx] = `({stringify_components(component:components(), " | ")})` :: any

        elseif component_tag == "intersection" then
            components[idx] = `({stringify_components(component:components(), " & ")})` :: any

        else
            components[idx] = stringify_main(component, 1)
        end
    end

    return table.concat(components, concatenator)
end

type function stringify_function(input: type)
    local params = input:parameters()
    local params_head, params_tail = params.head, params.tail

    local stringified_params: string
    if params_head then
        stringified_params = ""

        local params_head_len = #params_head
        for idx = 1, params_head_len - 1 do
            stringified_params ..= `{stringify(params_head[idx])}, `
        end
        stringified_params ..= `{stringify(params_head[params_head_len])}`

        if params_tail then
            local params_tail_tag = params_tail.tag
            if params_tail_tag == "union" or params_tail_tag == "intersection" then
                stringified_params ..= `, ...({stringify(params_tail)}))`
            else
                stringified_params ..= `, ...{stringify(params_tail)})`
            end
        end

    elseif params_tail then
        local params_tail_tag = params_tail.tag
        if params_tail_tag == "union" or params_tail_tag == "intersection" then
            stringified_params = `...({stringify(params_tail)})`
        else
            stringified_params = `...{stringify(params_tail)}`
        end
    end

    local returns = input:returns()
    local returns_head, returns_tail = returns.head, returns.tail

    local returns_head_len: number
    local stringified_returns: string
    if returns_head then
        stringified_returns = ""

        returns_head_len = #returns_head
        for idx = 1, returns_head_len - 1 do
            stringified_returns ..= `{stringify(returns_head[idx])}, `
        end
        stringified_returns ..= `{stringify(returns_head[returns_head_len])}`

        if returns_tail then
            local returns_tail_tag = returns_tail.tag
            if returns_tail_tag == "union" or returns_tail_tag == "intersection" then
                stringified_returns ..= `, ...({stringify(returns_tail)}))`

            else
                stringified_returns ..= `, ...{stringify(returns_tail)})`
            end
        end

    elseif returns_tail then
        local returns_tail_tag = returns_tail.tag
        if returns_tail_tag == "union" or returns_tail_tag == "intersection" then
            stringified_returns = `...({stringify(returns_tail)})`

        else
            stringified_returns = `...{stringify(returns_tail)}`
        end
    end

    local stringified_returns =
        if returns_tail or (returns_head and returns_head_len >= 2) then `({stringified_returns})`
        else stringified_returns

    return `({stringified_params}) -> {stringified_returns}`
end

type function stringify_main(
    input: type, nestedness: number,
    pre_padding: string?, post_padding: string?
)
    local input_tag = input.tag

    if input_tag == "negation" then
        local input_inner = input:inner()
        local input_inner_tag = input_inner.tag

        return
            if (
                input_inner_tag == "union" or input_inner_tag == "intersection"
            ) then `~({stringify_main(input:inner(), nestedness)})`
            else `~{stringify_main(input:inner(), nestedness)}`
    end

    if input_tag == "union" then return stringify_components(input:components(), " | ")
    elseif input_tag == "intersection" then return stringify_components(input:components(), " & ")
    elseif input_tag == "table" then return stringify_table(input, nestedness, pre_padding, post_padding)
    elseif input_tag == "function" then return stringify_function(input)
    elseif input_tag == "unknown" then return "unknown"
    elseif input_tag == "never" then return "never"
    elseif input_tag == "any" then return "any"
    elseif input_tag == "boolean" then return "boolean"
    elseif input_tag == "number" then return "number"
    elseif input_tag == "string" then return "string" end

    local input = if input_tag == "singleton" then input:value() else input
    local input_type = type(input)

    return
        if input_type == "string" then `"{input}"`
        else tostring(input)
end

type function stringify(input: type)
    return stringify_main(input, 1)
end
----------------------------------------------------------------------------------------

--> Hashset Helpers --------------------------------------------------------------------
type function hashset_can_insert(input_tag: string)
    return input_tag == "singleton"
end

type function hashset_insert(
    hashset: typeof(types.newtable()),
    non_hashsettable: { type },
    input: type,
    input_tag: string
)
    if hashset_can_insert(input_tag) then
        hashset:setproperty(input, types.never)

    else
        table.insert(non_hashsettable, input)
    end
end

type function hashset_has_type(
    hashset: typeof(types.newtable()),
    non_hashsettable: { type },
    input: type,
    input_tag: string
)
    if hashset_can_insert(input_tag) then
        return if hashset:readproperty(input) then true else false

    else
        for _, component in non_hashsettable do
            if input == component then return true end
        end

        return false
    end
end

type function hashset_from_components(input: { type })
    local hashset = types.newtable()

    local input_len = #input

    for idx = input_len, 1, -1 do
        local component = input[idx]

        if hashset_can_insert(component.tag) then
            hashset:setproperty(component, types.never)
            input_len = table_value_swap_remove(input, input_len, idx)
        end
    end

    -- The input table should now only contain the
    -- components that could not be inserted into the hashset.
    return hashset, input
end
----------------------------------------------------------------------------------------

--> Union Helpers ----------------------------------------------------------------------
type function union_from_components(input: { type })
    local input_len = #input
    return if input_len == 0 then nil elseif input_len == 1 then input[1] else types.unionof(table.unpack(input))
end
----------------------------------------------------------------------------------------

--> Intersection Helpers ---------------------------------------------------------------
type function intersection_from_components(input: { type })
    local input_len = #input
    return if input_len == 0 then nil elseif input_len == 1 then input[1] else types.intersectionof(table.unpack(input))
end
----------------------------------------------------------------------------------------

--> Table Helpers ----------------------------------------------------------------------
-- Sets the value of a table in a way which works around the
-- erroneously seperated read and write fields in type hints issue.
type function table_set_property(
    input: type,
    key: type,
    value: { read: type?, write: type? }?
)
    if value == nil then return end

    local value_read, value_write = value.read, value.write
    if value_read == value_write then
        if value_read then
            input:setproperty(key, value_read)
        end

    else
        input:setreadproperty(key, value_read)
        input:setwriteproperty(key, value_write)
    end
end

type function table_value_swap_remove(input: { [any]: any }, idx: number): type
    -- Last item, we can just remove from
    -- the end of the input table.
    if idx == #input then
        return table.remove(input, idx)

    -- Swap remove optimisation.
    else
        local output = input[idx]

        input[idx] = table.remove(input)

        return output
    end
end

type function table_value_is_empty(properties: { [any]: any }, indexer: type)
    if indexer then return false end

    for _ in properties do return false end
    return true
end
----------------------------------------------------------------------------------------

--> Extern Helpers ---------------------------------------------------------------------
type function extern_is_in_datamodel(input: type): boolean
    local valueParent = extern_find_property(input, types.singleton("Parent"))
    if not valueParent then return false end

    -- If the parent is nil then the input is not in the datamodel.
    if read_write_find_first(valueParent, function(value: type) return value.tag == "nil" end) then
        return false
    end

    return true
end

type function extern_find_property(input: type, propertyName: type): { read: type?, write: type? }?
    assert_is(input, "input", input.tag, "extern")

    local head = input

    while head do
        for key, value: { read: type?, write: type? } in head:properties() do
            if key == propertyName then return value end
        end

        head = head:readparent()
    end

    return nil
end

type function extern_filter(input: type, callback: (value: type) -> type?)
    assert_is(input, "input", input.tag, "extern")

    local output = types.newtable()

    local head = input

    while head do
        for key, value: { read: type?, write: type? } in head:properties() do
            read_write_map(value, function(value: type?): type?
                if not value then return nil end

                return callback(value)
            end)

            table_set_property(output, key, value)
        end

        head = head:readparent()
    end

    return output
end
----------------------------------------------------------------------------------------

--[=[
    Returns a table of properties for the given Roblox instance.

    @param input type -- The instance to get properties for.

    @example
    type Result = PropertiesOf<Part>

    --[[
    type Result = {
        Anchored: boolean,
        ... *TRUNCATED*
    ]]
]=]
export type function PropertiesOf(input: type)
    return extern_filter(input, function(value: type): type?
        return type_find_first(value, function(value: type)
            local value_tag = value.tag
            if value_tag == "table" or value_tag == "function" then return false end

            -- If the value is in the datamodel then it is a descendant / child, not a property.
            if value_tag == "extern" and extern_is_in_datamodel(value) then return false end

            return true
        end)
    end)
end

--[=[
    Returns a table of methods for the given Roblox instance.

    @param input type -- The instance to get methods for.

    @example
    type Result = MethodsOf<Part>

    --[[
    type Result = {
        AddTag: (Instance, string) -> nil,
        ... *TRUNCATED*
    ]]
]=]
export type function MethodsOf(input: type)
    return extern_filter(input, function(value: type): type?
        if value.tag ~= "function" then return nil end

        return value
    end)
end

--[=[
    Returns a table of events for the given Roblox instance.

    @param input type -- The instance to get events for.

    @example
    type Result = EventsOf<Part>

    --[[
    type Result = {
        AncestryChanged: {
            Connect: t1,
            ConnectParallel: (t2, (Instance, Instance?) -> ()) -> RBXScriptConnection,
            Once: (t2, (Instance, Instance?) -> ()) -> RBXScriptConnection,
            Wait: (t2) -> (Instance, Instance?)
        },
        ... *TRUNCATED*
    ]]
]=]
export type function EventsOf(input: type)
    return extern_filter(input, function(value: type): type?
        if not is_event_schema(value) then return nil end

        return value
    end)
end

--[=[
    Returns a table of children for the given Roblox instance.

    @param input type -- The instance to get children for.

    @example
    type Result = ChildrenOf<typeof(workspace.Part)>

    --[[
    type Result = {
        1: Attachment
    }
    ]]
]=]
export type function ChildrenOf(input: type)
    assert_is(input, "input", input.tag, "extern")

    local output = types.newtable()
    local output_idx = 0

    local head = input

    while head do
        for key, value: { read: type?, write: type? } in head:properties() do
            if key:value() == "Parent" then continue end

            read_write_map(value, function(value: type?): type?
                if not value then return nil end
                if value.tag ~= "extern" then return nil end
                if not extern_find_property(value, types.singleton("ClassName")) then return nil end

                -- If the value is not in the datamodel then it is a property, not a child.
                if not extern_is_in_datamodel(value) then return nil end

                return value
            end)

            local value_read, value_write = value.read, value.write

            if value_read == value_write then
                if value_read then
                    output_idx += 1
                    output:setproperty(types.singleton(`{output_idx}`), value_read)
                end

            else
                if value_read then
                    output_idx += 1
                    output:setreadproperty(types.singleton(`{output_idx}`), value_read)
                end

                if value_write then
                    output_idx += 1
                    output:setwriteproperty(types.singleton(`{output_idx}`), value_write)
                end
            end
        end

        head = head:readparent()
    end

    return output
end

--[=[
    Returns a table of descendants for the given Roblox instance.

    @param input type -- The instance to get descendants for.

    @example
    type Result = DescendantsOf<typeof(workspace)>

    --[[
    type Result = {
        1: Part,
        2: Camera,
        3: Terrain,
        4: Attachment
    }
    ]]
]=]
export type function DescendantsOf(input: type)
    local output = types.newtable()
    local output_idx = 0

    local stack: { type } = { input }

    while #stack ~= 0 do
        local inner_input = table_value_swap_remove(stack, 1)

        local inner_output = ChildrenOf(inner_input)

        for key, value: { read: type?, write: type? } in inner_output:properties() do
            local value_read, value_write = value.read, value.write

            if value_read == value_write then
                if value_read then
                    table.insert(stack, value_read)
                    output_idx += 1
                    table_set_property(output, types.singleton(`{output_idx}`), value)
                end

            else
                if value_read then
                    table.insert(stack, value_read)
                    output_idx += 1
                    output:setreadproperty(types.singleton(`{output_idx}`), value_read)
                end

                if value_write then
                    table.insert(stack, value_write)
                    output_idx += 1
                    output:setwriteproperty(types.singleton(`{output_idx}`), value_write)
                end
            end
        end
    end

    return output
end

return nil


--> Runs checks on code quality

local exec = require("util")

exec("luau-lsp analyze --settings=.vscode/settings.json --sourcemap=sourcemap.json src")
exec("selene src")
exec("stylua --check src")


local exec = require("util")

exec("rojo sourcemap dev.project.json --output sourcemap.json --watch & darklua process src/ rbx/ --watch")


--> Installs dependencies and sets up the sourcemap

local exec = require("util")

exec("wally install")
exec("lune setup")
exec("rojo sourcemap dev.project.json --output sourcemap.json")


--> Internal utility library depended upon by other scripts

local process = require("@lune/process")
local stdio = require("@lune/stdio")

return function(command: string)
	local args = command:split(" ")
	local program = table.remove(args, 1) :: string

	local result = process.spawn(program, args, {
		stdio = "forward",
		shell = true,
	})

	if not result.ok then
		stdio.ewrite(
			`\n{stdio.style("bold") .. stdio.color("red") .. "err" .. stdio.color("reset")}: {program} exited with code {result.code}\n`
		)
		process.exit(result.code)
	end
end


export type Activation = "Sigmoid" | "TanH" | "ArcTan" | "SoftPlus" | "Linear" | "ReLU" | "LeakyReLU" | "ELU"

local Activation = {
	Sigmoid = "Sigmoid",
	TanH = "TanH",
	ArcTan = "ArcTan",
	SoftPlus = "SoftPlus",
	Linear = "Linear",
	ReLU = "ReLU",
	LeakyReLU = "LeakyReLU",
	ELU = "ELU",
}

local functions = {
	[Activation.Sigmoid] = function(x: number): number
		return 1 / (1 + math.exp(-x))
	end,

	[Activation.TanH] = math.tanh,

	[Activation.ArcTan] = math.atan,

	[Activation.SoftPlus] = function(y: number): number
		return math.log(math.exp(y) + 1)
	end,

	[Activation.Linear] = function(x: number): number
		return x
	end,

	[Activation.ReLU] = function(x: number): number
		return math.max(0, x)
	end,

	[Activation.LeakyReLU] = function(x: number): number
		return math.max(0.01 * x, x)
	end,

	[Activation.ELU] = function(x: number): number
		return x < 0 and 2 * (math.exp(x) - 1) or x
	end,
}

local derivatives = {
	[Activation.Sigmoid] = function(y: number): number
		return y * (1 - y)
	end,

	[Activation.TanH] = function(y: number): number
		return 1 - y ^ 2
	end,

	[Activation.ArcTan] = function(y: number): number
		return 1 / (math.tan(y) ^ 2 + 1)
	end,

	[Activation.SoftPlus] = function(y: number): number
		return 1 / (1 + 1 / (math.exp(y) - 1))
	end,

	[Activation.Linear] = function(): number
		return 1
	end,

	[Activation.ReLU] = function(y: number): number
		return y > 0 and 1 or 0
	end,

	[Activation.LeakyReLU] = function(y: number): number
		return y > 0 and 1 or 0.01
	end,

	[Activation.ELU] = function(y: number): number
		return y < 0 and y + 2 or 1
	end,
}

return {
	Activation = Activation,
	functions = functions,
	derivatives = derivatives,
}


--[=[
	Creates a deep copy of the given object. Can be used to clone a neural
	network, since networks are JSON encodable.

	@param object The object to clone.
	@return A deep copy of the object.
]=]
local function copy<T>(object: T): T
	if type(object) == "table" then
		local clone = table.clone(object)

		for key, value in next, clone do
			clone[key] = copy(value)
		end

		return clone :: any
	end

	return object
end

return copy


local types = require("./types")
type Network = types.Network

--[=[
	Creates a new neural network with the specified shape and activation
	functions. The shape is an array of numbers, where each element represents
	the amount of neurons in that layer.

	@param shape The shape of the network.
	@param activation The activation functions of each layer.
	@return A new neural network.
]=]
local function create(shape: { number }, activation: string | { string }): Network
	local width = #shape - 1
	local layers = table.move(shape, 2, width + 1, 1, {})

	local network: Network = {
		size = width,
		shape = shape,
		layers = table.create(width),
		weights = table.create(width),
		biases = table.create(width),
		activation = if type(activation) == "table" then activation else table.create(width, activation),
	}

	for i, size in next, layers do
		network.layers[i] = table.create(size, 0)
		network.biases[i] = table.create(size, 0)
		network.weights[i] = table.create(size)

		for j = 1, size do
			network.weights[i][j] = table.create(shape[i], 0)
		end
	end

	return network
end

return create


local task = task or require("@lune/task")

local init = require("./initializers")
local types = require("./types")
type Network = types.Network
local copy = require("./copy")

type Thenable = {
	andThen: (self: Thenable, resolved: ((...any) -> ())?, rejected: ((unknown) -> ())?) -> Thenable,
}

export type Agent = {
	network: Network,
	fitness: number,
}

--[=[
	Runs a genetic algorithm to evolve the neural network to minimize the
	error between the output signal and the target signal.

	Calls the `spawn` function to assess the fitness of each agent in the
	population, where `spawn` should return a fitness score for the agent.
	If `spawn` returns a promise, the genetic algorithm will run all agents
	in parallel.

	@param options The genetic algorithm options.
	@return Each agent in the population sorted by fitness.
]=]
local function evolution(params: {
	network: Network,
	population: number,
	epochs: number?,
	mutation: number?,
	spawn: (network: Network, index: number) -> Thenable | number,
	done: ((epoch: number, agents: { Agent }) -> ())?,
}): { Agent }
	local agents: { Agent } = table.create(params.population)

	for index = 1, params.population do
		agents[index] = {
			network = init.normal(copy(params.network)),
			fitness = 0,
		}
	end

	local function mutate()
		return (math.random() * 2 - 1) * (params.mutation or 0.1)
	end

	-- Combine two networks by randomly selecting biases and weights from either
	-- parent and applying mutations.
	local function crossover(parent: Network, partner: Network)
		local offspring = copy(parent)

		for i, biases in next, offspring.biases do
			for j, weights in next, offspring.weights[i] do
				for k, weight in next, weights do
					weights[k] = mutate() + (math.random() < 0.5 and weight or partner.weights[i][j][k])
				end

				biases[j] = mutate() + (math.random() < 0.5 and biases[j] or partner.biases[i][j])
			end
		end

		return offspring
	end

	-- Returns a function that selects a random agent from the population based
	-- on their fitness values, assuming that the agent population is sorted in
	-- descending order.
	local function distribute()
		-- Shift the fitness values to be non-negative for a uniform distribution.
		local shift = -math.min(0, agents[params.population].fitness) * params.population
		local networks = table.create(params.population)
		local sum = shift

		for index, agent in next, agents do
			sum += agent.fitness
			networks[index] = agent.network
		end

		return function()
			local cursor = math.random() * sum
			local accumulator = shift

			for index, agent in next, agents do
				accumulator += agent.fitness

				if accumulator >= cursor then
					return networks[index]
				end
			end

			error("Failed to select an agent from the population")
		end
	end

	-- The top 20% of agents are selected to be copied directly into the next
	-- generation. The remaining 80% are generated by combining two randomly
	-- selected agents proportional to their fitness values.
	local function evolve()
		table.sort(agents, function(a, b)
			return a.fitness > b.fitness
		end)

		local pick = distribute()

		for index, agent in next, agents do
			if index > params.population * 0.2 then
				agent.network = crossover(pick(), pick())
			end
		end
	end

	-- Run the genetic algorithm for a single epoch, spawning agents (either
	-- sequentially or in parallel) and waiting for all agents to complete
	-- before advancing to the next generation.
	local function simulate()
		local threads = 0

		for index, agent in next, agents do
			local fitness = params.spawn(agent.network, index)

			if type(fitness) == "number" then
				agents[index].fitness = fitness
				continue
			end

			threads += 1

			-- If agents are running in parallel, wait for all agents to complete
			-- before resuming the genetic algorithm.
			(fitness :: Thenable):andThen(function(fitness: number)
				threads -= 1
				agents[index].fitness = fitness
			end, function(err: any)
				threads -= 1
				agents[index].fitness = 0
				warn(`Agent {index} encountered an error:`, err)
			end)
		end

		while threads > 0 do
			task.wait()
		end
	end

	for epoch = 1, params.epochs or 100 do
		if epoch > 1 then
			evolve()
		end

		simulate()

		if params.done and params.done(epoch, agents) then
			break
		end
	end

	return agents
end

return evolution


local activation = require("./activation")
local backpropagate = require("./backpropagate")
local copy = require("./copy")
local create = require("./create")
local evolution = require("./evolution")
local init = require("./initializers")
local initialize = require("./initialize")
local predict = require("./predict")
local types = require("./types")

export type Network = types.Network
export type Activation = activation.Activation
export type Agent = evolution.Agent

return {
	Activation = activation.Activation,
	backpropagate = backpropagate,
	clone = copy,
	create = create,
	evolution = evolution,
	init = init,
	initialize = initialize,
	predict = predict,
}


local types = require("./types")
type Network = types.Network

export type Filter = "weights" | "biases" | "all"

--[=[
	Calls the `initializer` for each weight and bias in the network. If the
	`initializer` returns `undefined`, the weight or bias will not be changed.

	The `initializer` receives the layer index and the neuron index. If the
	function is adjusting a weight, it will also receive the previous neuron
	index. Otherwise, it will receive `-1`.

	@param network The neural network.
	@param initializer The initializer function.
	@param filter The target of the initializer. Defaults to `"all"`.
	@return The neural network.
]=]
local function initialize(
	network: Network,
	initializer: (number, number, number) -> number | false?,
	filter: Filter?
): Network
	filter = filter or "all"

	if filter == "weights" or filter == "all" then
		for i, weights in next, network.weights do
			for j, neuron in next, weights do
				for k in next, neuron do
					neuron[k] = initializer(i, j, k) or neuron[k]
				end
			end
		end
	end

	if filter == "biases" or filter == "all" then
		for i, biases in next, network.biases do
			for j in next, biases do
				biases[j] = initializer(i, j, -1) or biases[j]
			end
		end
	end

	return network
end

return initialize


local Activation = require("./activation").Activation
local initialize = require("./initialize")
type Filter = initialize.Filter
local types = require("./types")
type Network = types.Network

local DEFAULT_GAIN = {
	[Activation.TanH] = 5 / 3,
	[Activation.ReLU] = 2 ^ 0.5,
	[Activation.LeakyReLU] = (2 / 1.01) ^ 0.5,
}

--[=[
	Perform a Box-Muller transform to convert two uniform random numbers into a
	single Gaussian-distributed random number.

	@param mean The mean of the distribution.
	@param std The standard deviation of the distribution.
	@return A normally-distributed random number.
]=]
local function gaussian(mean: number, std: number): number
	local u = 1 - math.random() -- Converting [0, 1) to (0, 1)
	local v = math.random()
	local z = (-2 * math.log(u)) ^ 0.5 * math.cos(2 * math.pi * v)

	return mean + std * z
end

--[=[
	A collection of initialization functions to prepare a neural network for
	optimization.
]=]
return {
	zeros = function(network: Network, filter: Filter?)
		return initialize(network, function()
			return 0
		end, filter)
	end,

	ones = function(network: Network, filter: Filter?)
		return initialize(network, function()
			return 1
		end, filter)
	end,

	constant = function(network: Network, value: number, filter: Filter?)
		return initialize(network, function()
			return value
		end, filter)
	end,

	uniform = function(network: Network, min: number?, max: number?, filter: Filter?)
		min = min or 0
		max = max or 1
		assert(min and max, "Luau")

		return initialize(network, function()
			return math.random() * (max - min) + min
		end, filter)
	end,

	normal = function(network: Network, mean: number?, std: number?, filter: Filter?)
		mean = mean or 0
		std = std or 1
		assert(mean and std, "Luau")

		return initialize(network, function()
			return gaussian(mean, std)
		end, filter)
	end,

	xavierNormal = function(network: Network, gain: number?)
		return initialize(network, function(layer)
			local scale = gain or DEFAULT_GAIN[network.activation[layer]] or 1
			local fanIn = network.shape[layer]
			local fanOut = network.shape[layer + 1]

			return gaussian(0, scale * (2 / (fanIn + fanOut)) ^ 0.5)
		end, "weights")
	end,

	xavierUniform = function(network: Network, gain: number?)
		return initialize(network, function(layer)
			local scale = gain or DEFAULT_GAIN[network.activation[layer]] or 1
			local fanIn = network.shape[layer]
			local fanOut = network.shape[layer + 1]
			local range = scale * (6 / (fanIn + fanOut)) ^ 0.5

			return math.random() * 2 * range - range
		end, "weights")
	end,
}


local activation = require("./activation")
local types = require("./types")
type Network = types.Network

--[=[
	Feeds the input signal through the network and returns the output signal.

	@param network The neural network.
	@param input The input signal.
	@return The output signal.
]=]
local function predict(network: Network, inputs: { number }): { number }
	local previous = inputs

	for i, layer in next, network.layers do
		local activation = activation.functions[network.activation[i]]

		for j, bias in next, network.biases[i] do
			local sum = bias

			for k, weight in next, network.weights[i][j] do
				sum += weight * previous[k]
			end

			layer[j] = activation(sum)
		end

		previous = layer
	end

	return network.layers[network.size]
end

return predict


--[=[
	The neural network model, containing the neurons, weights, biases, and
	activation functions of the network. The network is JSON encodable, so
	it is safe to serialize and send over a remote event.
]=]
export type Network = {
	--[=[
		The total amount of layers in the network, excluding the input layer.
	]=]
	size: number,
	--[=[
		The shape of the network, where each element represents the amount of
		neurons in that layer. The first element is the input layer, the last
		element is the output layer, and the rest are hidden layers.
	]=]
	shape: { number },
	--[=[
		A 2D array containing the signals of each neuron in each layer. Keyed by
		the layer index and the neuron index.
	]=]
	layers: { { number } },
	--[=[
		A 3D array containing the weights of each connection between neurons.
		Keyed by the layer index, the neuron itself, and the previous neuron.
	]=]
	weights: { { { number } } },
	--[=[
		A 2D array containing the biases of each neuron in each layer. Keyed by
		the layer index and the neuron index.
	]=]
	biases: { { number } },
	--[=[
		The activation functions of each layer after the input layer. Keyed by
		the index of the layer.
	]=]
	activation: { string },
}

return {}


-- Types
type Properties = {
    prompt: ProximityPrompt,
    ButtonHeldDown: any,
    CurrentBarSize: any,
    CurrentFrameScaleFactor: any,
    PromptTransparency: any,
    InputFrameScaleFactor: any,
}

-- Services
local RunService = game:GetService("RunService")
local SoundService = game:GetService("SoundService")

-- Imports
local SoundData = require(script.Parent.SoundData)

local function PlaySound(Scope : any, SoundId : string, Properties : {[string]: any}?)
	local Sound = SoundService:FindFirstChild(SoundId.Value)

	if not Sound then
		Sound = Scope:New(Scope:New("Sound", {
			Name = SoundId,
			SoundId = SoundId,
			Volume = SoundData.SoundVolume,
			Parent = SoundService,
		}), Properties or {})
	end

	Sound:Play()
end

local function StopSound(SoundId)
	local Sound = SoundService:FindFirstChild(SoundId.Value)

	if Sound then
		Sound:Stop()
	end
end

return function(Scope : any, Properties : Properties) : {RBXScriptConnection}
    local InputConnections = {}
	local HoldStart = os.clock()

	PlaySound(Scope, SoundData.AppearSoundId)

	if Properties.prompt.HoldDuration > 0 then
		InputConnections[#InputConnections + 1] = RunService.RenderStepped:Connect(function(deltaTime)
			local TimeHeldDown = os.clock() - HoldStart

			if Properties.ButtonHeldDown.Value then
				Properties.CurrentBarSize.Value = math.min(Properties.CurrentBarSize.Value + deltaTime / Properties.prompt.HoldDuration, 1)

				SoundService:FindFirstChild(SoundData.HoldSoundId.Value).PlaybackSpeed = 0.5 + TimeHeldDown
			else
				Properties.CurrentBarSize.Value = 0
			end
		end)

		InputConnections[#InputConnections + 1] = Properties.prompt.PromptButtonHoldBegan:Connect(function()
			HoldStart = os.clock()
			PlaySound(Scope, SoundData.HoldSoundId, {
				Looped = true,
				Volume = 0.2,
			})
			PlaySound(Scope, SoundData.ClickSoundId)
			Properties.ButtonHeldDown.Value = true
			Properties.CurrentFrameScaleFactor.Value = Properties.InputFrameScaleFactor
		end)

		InputConnections[#InputConnections + 1] = Properties.prompt.PromptButtonHoldEnded:Connect(function()
			StopSound(SoundData.HoldSoundId)
			Properties.ButtonHeldDown.Value = false
			Properties.CurrentFrameScaleFactor.Value = 1
		end)
	end

	InputConnections[#InputConnections + 1] = Properties.prompt.Triggered:Connect(function()
		if Properties.prompt.HoldDuration > 0 then
			PlaySound(Scope, SoundData.TriggerSoundId)
		else
			PlaySound(Scope, SoundData.ClickSoundId)
			Properties.ButtonHeldDown.Value = true

			task.delay(1 / 60, function()
                Properties.ButtonHeldDown.Value = false
			end)
		end
	end)

    return InputConnections
end


return {
	[Enum.KeyCode.ButtonX] = "rbxasset://textures/ui/Controls/xboxX.png",
	[Enum.KeyCode.ButtonY] = "rbxasset://textures/ui/Controls/xboxY.png",
	[Enum.KeyCode.ButtonA] = "rbxasset://textures/ui/Controls/xboxA.png",
	[Enum.KeyCode.ButtonB] = "rbxasset://textures/ui/Controls/xboxB.png",
	[Enum.KeyCode.DPadLeft] = "rbxasset://textures/ui/Controls/dpadLeft.png",
	[Enum.KeyCode.DPadRight] = "rbxasset://textures/ui/Controls/dpadRight.png",
	[Enum.KeyCode.DPadUp] = "rbxasset://textures/ui/Controls/dpadUp.png",
	[Enum.KeyCode.DPadDown] = "rbxasset://textures/ui/Controls/dpadDown.png",
	[Enum.KeyCode.ButtonSelect] = "rbxasset://textures/ui/Controls/xboxView.png",
	[Enum.KeyCode.ButtonStart] = "rbxasset://textures/ui/Controls/xboxmenu.png",
	[Enum.KeyCode.ButtonL1] = "rbxasset://textures/ui/Controls/xboxLB.png",
	[Enum.KeyCode.ButtonR1] = "rbxasset://textures/ui/Controls/xboxRB.png",
	[Enum.KeyCode.ButtonL2] = "rbxasset://textures/ui/Controls/xboxLT.png",
	[Enum.KeyCode.ButtonR2] = "rbxasset://textures/ui/Controls/xboxRT.png",
	[Enum.KeyCode.ButtonL3] = "rbxasset://textures/ui/Controls/xboxLS.png",
	[Enum.KeyCode.ButtonR3] = "rbxasset://textures/ui/Controls/xboxRS.png",
	[Enum.KeyCode.Thumbstick1] = "rbxasset://textures/ui/Controls/xboxLSDirectional.png",
	[Enum.KeyCode.Thumbstick2] = "rbxasset://textures/ui/Controls/xboxRSDirectional.png",
}


-- Constants
local GAMEPAD_BUTTON_IMAGE = require(script.GamepadButtonImage)

return function(Scope : any, prompt, Config : {})
    if not GAMEPAD_BUTTON_IMAGE[prompt.GamepadKeyCode] then
        return
    end

    return {
        Scope:New("ImageLabel", {
            Name = "ButtonImage",
            AnchorPoint = Vector2.new(0.5, 0.5),
            Size = UDim2.fromOffset(24, 24),
            Position = UDim2.fromScale(0.5, 0.5),
            BackgroundTransparency = 1,
            ImageTransparency = 1,
            Image = GAMEPAD_BUTTON_IMAGE[prompt.GamepadKeyCode],
            ImageColor3 = Config.TextColor,
        })
    }
end


return {
	[Enum.KeyCode.LeftControl] = "Ctrl",
	[Enum.KeyCode.RightControl] = "Ctrl",
	[Enum.KeyCode.LeftAlt] = "Alt",
	[Enum.KeyCode.RightAlt] = "Alt",
	[Enum.KeyCode.F1] = "F1",
	[Enum.KeyCode.F2] = "F2",
	[Enum.KeyCode.F3] = "F3",
	[Enum.KeyCode.F4] = "F4",
	[Enum.KeyCode.F5] = "F5",
	[Enum.KeyCode.F6] = "F6",
	[Enum.KeyCode.F7] = "F7",
	[Enum.KeyCode.F8] = "F8",
	[Enum.KeyCode.F9] = "F9",
	[Enum.KeyCode.F10] = "F10",
	[Enum.KeyCode.F11] = "F11",
	[Enum.KeyCode.F12] = "F12",
}


return {
	["'"] = "rbxasset://textures/ui/Controls/apostrophe.png",
	[","] = "rbxasset://textures/ui/Controls/comma.png",
	["`"] = "rbxasset://textures/ui/Controls/graveaccent.png",
	["."] = "rbxasset://textures/ui/Controls/period.png",
	[" "] = "rbxasset://textures/ui/Controls/spacebar.png",
}


return {
	[Enum.KeyCode.Backspace] = "rbxasset://textures/ui/Controls/backspace.png",
	[Enum.KeyCode.Return] = "rbxasset://textures/ui/Controls/return.png",
	[Enum.KeyCode.LeftShift] = "rbxasset://textures/ui/Controls/shift.png",
	[Enum.KeyCode.RightShift] = "rbxasset://textures/ui/Controls/shift.png",
	[Enum.KeyCode.Tab] = "rbxasset://textures/ui/Controls/tab.png",
}