<roblox version="4">
  <Item class="ReplicatedStorage" referent="0">
    <Properties>
      <string name="Name">ReplicatedStorage</string>
    </Properties>
    <Item class="Folder" referent="1">
      <Properties>
        <string name="Name">Packages</string>
      </Properties>
      <Item class="ModuleScript" referent="2">
        <Properties>
          <string name="Name">Knit</string>
          <string name="Source"><![CDATA[return require(script.Parent._Index["sleitnick_knit@1.7.0"]["knit"])
]]></string>
        </Properties>
      </Item>
      <Item class="ModuleScript" referent="3">
        <Properties>
          <string name="Name">ProfileService</string>
          <string name="Source"><![CDATA[return require(script.Parent._Index["jakebball_profileservice@1.5.0"]["profileservice"])
]]></string>
        </Properties>
      </Item>
      <Item class="ModuleScript" referent="4">
        <Properties>
          <string name="Name">Promise</string>
          <string name="Source"><![CDATA[return require(script.Parent._Index["evaera_promise@4.0.0"]["promise"])
]]></string>
        </Properties>
      </Item>
      <Item class="ModuleScript" referent="5">
        <Properties>
          <string name="Name">Signal</string>
          <string name="Source"><![CDATA[return require(script.Parent._Index["sleitnick_signal@2.0.3"]["signal"])
]]></string>
        </Properties>
      </Item>
      <Item class="ModuleScript" referent="6">
        <Properties>
          <string name="Name">TableUtil</string>
          <string name="Source"><![CDATA[return require(script.Parent._Index["sleitnick_table-util@1.2.1"]["table-util"])
]]></string>
        </Properties>
      </Item>
      <Item class="Folder" referent="7">
        <Properties>
          <string name="Name">_Index</string>
        </Properties>
        <Item class="Folder" referent="8">
          <Properties>
            <string name="Name">evaera_promise@4.0.0</string>
          </Properties>
          <Item class="ModuleScript" referent="9">
            <Properties>
              <string name="Name">promise</string>
              <string name="Source"><![CDATA[--[[
	An implementation of Promises similar to Promise/A+.
]]

local ERROR_NON_PROMISE_IN_LIST = "Non-promise value passed into %s at index %s"
local ERROR_NON_LIST = "Please pass a list of promises to %s"
local ERROR_NON_FUNCTION = "Please pass a handler function to %s!"
local MODE_KEY_METATABLE = { __mode = "k" }

local function isCallable(value)
	if type(value) == "function" then
		return true
	end

	if type(value) == "table" then
		local metatable = getmetatable(value)
		if metatable and type(rawget(metatable, "__call")) == "function" then
			return true
		end
	end

	return false
end

--[[
	Creates an enum dictionary with some metamethods to prevent common mistakes.
]]
local function makeEnum(enumName, members)
	local enum = {}

	for _, memberName in ipairs(members) do
		enum[memberName] = memberName
	end

	return setmetatable(enum, {
		__index = function(_, k)
			error(string.format("%s is not in %s!", k, enumName), 2)
		end,
		__newindex = function()
			error(string.format("Creating new members in %s is not allowed!", enumName), 2)
		end,
	})
end

--[=[
	An object to represent runtime errors that occur during execution.
	Promises that experience an error like this will be rejected with
	an instance of this object.

	@class Error
]=]
local Error
do
	Error = {
		Kind = makeEnum("Promise.Error.Kind", {
			"ExecutionError",
			"AlreadyCancelled",
			"NotResolvedInTime",
			"TimedOut",
		}),
	}
	Error.__index = Error

	function Error.new(options, parent)
		options = options or {}
		return setmetatable({
			error = tostring(options.error) or "[This error has no error text.]",
			trace = options.trace,
			context = options.context,
			kind = options.kind,
			parent = parent,
			createdTick = os.clock(),
			createdTrace = debug.traceback(),
		}, Error)
	end

	function Error.is(anything)
		if type(anything) == "table" then
			local metatable = getmetatable(anything)

			if type(metatable) == "table" then
				return rawget(anything, "error") ~= nil and type(rawget(metatable, "extend")) == "function"
			end
		end

		return false
	end

	function Error.isKind(anything, kind)
		assert(kind ~= nil, "Argument #2 to Promise.Error.isKind must not be nil")

		return Error.is(anything) and anything.kind == kind
	end

	function Error:extend(options)
		options = options or {}

		options.kind = options.kind or self.kind

		return Error.new(options, self)
	end

	function Error:getErrorChain()
		local runtimeErrors = { self }

		while runtimeErrors[#runtimeErrors].parent do
			table.insert(runtimeErrors, runtimeErrors[#runtimeErrors].parent)
		end

		return runtimeErrors
	end

	function Error:__tostring()
		local errorStrings = {
			string.format("-- Promise.Error(%s) --", self.kind or "?"),
		}

		for _, runtimeError in ipairs(self:getErrorChain()) do
			table.insert(
				errorStrings,
				table.concat({
					runtimeError.trace or runtimeError.error,
					runtimeError.context,
				}, "\n")
			)
		end

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

--[[
	Packs a number of arguments into a table and returns its length.

	Used to cajole varargs without dropping sparse values.
]]
local function pack(...)
	return select("#", ...), { ... }
end

--[[
	Returns first value (success), and packs all following values.
]]
local function packResult(success, ...)
	return success, select("#", ...), { ... }
end

local function makeErrorHandler(traceback)
	assert(traceback ~= nil, "traceback is nil")

	return function(err)
		-- If the error object is already a table, forward it directly.
		-- Should we extend the error here and add our own trace?

		if type(err) == "table" then
			return err
		end

		return Error.new({
			error = err,
			kind = Error.Kind.ExecutionError,
			trace = debug.traceback(tostring(err), 2),
			context = "Promise created at:\n\n" .. traceback,
		})
	end
end

--[[
	Calls a Promise executor with error handling.
]]
local function runExecutor(traceback, callback, ...)
	return packResult(xpcall(callback, makeErrorHandler(traceback), ...))
end

--[[
	Creates a function that invokes a callback with correct error handling and
	resolution mechanisms.
]]
local function createAdvancer(traceback, callback, resolve, reject)
	return function(...)
		local ok, resultLength, result = runExecutor(traceback, callback, ...)

		if ok then
			resolve(unpack(result, 1, resultLength))
		else
			reject(result[1])
		end
	end
end

local function isEmpty(t)
	return next(t) == nil
end

--[=[
	An enum value used to represent the Promise's status.
	@interface Status
	@tag enum
	@within Promise
	.Started "Started" -- The Promise is executing, and not settled yet.
	.Resolved "Resolved" -- The Promise finished successfully.
	.Rejected "Rejected" -- The Promise was rejected.
	.Cancelled "Cancelled" -- The Promise was cancelled before it finished.
]=]
--[=[
	@prop Status Status
	@within Promise
	@readonly
	@tag enums
	A table containing all members of the `Status` enum, e.g., `Promise.Status.Resolved`.
]=]
--[=[
	A Promise is an object that represents a value that will exist in the future, but doesn't right now.
	Promises allow you to then attach callbacks that can run once the value becomes available (known as *resolving*),
	or if an error has occurred (known as *rejecting*).

	@class Promise
	@__index prototype
]=]
local Promise = {
	Error = Error,
	Status = makeEnum("Promise.Status", { "Started", "Resolved", "Rejected", "Cancelled" }),
	_getTime = os.clock,
	_timeEvent = game:GetService("RunService").Heartbeat,
	_unhandledRejectionCallbacks = {},
}
Promise.prototype = {}
Promise.__index = Promise.prototype

function Promise._new(traceback, callback, parent)
	if parent ~= nil and not Promise.is(parent) then
		error("Argument #2 to Promise.new must be a promise or nil", 2)
	end

	local self = {
		-- The executor thread.
		_thread = nil,

		-- Used to locate where a promise was created
		_source = traceback,

		_status = Promise.Status.Started,

		-- A table containing a list of all results, whether success or failure.
		-- Only valid if _status is set to something besides Started
		_values = nil,

		-- Lua doesn't like sparse arrays very much, so we explicitly store the
		-- length of _values to handle middle nils.
		_valuesLength = -1,

		-- Tracks if this Promise has no error observers..
		_unhandledRejection = true,

		-- Queues representing functions we should invoke when we update!
		_queuedResolve = {},
		_queuedReject = {},
		_queuedFinally = {},

		-- The function to run when/if this promise is cancelled.
		_cancellationHook = nil,

		-- The "parent" of this promise in a promise chain. Required for
		-- cancellation propagation upstream.
		_parent = parent,

		-- Consumers are Promises that have chained onto this one.
		-- We track them for cancellation propagation downstream.
		_consumers = setmetatable({}, MODE_KEY_METATABLE),
	}

	if parent and parent._status == Promise.Status.Started then
		parent._consumers[self] = true
	end

	setmetatable(self, Promise)

	local function resolve(...)
		self:_resolve(...)
	end

	local function reject(...)
		self:_reject(...)
	end

	local function onCancel(cancellationHook)
		if cancellationHook then
			if self._status == Promise.Status.Cancelled then
				cancellationHook()
			else
				self._cancellationHook = cancellationHook
			end
		end

		return self._status == Promise.Status.Cancelled
	end

	self._thread = coroutine.create(function()
		local ok, _, result = runExecutor(self._source, callback, resolve, reject, onCancel)

		if not ok then
			reject(result[1])
		end
	end)

	task.spawn(self._thread)

	return self
end

--[=[
	Construct a new Promise that will be resolved or rejected with the given callbacks.

	If you `resolve` with a Promise, it will be chained onto.

	You can safely yield within the executor function and it will not block the creating thread.

	```lua
	local myFunction()
		return Promise.new(function(resolve, reject, onCancel)
			wait(1)
			resolve("Hello world!")
		end)
	end

	myFunction():andThen(print)
	```

	You do not need to use `pcall` within a Promise. Errors that occur during execution will be caught and turned into a rejection automatically. If `error()` is called with a table, that table will be the rejection value. Otherwise, string errors will be converted into `Promise.Error(Promise.Error.Kind.ExecutionError)` objects for tracking debug information.

	You may register an optional cancellation hook by using the `onCancel` argument:

	* This should be used to abort any ongoing operations leading up to the promise being settled.
	* Call the `onCancel` function with a function callback as its only argument to set a hook which will in turn be called when/if the promise is cancelled.
	* `onCancel` returns `true` if the Promise was already cancelled when you called `onCancel`.
	* Calling `onCancel` with no argument will not override a previously set cancellation hook, but it will still return `true` if the Promise is currently cancelled.
	* You can set the cancellation hook at any time before resolving.
	* When a promise is cancelled, calls to `resolve` or `reject` will be ignored, regardless of if you set a cancellation hook or not.

	:::caution
	If the Promise is cancelled, the `executor` thread is closed with `coroutine.close` after the cancellation hook is called.

	You must perform any cleanup code in the cancellation hook: any time your executor yields, it **may never resume**.
	:::

	@param executor (resolve: (...: any) -> (), reject: (...: any) -> (), onCancel: (abortHandler?: () -> ()) -> boolean) -> ()
	@return Promise
]=]
function Promise.new(executor)
	return Promise._new(debug.traceback(nil, 2), executor)
end

function Promise:__tostring()
	return string.format("Promise(%s)", self._status)
end

--[=[
	The same as [Promise.new](/api/Promise#new), except execution begins after the next `Heartbeat` event.

	This is a spiritual replacement for `spawn`, but it does not suffer from the same [issues](https://eryn.io/gist/3db84579866c099cdd5bb2ff37947cec) as `spawn`.

	```lua
	local function waitForChild(instance, childName, timeout)
	  return Promise.defer(function(resolve, reject)
		local child = instance:WaitForChild(childName, timeout)

		;(child and resolve or reject)(child)
	  end)
	end
	```

	@param executor (resolve: (...: any) -> (), reject: (...: any) -> (), onCancel: (abortHandler?: () -> ()) -> boolean) -> ()
	@return Promise
]=]
function Promise.defer(executor)
	local traceback = debug.traceback(nil, 2)
	local promise
	promise = Promise._new(traceback, function(resolve, reject, onCancel)
		local connection
		connection = Promise._timeEvent:Connect(function()
			connection:Disconnect()
			local ok, _, result = runExecutor(traceback, executor, resolve, reject, onCancel)

			if not ok then
				reject(result[1])
			end
		end)
	end)

	return promise
end

-- Backwards compatibility
Promise.async = Promise.defer

--[=[
	Creates an immediately resolved Promise with the given value.

	```lua
	-- Example using Promise.resolve to deliver cached values:
	function getSomething(name)
		if cache[name] then
			return Promise.resolve(cache[name])
		else
			return Promise.new(function(resolve, reject)
				local thing = getTheThing()
				cache[name] = thing

				resolve(thing)
			end)
		end
	end
	```

	@param ... any
	@return Promise<...any>
]=]
function Promise.resolve(...)
	local length, values = pack(...)
	return Promise._new(debug.traceback(nil, 2), function(resolve)
		resolve(unpack(values, 1, length))
	end)
end

--[=[
	Creates an immediately rejected Promise with the given value.

	:::caution
	Something needs to consume this rejection (i.e. `:catch()` it), otherwise it will emit an unhandled Promise rejection warning on the next frame. Thus, you should not create and store rejected Promises for later use. Only create them on-demand as needed.
	:::

	@param ... any
	@return Promise<...any>
]=]
function Promise.reject(...)
	local length, values = pack(...)
	return Promise._new(debug.traceback(nil, 2), function(_, reject)
		reject(unpack(values, 1, length))
	end)
end

--[[
	Runs a non-promise-returning function as a Promise with the
  given arguments.
]]
function Promise._try(traceback, callback, ...)
	local valuesLength, values = pack(...)

	return Promise._new(traceback, function(resolve)
		resolve(callback(unpack(values, 1, valuesLength)))
	end)
end

--[=[
	Begins a Promise chain, calling a function and returning a Promise resolving with its return value. If the function errors, the returned Promise will be rejected with the error. You can safely yield within the Promise.try callback.

	:::info
	`Promise.try` is similar to [Promise.promisify](#promisify), except the callback is invoked immediately instead of returning a new function.
	:::

	```lua
	Promise.try(function()
		return math.random(1, 2) == 1 and "ok" or error("Oh an error!")
	end)
		:andThen(function(text)
			print(text)
		end)
		:catch(function(err)
			warn("Something went wrong")
		end)
	```

	@param callback (...: T...) -> ...any
	@param ... T... -- Additional arguments passed to `callback`
	@return Promise
]=]
function Promise.try(callback, ...)
	return Promise._try(debug.traceback(nil, 2), callback, ...)
end

--[[
	Returns a new promise that:
		* is resolved when all input promises resolve
		* is rejected if ANY input promises reject
]]
function Promise._all(traceback, promises, amount)
	if type(promises) ~= "table" then
		error(string.format(ERROR_NON_LIST, "Promise.all"), 3)
	end

	-- We need to check that each value is a promise here so that we can produce
	-- a proper error rather than a rejected promise with our error.
	for i, promise in pairs(promises) do
		if not Promise.is(promise) then
			error(string.format(ERROR_NON_PROMISE_IN_LIST, "Promise.all", tostring(i)), 3)
		end
	end

	-- If there are no values then return an already resolved promise.
	if #promises == 0 or amount == 0 then
		return Promise.resolve({})
	end

	return Promise._new(traceback, function(resolve, reject, onCancel)
		-- An array to contain our resolved values from the given promises.
		local resolvedValues = {}
		local newPromises = {}

		-- Keep a count of resolved promises because just checking the resolved
		-- values length wouldn't account for promises that resolve with nil.
		local resolvedCount = 0
		local rejectedCount = 0
		local done = false

		local function cancel()
			for _, promise in ipairs(newPromises) do
				promise:cancel()
			end
		end

		-- Called when a single value is resolved and resolves if all are done.
		local function resolveOne(i, ...)
			if done then
				return
			end

			resolvedCount = resolvedCount + 1

			if amount == nil then
				resolvedValues[i] = ...
			else
				resolvedValues[resolvedCount] = ...
			end

			if resolvedCount >= (amount or #promises) then
				done = true
				resolve(resolvedValues)
				cancel()
			end
		end

		onCancel(cancel)

		-- We can assume the values inside `promises` are all promises since we
		-- checked above.
		for i, promise in ipairs(promises) do
			newPromises[i] = promise:andThen(function(...)
				resolveOne(i, ...)
			end, function(...)
				rejectedCount = rejectedCount + 1

				if amount == nil or #promises - rejectedCount < amount then
					cancel()
					done = true

					reject(...)
				end
			end)
		end

		if done then
			cancel()
		end
	end)
end

--[=[
	Accepts an array of Promises and returns a new promise that:
	* is resolved after all input promises resolve.
	* is rejected if *any* input promises reject.

	:::info
	Only the first return value from each promise will be present in the resulting array.
	:::

	After any input Promise rejects, all other input Promises that are still pending will be cancelled if they have no other consumers.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.all(promises)
	```

	@param promises {Promise<T>}
	@return Promise<{T}>
]=]
function Promise.all(promises)
	return Promise._all(debug.traceback(nil, 2), promises)
end

--[=[
	Folds an array of values or promises into a single value. The array is traversed sequentially.

	The reducer function can return a promise or value directly. Each iteration receives the resolved value from the previous, and the first receives your defined initial value.

	The folding will stop at the first rejection encountered.
	```lua
	local basket = {"blueberry", "melon", "pear", "melon"}
	Promise.fold(basket, function(cost, fruit)
		if fruit == "blueberry" then
			return cost -- blueberries are free!
		else
			-- call a function that returns a promise with the fruit price
			return fetchPrice(fruit):andThen(function(fruitCost)
				return cost + fruitCost
			end)
		end
	end, 0)
	```

	@since v3.1.0
	@param list {T | Promise<T>}
	@param reducer (accumulator: U, value: T, index: number) -> U | Promise<U>
	@param initialValue U
]=]
function Promise.fold(list, reducer, initialValue)
	assert(type(list) == "table", "Bad argument #1 to Promise.fold: must be a table")
	assert(isCallable(reducer), "Bad argument #2 to Promise.fold: must be a function")

	local accumulator = Promise.resolve(initialValue)
	return Promise.each(list, function(resolvedElement, i)
		accumulator = accumulator:andThen(function(previousValueResolved)
			return reducer(previousValueResolved, resolvedElement, i)
		end)
	end):andThen(function()
		return accumulator
	end)
end

--[=[
	Accepts an array of Promises and returns a Promise that is resolved as soon as `count` Promises are resolved from the input array. The resolved array values are in the order that the Promises resolved in. When this Promise resolves, all other pending Promises are cancelled if they have no other consumers.

	`count` 0 results in an empty array. The resultant array will never have more than `count` elements.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.some(promises, 2) -- Only resolves with first 2 promises to resolve
	```

	@param promises {Promise<T>}
	@param count number
	@return Promise<{T}>
]=]
function Promise.some(promises, count)
	assert(type(count) == "number", "Bad argument #2 to Promise.some: must be a number")

	return Promise._all(debug.traceback(nil, 2), promises, count)
end

--[=[
	Accepts an array of Promises and returns a Promise that is resolved as soon as *any* of the input Promises resolves. It will reject only if *all* input Promises reject. As soon as one Promises resolves, all other pending Promises are cancelled if they have no other consumers.

	Resolves directly with the value of the first resolved Promise. This is essentially [[Promise.some]] with `1` count, except the Promise resolves with the value directly instead of an array with one element.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.any(promises) -- Resolves with first value to resolve (only rejects if all 3 rejected)
	```

	@param promises {Promise<T>}
	@return Promise<T>
]=]
function Promise.any(promises)
	return Promise._all(debug.traceback(nil, 2), promises, 1):andThen(function(values)
		return values[1]
	end)
end

--[=[
	Accepts an array of Promises and returns a new Promise that resolves with an array of in-place Statuses when all input Promises have settled. This is equivalent to mapping `promise:finally` over the array of Promises.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.allSettled(promises)
	```

	@param promises {Promise<T>}
	@return Promise<{Status}>
]=]
function Promise.allSettled(promises)
	if type(promises) ~= "table" then
		error(string.format(ERROR_NON_LIST, "Promise.allSettled"), 2)
	end

	-- We need to check that each value is a promise here so that we can produce
	-- a proper error rather than a rejected promise with our error.
	for i, promise in pairs(promises) do
		if not Promise.is(promise) then
			error(string.format(ERROR_NON_PROMISE_IN_LIST, "Promise.allSettled", tostring(i)), 2)
		end
	end

	-- If there are no values then return an already resolved promise.
	if #promises == 0 then
		return Promise.resolve({})
	end

	return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
		-- An array to contain our resolved values from the given promises.
		local fates = {}
		local newPromises = {}

		-- Keep a count of resolved promises because just checking the resolved
		-- values length wouldn't account for promises that resolve with nil.
		local finishedCount = 0

		-- Called when a single value is resolved and resolves if all are done.
		local function resolveOne(i, ...)
			finishedCount = finishedCount + 1

			fates[i] = ...

			if finishedCount >= #promises then
				resolve(fates)
			end
		end

		onCancel(function()
			for _, promise in ipairs(newPromises) do
				promise:cancel()
			end
		end)

		-- We can assume the values inside `promises` are all promises since we
		-- checked above.
		for i, promise in ipairs(promises) do
			newPromises[i] = promise:finally(function(...)
				resolveOne(i, ...)
			end)
		end
	end)
end

--[=[
	Accepts an array of Promises and returns a new promise that is resolved or rejected as soon as any Promise in the array resolves or rejects.

	:::warning
	If the first Promise to settle from the array settles with a rejection, the resulting Promise from `race` will reject.

	If you instead want to tolerate rejections, and only care about at least one Promise resolving, you should use [Promise.any](#any) or [Promise.some](#some) instead.
	:::

	All other Promises that don't win the race will be cancelled if they have no other consumers.

	```lua
	local promises = {
		returnsAPromise("example 1"),
		returnsAPromise("example 2"),
		returnsAPromise("example 3"),
	}

	return Promise.race(promises) -- Only returns 1st value to resolve or reject
	```

	@param promises {Promise<T>}
	@return Promise<T>
]=]
function Promise.race(promises)
	assert(type(promises) == "table", string.format(ERROR_NON_LIST, "Promise.race"))

	for i, promise in pairs(promises) do
		assert(Promise.is(promise), string.format(ERROR_NON_PROMISE_IN_LIST, "Promise.race", tostring(i)))
	end

	return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
		local newPromises = {}
		local finished = false

		local function cancel()
			for _, promise in ipairs(newPromises) do
				promise:cancel()
			end
		end

		local function finalize(callback)
			return function(...)
				cancel()
				finished = true
				return callback(...)
			end
		end

		if onCancel(finalize(reject)) then
			return
		end

		for i, promise in ipairs(promises) do
			newPromises[i] = promise:andThen(finalize(resolve), finalize(reject))
		end

		if finished then
			cancel()
		end
	end)
end

--[=[
	Iterates serially over the given an array of values, calling the predicate callback on each value before continuing.

	If the predicate returns a Promise, we wait for that Promise to resolve before moving on to the next item
	in the array.

	:::info
	`Promise.each` is similar to `Promise.all`, except the Promises are ran in order instead of all at once.

	But because Promises are eager, by the time they are created, they're already running. Thus, we need a way to defer creation of each Promise until a later time.

	The predicate function exists as a way for us to operate on our data instead of creating a new closure for each Promise. If you would prefer, you can pass in an array of functions, and in the predicate, call the function and return its return value.
	:::

	```lua
	Promise.each({
		"foo",
		"bar",
		"baz",
		"qux"
	}, function(value, index)
		return Promise.delay(1):andThen(function()
		print(("%d) Got %s!"):format(index, value))
		end)
	end)

	--[[
		(1 second passes)
		> 1) Got foo!
		(1 second passes)
		> 2) Got bar!
		(1 second passes)
		> 3) Got baz!
		(1 second passes)
		> 4) Got qux!
	]]
	```

	If the Promise a predicate returns rejects, the Promise from `Promise.each` is also rejected with the same value.

	If the array of values contains a Promise, when we get to that point in the list, we wait for the Promise to resolve before calling the predicate with the value.

	If a Promise in the array of values is already Rejected when `Promise.each` is called, `Promise.each` rejects with that value immediately (the predicate callback will never be called even once). If a Promise in the list is already Cancelled when `Promise.each` is called, `Promise.each` rejects with `Promise.Error(Promise.Error.Kind.AlreadyCancelled`). If a Promise in the array of values is Started at first, but later rejects, `Promise.each` will reject with that value and iteration will not continue once iteration encounters that value.

	Returns a Promise containing an array of the returned/resolved values from the predicate for each item in the array of values.

	If this Promise returned from `Promise.each` rejects or is cancelled for any reason, the following are true:
	- Iteration will not continue.
	- Any Promises within the array of values will now be cancelled if they have no other consumers.
	- The Promise returned from the currently active predicate will be cancelled if it hasn't resolved yet.

	@since 3.0.0
	@param list {T | Promise<T>}
	@param predicate (value: T, index: number) -> U | Promise<U>
	@return Promise<{U}>
]=]
function Promise.each(list, predicate)
	assert(type(list) == "table", string.format(ERROR_NON_LIST, "Promise.each"))
	assert(isCallable(predicate), string.format(ERROR_NON_FUNCTION, "Promise.each"))

	return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
		local results = {}
		local promisesToCancel = {}

		local cancelled = false

		local function cancel()
			for _, promiseToCancel in ipairs(promisesToCancel) do
				promiseToCancel:cancel()
			end
		end

		onCancel(function()
			cancelled = true

			cancel()
		end)

		-- We need to preprocess the list of values and look for Promises.
		-- If we find some, we must register our andThen calls now, so that those Promises have a consumer
		-- from us registered. If we don't do this, those Promises might get cancelled by something else
		-- before we get to them in the series because it's not possible to tell that we plan to use it
		-- unless we indicate it here.

		local preprocessedList = {}

		for index, value in ipairs(list) do
			if Promise.is(value) then
				if value:getStatus() == Promise.Status.Cancelled then
					cancel()
					return reject(Error.new({
						error = "Promise is cancelled",
						kind = Error.Kind.AlreadyCancelled,
						context = string.format(
							"The Promise that was part of the array at index %d passed into Promise.each was already cancelled when Promise.each began.\n\nThat Promise was created at:\n\n%s",
							index,
							value._source
						),
					}))
				elseif value:getStatus() == Promise.Status.Rejected then
					cancel()
					return reject(select(2, value:await()))
				end

				-- Chain a new Promise from this one so we only cancel ours
				local ourPromise = value:andThen(function(...)
					return ...
				end)

				table.insert(promisesToCancel, ourPromise)
				preprocessedList[index] = ourPromise
			else
				preprocessedList[index] = value
			end
		end

		for index, value in ipairs(preprocessedList) do
			if Promise.is(value) then
				local success
				success, value = value:await()

				if not success then
					cancel()
					return reject(value)
				end
			end

			if cancelled then
				return
			end

			local predicatePromise = Promise.resolve(predicate(value, index))

			table.insert(promisesToCancel, predicatePromise)

			local success, result = predicatePromise:await()

			if not success then
				cancel()
				return reject(result)
			end

			results[index] = result
		end

		resolve(results)
	end)
end

--[=[
	Checks whether the given object is a Promise via duck typing. This only checks if the object is a table and has an `andThen` method.

	@param object any
	@return boolean -- `true` if the given `object` is a Promise.
]=]
function Promise.is(object)
	if type(object) ~= "table" then
		return false
	end

	local objectMetatable = getmetatable(object)

	if objectMetatable == Promise then
		-- The Promise came from this library.
		return true
	elseif objectMetatable == nil then
		-- No metatable, but we should still chain onto tables with andThen methods
		return isCallable(object.andThen)
	elseif
		type(objectMetatable) == "table"
		and type(rawget(objectMetatable, "__index")) == "table"
		and isCallable(rawget(rawget(objectMetatable, "__index"), "andThen"))
	then
		-- Maybe this came from a different or older Promise library.
		return true
	end

	return false
end

--[=[
	Wraps a function that yields into one that returns a Promise.

	Any errors that occur while executing the function will be turned into rejections.

	:::info
	`Promise.promisify` is similar to [Promise.try](#try), except the callback is returned as a callable function instead of being invoked immediately.
	:::

	```lua
	local sleep = Promise.promisify(wait)

	sleep(1):andThen(print)
	```

	```lua
	local isPlayerInGroup = Promise.promisify(function(player, groupId)
		return player:IsInGroup(groupId)
	end)
	```

	@param callback (...: any) -> ...any
	@return (...: any) -> Promise
]=]
function Promise.promisify(callback)
	return function(...)
		return Promise._try(debug.traceback(nil, 2), callback, ...)
	end
end

--[=[
	Returns a Promise that resolves after `seconds` seconds have passed. The Promise resolves with the actual amount of time that was waited.

	This function is **not** a wrapper around `wait`. `Promise.delay` uses a custom scheduler which provides more accurate timing. As an optimization, cancelling this Promise instantly removes the task from the scheduler.

	:::warning
	Passing `NaN`, infinity, or a number less than 1/60 is equivalent to passing 1/60.
	:::

	```lua
		Promise.delay(5):andThenCall(print, "This prints after 5 seconds")
	```

	@function delay
	@within Promise
	@param seconds number
	@return Promise<number>
]=]
do
	-- uses a sorted doubly linked list (queue) to achieve O(1) remove operations and O(n) for insert

	-- the initial node in the linked list
	local first
	local connection

	function Promise.delay(seconds)
		assert(type(seconds) == "number", "Bad argument #1 to Promise.delay, must be a number.")
		-- If seconds is -INF, INF, NaN, or less than 1 / 60, assume seconds is 1 / 60.
		-- This mirrors the behavior of wait()
		if not (seconds >= 1 / 60) or seconds == math.huge then
			seconds = 1 / 60
		end

		return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
			local startTime = Promise._getTime()
			local endTime = startTime + seconds

			local node = {
				resolve = resolve,
				startTime = startTime,
				endTime = endTime,
			}

			if connection == nil then -- first is nil when connection is nil
				first = node
				connection = Promise._timeEvent:Connect(function()
					local threadStart = Promise._getTime()

					while first ~= nil and first.endTime < threadStart do
						local current = first
						first = current.next

						if first == nil then
							connection:Disconnect()
							connection = nil
						else
							first.previous = nil
						end

						current.resolve(Promise._getTime() - current.startTime)
					end
				end)
			else -- first is non-nil
				if first.endTime < endTime then -- if `node` should be placed after `first`
					-- we will insert `node` between `current` and `next`
					-- (i.e. after `current` if `next` is nil)
					local current = first
					local next = current.next

					while next ~= nil and next.endTime < endTime do
						current = next
						next = current.next
					end

					-- `current` must be non-nil, but `next` could be `nil` (i.e. last item in list)
					current.next = node
					node.previous = current

					if next ~= nil then
						node.next = next
						next.previous = node
					end
				else
					-- set `node` to `first`
					node.next = first
					first.previous = node
					first = node
				end
			end

			onCancel(function()
				-- remove node from queue
				local next = node.next

				if first == node then
					if next == nil then -- if `node` is the first and last
						connection:Disconnect()
						connection = nil
					else -- if `node` is `first` and not the last
						next.previous = nil
					end
					first = next
				else
					local previous = node.previous
					-- since `node` is not `first`, then we know `previous` is non-nil
					previous.next = next

					if next ~= nil then
						next.previous = previous
					end
				end
			end)
		end)
	end
end

--[=[
	Returns a new Promise that resolves if the chained Promise resolves within `seconds` seconds, or rejects if execution time exceeds `seconds`. The chained Promise will be cancelled if the timeout is reached.

	Rejects with `rejectionValue` if it is non-nil. If a `rejectionValue` is not given, it will reject with a `Promise.Error(Promise.Error.Kind.TimedOut)`. This can be checked with [[Error.isKind]].

	```lua
	getSomething():timeout(5):andThen(function(something)
		-- got something and it only took at max 5 seconds
	end):catch(function(e)
		-- Either getting something failed or the time was exceeded.

		if Promise.Error.isKind(e, Promise.Error.Kind.TimedOut) then
			warn("Operation timed out!")
		else
			warn("Operation encountered an error!")
		end
	end)
	```

	Sugar for:

	```lua
	Promise.race({
		Promise.delay(seconds):andThen(function()
			return Promise.reject(
				rejectionValue == nil
				and Promise.Error.new({ kind = Promise.Error.Kind.TimedOut })
				or rejectionValue
			)
		end),
		promise
	})
	```

	@param seconds number
	@param rejectionValue? any -- The value to reject with if the timeout is reached
	@return Promise
]=]
function Promise.prototype:timeout(seconds, rejectionValue)
	local traceback = debug.traceback(nil, 2)

	return Promise.race({
		Promise.delay(seconds):andThen(function()
			return Promise.reject(rejectionValue == nil and Error.new({
				kind = Error.Kind.TimedOut,
				error = "Timed out",
				context = string.format(
					"Timeout of %d seconds exceeded.\n:timeout() called at:\n\n%s",
					seconds,
					traceback
				),
			}) or rejectionValue)
		end),
		self,
	})
end

--[=[
	Returns the current Promise status.

	@return Status
]=]
function Promise.prototype:getStatus()
	return self._status
end

--[[
	Creates a new promise that receives the result of this promise.

	The given callbacks are invoked depending on that result.
]]
function Promise.prototype:_andThen(traceback, successHandler, failureHandler)
	self._unhandledRejection = false

	-- If we are already cancelled, we return a cancelled Promise
	if self._status == Promise.Status.Cancelled then
		local promise = Promise.new(function() end)
		promise:cancel()

		return promise
	end

	-- Create a new promise to follow this part of the chain
	return Promise._new(traceback, function(resolve, reject, onCancel)
		-- Our default callbacks just pass values onto the next promise.
		-- This lets success and failure cascade correctly!

		local successCallback = resolve
		if successHandler then
			successCallback = createAdvancer(traceback, successHandler, resolve, reject)
		end

		local failureCallback = reject
		if failureHandler then
			failureCallback = createAdvancer(traceback, failureHandler, resolve, reject)
		end

		if self._status == Promise.Status.Started then
			-- If we haven't resolved yet, put ourselves into the queue
			table.insert(self._queuedResolve, successCallback)
			table.insert(self._queuedReject, failureCallback)

			onCancel(function()
				-- These are guaranteed to exist because the cancellation handler is guaranteed to only
				-- be called at most once
				if self._status == Promise.Status.Started then
					table.remove(self._queuedResolve, table.find(self._queuedResolve, successCallback))
					table.remove(self._queuedReject, table.find(self._queuedReject, failureCallback))
				end
			end)
		elseif self._status == Promise.Status.Resolved then
			-- This promise has already resolved! Trigger success immediately.
			successCallback(unpack(self._values, 1, self._valuesLength))
		elseif self._status == Promise.Status.Rejected then
			-- This promise died a terrible death! Trigger failure immediately.
			failureCallback(unpack(self._values, 1, self._valuesLength))
		end
	end, self)
end

--[=[
	Chains onto an existing Promise and returns a new Promise.

	:::warning
	Within the failure handler, you should never assume that the rejection value is a string. Some rejections within the Promise library are represented by [[Error]] objects. If you want to treat it as a string for debugging, you should call `tostring` on it first.
	:::

	You can return a Promise from the success or failure handler and it will be chained onto.

	Calling `andThen` on a cancelled Promise returns a cancelled Promise.

	:::tip
	If the Promise returned by `andThen` is cancelled, `successHandler` and `failureHandler` will not run.

	To run code no matter what, use [Promise:finally].
	:::

	@param successHandler (...: any) -> ...any
	@param failureHandler? (...: any) -> ...any
	@return Promise<...any>
]=]
function Promise.prototype:andThen(successHandler, failureHandler)
	assert(successHandler == nil or isCallable(successHandler), string.format(ERROR_NON_FUNCTION, "Promise:andThen"))
	assert(failureHandler == nil or isCallable(failureHandler), string.format(ERROR_NON_FUNCTION, "Promise:andThen"))

	return self:_andThen(debug.traceback(nil, 2), successHandler, failureHandler)
end

--[=[
	Shorthand for `Promise:andThen(nil, failureHandler)`.

	Returns a Promise that resolves if the `failureHandler` worked without encountering an additional error.

	:::warning
	Within the failure handler, you should never assume that the rejection value is a string. Some rejections within the Promise library are represented by [[Error]] objects. If you want to treat it as a string for debugging, you should call `tostring` on it first.
	:::

	Calling `catch` on a cancelled Promise returns a cancelled Promise.

	:::tip
	If the Promise returned by `catch` is cancelled,  `failureHandler` will not run.

	To run code no matter what, use [Promise:finally].
	:::

	@param failureHandler (...: any) -> ...any
	@return Promise<...any>
]=]
function Promise.prototype:catch(failureHandler)
	assert(failureHandler == nil or isCallable(failureHandler), string.format(ERROR_NON_FUNCTION, "Promise:catch"))
	return self:_andThen(debug.traceback(nil, 2), nil, failureHandler)
end

--[=[
	Similar to [Promise.andThen](#andThen), except the return value is the same as the value passed to the handler. In other words, you can insert a `:tap` into a Promise chain without affecting the value that downstream Promises receive.

	```lua
		getTheValue()
		:tap(print)
		:andThen(function(theValue)
			print("Got", theValue, "even though print returns nil!")
		end)
	```

	If you return a Promise from the tap handler callback, its value will be discarded but `tap` will still wait until it resolves before passing the original value through.

	@param tapHandler (...: any) -> ...any
	@return Promise<...any>
]=]
function Promise.prototype:tap(tapHandler)
	assert(isCallable(tapHandler), string.format(ERROR_NON_FUNCTION, "Promise:tap"))
	return self:_andThen(debug.traceback(nil, 2), function(...)
		local callbackReturn = tapHandler(...)

		if Promise.is(callbackReturn) then
			local length, values = pack(...)
			return callbackReturn:andThen(function()
				return unpack(values, 1, length)
			end)
		end

		return ...
	end)
end

--[=[
	Attaches an `andThen` handler to this Promise that calls the given callback with the predefined arguments. The resolved value is discarded.

	```lua
		promise:andThenCall(someFunction, "some", "arguments")
	```

	This is sugar for

	```lua
		promise:andThen(function()
		return someFunction("some", "arguments")
		end)
	```

	@param callback (...: any) -> any
	@param ...? any -- Additional arguments which will be passed to `callback`
	@return Promise
]=]
function Promise.prototype:andThenCall(callback, ...)
	assert(isCallable(callback), string.format(ERROR_NON_FUNCTION, "Promise:andThenCall"))
	local length, values = pack(...)
	return self:_andThen(debug.traceback(nil, 2), function()
		return callback(unpack(values, 1, length))
	end)
end

--[=[
	Attaches an `andThen` handler to this Promise that discards the resolved value and returns the given value from it.

	```lua
		promise:andThenReturn("some", "values")
	```

	This is sugar for

	```lua
		promise:andThen(function()
			return "some", "values"
		end)
	```

	:::caution
	Promises are eager, so if you pass a Promise to `andThenReturn`, it will begin executing before `andThenReturn` is reached in the chain. Likewise, if you pass a Promise created from [[Promise.reject]] into `andThenReturn`, it's possible that this will trigger the unhandled rejection warning. If you need to return a Promise, it's usually best practice to use [[Promise.andThen]].
	:::

	@param ... any -- Values to return from the function
	@return Promise
]=]
function Promise.prototype:andThenReturn(...)
	local length, values = pack(...)
	return self:_andThen(debug.traceback(nil, 2), function()
		return unpack(values, 1, length)
	end)
end

--[=[
	Cancels this promise, preventing the promise from resolving or rejecting. Does not do anything if the promise is already settled.

	Cancellations will propagate upwards and downwards through chained promises.

	Promises will only be cancelled if all of their consumers are also cancelled. This is to say that if you call `andThen` twice on the same promise, and you cancel only one of the child promises, it will not cancel the parent promise until the other child promise is also cancelled.

	```lua
		promise:cancel()
	```
]=]
function Promise.prototype:cancel()
	if self._status ~= Promise.Status.Started then
		return
	end

	self._status = Promise.Status.Cancelled

	if self._cancellationHook then
		self._cancellationHook()
	end

	coroutine.close(self._thread)

	if self._parent then
		self._parent:_consumerCancelled(self)
	end

	for child in pairs(self._consumers) do
		child:cancel()
	end

	self:_finalize()
end

--[[
	Used to decrease the number of consumers by 1, and if there are no more,
	cancel this promise.
]]
function Promise.prototype:_consumerCancelled(consumer)
	if self._status ~= Promise.Status.Started then
		return
	end

	self._consumers[consumer] = nil

	if next(self._consumers) == nil then
		self:cancel()
	end
end

--[[
	Used to set a handler for when the promise resolves, rejects, or is
	cancelled.
]]
function Promise.prototype:_finally(traceback, finallyHandler)
	self._unhandledRejection = false

	local promise = Promise._new(traceback, function(resolve, reject, onCancel)
		local handlerPromise

		onCancel(function()
			-- The finally Promise is not a proper consumer of self. We don't care about the resolved value.
			-- All we care about is running at the end. Therefore, if self has no other consumers, it's safe to
			-- cancel. We don't need to hold out cancelling just because there's a finally handler.
			self:_consumerCancelled(self)

			if handlerPromise then
				handlerPromise:cancel()
			end
		end)

		local finallyCallback = resolve
		if finallyHandler then
			finallyCallback = function(...)
				local callbackReturn = finallyHandler(...)

				if Promise.is(callbackReturn) then
					handlerPromise = callbackReturn

					callbackReturn
						:finally(function(status)
							if status ~= Promise.Status.Rejected then
								resolve(self)
							end
						end)
						:catch(function(...)
							reject(...)
						end)
				else
					resolve(self)
				end
			end
		end

		if self._status == Promise.Status.Started then
			-- The promise is not settled, so queue this.
			table.insert(self._queuedFinally, finallyCallback)
		else
			-- The promise already settled or was cancelled, run the callback now.
			finallyCallback(self._status)
		end
	end)

	return promise
end

--[=[
	Set a handler that will be called regardless of the promise's fate. The handler is called when the promise is
	resolved, rejected, *or* cancelled.

	Returns a new Promise that:
	- resolves with the same values that this Promise resolves with.
	- rejects with the same values that this Promise rejects with.
	- is cancelled if this Promise is cancelled.

	If the value you return from the handler is a Promise:
	- We wait for the Promise to resolve, but we ultimately discard the resolved value.
	- If the returned Promise rejects, the Promise returned from `finally` will reject with the rejected value from the
	*returned* promise.
	- If the `finally` Promise is cancelled, and you returned a Promise from the handler, we cancel that Promise too.

	Otherwise, the return value from the `finally` handler is entirely discarded.

	:::note Cancellation
	As of Promise v4, `Promise:finally` does not count as a consumer of the parent Promise for cancellation purposes.
	This means that if all of a Promise's consumers are cancelled and the only remaining callbacks are finally handlers,
	the Promise is cancelled and the finally callbacks run then and there.

	Cancellation still propagates through the `finally` Promise though: if you cancel the `finally` Promise, it can cancel
	its parent Promise if it had no other consumers. Likewise, if the parent Promise is cancelled, the `finally` Promise
	will also be cancelled.
	:::

	```lua
	local thing = createSomething()

	doSomethingWith(thing)
		:andThen(function()
			print("It worked!")
			-- do something..
		end)
		:catch(function()
			warn("Oh no it failed!")
		end)
		:finally(function()
			-- either way, destroy thing

			thing:Destroy()
		end)

	```

	@param finallyHandler (status: Status) -> ...any
	@return Promise<...any>
]=]
function Promise.prototype:finally(finallyHandler)
	assert(finallyHandler == nil or isCallable(finallyHandler), string.format(ERROR_NON_FUNCTION, "Promise:finally"))
	return self:_finally(debug.traceback(nil, 2), finallyHandler)
end

--[=[
	Same as `andThenCall`, except for `finally`.

	Attaches a `finally` handler to this Promise that calls the given callback with the predefined arguments.

	@param callback (...: any) -> any
	@param ...? any -- Additional arguments which will be passed to `callback`
	@return Promise
]=]
function Promise.prototype:finallyCall(callback, ...)
	assert(isCallable(callback), string.format(ERROR_NON_FUNCTION, "Promise:finallyCall"))
	local length, values = pack(...)
	return self:_finally(debug.traceback(nil, 2), function()
		return callback(unpack(values, 1, length))
	end)
end

--[=[
	Attaches a `finally` handler to this Promise that discards the resolved value and returns the given value from it.

	```lua
		promise:finallyReturn("some", "values")
	```

	This is sugar for

	```lua
		promise:finally(function()
			return "some", "values"
		end)
	```

	@param ... any -- Values to return from the function
	@return Promise
]=]
function Promise.prototype:finallyReturn(...)
	local length, values = pack(...)
	return self:_finally(debug.traceback(nil, 2), function()
		return unpack(values, 1, length)
	end)
end

--[=[
	Yields the current thread until the given Promise completes. Returns the Promise's status, followed by the values that the promise resolved or rejected with.

	@yields
	@return Status -- The Status representing the fate of the Promise
	@return ...any -- The values the Promise resolved or rejected with.
]=]
function Promise.prototype:awaitStatus()
	self._unhandledRejection = false

	if self._status == Promise.Status.Started then
		local thread = coroutine.running()

		self
			:finally(function()
				task.spawn(thread)
			end)
			-- The finally promise can propagate rejections, so we attach a catch handler to prevent the unhandled
			-- rejection warning from appearing
			:catch(
				function() end
			)

		coroutine.yield()
	end

	if self._status == Promise.Status.Resolved then
		return self._status, unpack(self._values, 1, self._valuesLength)
	elseif self._status == Promise.Status.Rejected then
		return self._status, unpack(self._values, 1, self._valuesLength)
	end

	return self._status
end

local function awaitHelper(status, ...)
	return status == Promise.Status.Resolved, ...
end

--[=[
	Yields the current thread until the given Promise completes. Returns true if the Promise resolved, followed by the values that the promise resolved or rejected with.

	:::caution
	If the Promise gets cancelled, this function will return `false`, which is indistinguishable from a rejection. If you need to differentiate, you should use [[Promise.awaitStatus]] instead.
	:::

	```lua
		local worked, value = getTheValue():await()

	if worked then
		print("got", value)
	else
		warn("it failed")
	end
	```

	@yields
	@return boolean -- `true` if the Promise successfully resolved
	@return ...any -- The values the Promise resolved or rejected with.
]=]
function Promise.prototype:await()
	return awaitHelper(self:awaitStatus())
end

local function expectHelper(status, ...)
	if status ~= Promise.Status.Resolved then
		error((...) == nil and "Expected Promise rejected with no value." or (...), 3)
	end

	return ...
end

--[=[
	Yields the current thread until the given Promise completes. Returns the values that the promise resolved with.

	```lua
	local worked = pcall(function()
		print("got", getTheValue():expect())
	end)

	if not worked then
		warn("it failed")
	end
	```

	This is essentially sugar for:

	```lua
	select(2, assert(promise:await()))
	```

	**Errors** if the Promise rejects or gets cancelled.

	@error any -- Errors with the rejection value if this Promise rejects or gets cancelled.
	@yields
	@return ...any -- The values the Promise resolved with.
]=]
function Promise.prototype:expect()
	return expectHelper(self:awaitStatus())
end

-- Backwards compatibility
Promise.prototype.awaitValue = Promise.prototype.expect

--[[
	Intended for use in tests.

	Similar to await(), but instead of yielding if the promise is unresolved,
	_unwrap will throw. This indicates an assumption that a promise has
	resolved.
]]
function Promise.prototype:_unwrap()
	if self._status == Promise.Status.Started then
		error("Promise has not resolved or rejected.", 2)
	end

	local success = self._status == Promise.Status.Resolved

	return success, unpack(self._values, 1, self._valuesLength)
end

function Promise.prototype:_resolve(...)
	if self._status ~= Promise.Status.Started then
		if Promise.is((...)) then
			(...):_consumerCancelled(self)
		end
		return
	end

	-- If the resolved value was a Promise, we chain onto it!
	if Promise.is((...)) then
		-- Without this warning, arguments sometimes mysteriously disappear
		if select("#", ...) > 1 then
			local message = string.format(
				"When returning a Promise from andThen, extra arguments are " .. "discarded! See:\n\n%s",
				self._source
			)
			warn(message)
		end

		local chainedPromise = ...

		local promise = chainedPromise:andThen(function(...)
			self:_resolve(...)
		end, function(...)
			local maybeRuntimeError = chainedPromise._values[1]

			-- Backwards compatibility < v2
			if chainedPromise._error then
				maybeRuntimeError = Error.new({
					error = chainedPromise._error,
					kind = Error.Kind.ExecutionError,
					context = "[No stack trace available as this Promise originated from an older version of the Promise library (< v2)]",
				})
			end

			if Error.isKind(maybeRuntimeError, Error.Kind.ExecutionError) then
				return self:_reject(maybeRuntimeError:extend({
					error = "This Promise was chained to a Promise that errored.",
					trace = "",
					context = string.format(
						"The Promise at:\n\n%s\n...Rejected because it was chained to the following Promise, which encountered an error:\n",
						self._source
					),
				}))
			end

			self:_reject(...)
		end)

		if promise._status == Promise.Status.Cancelled then
			self:cancel()
		elseif promise._status == Promise.Status.Started then
			-- Adopt ourselves into promise for cancellation propagation.
			self._parent = promise
			promise._consumers[self] = true
		end

		return
	end

	self._status = Promise.Status.Resolved
	self._valuesLength, self._values = pack(...)

	-- We assume that these callbacks will not throw errors.
	for _, callback in ipairs(self._queuedResolve) do
		coroutine.wrap(callback)(...)
	end

	self:_finalize()
end

function Promise.prototype:_reject(...)
	if self._status ~= Promise.Status.Started then
		return
	end

	self._status = Promise.Status.Rejected
	self._valuesLength, self._values = pack(...)

	-- If there are any rejection handlers, call those!
	if not isEmpty(self._queuedReject) then
		-- We assume that these callbacks will not throw errors.
		for _, callback in ipairs(self._queuedReject) do
			coroutine.wrap(callback)(...)
		end
	else
		-- At this point, no one was able to observe the error.
		-- An error handler might still be attached if the error occurred
		-- synchronously. We'll wait one tick, and if there are still no
		-- observers, then we should put a message in the console.

		local err = tostring((...))

		coroutine.wrap(function()
			Promise._timeEvent:Wait()

			-- Someone observed the error, hooray!
			if not self._unhandledRejection then
				return
			end

			-- Build a reasonable message
			local message = string.format("Unhandled Promise rejection:\n\n%s\n\n%s", err, self._source)

			for _, callback in ipairs(Promise._unhandledRejectionCallbacks) do
				task.spawn(callback, self, unpack(self._values, 1, self._valuesLength))
			end

			if Promise.TEST then
				-- Don't spam output when we're running tests.
				return
			end

			warn(message)
		end)()
	end

	self:_finalize()
end

--[[
	Calls any :finally handlers. We need this to be a separate method and
	queue because we must call all of the finally callbacks upon a success,
	failure, *and* cancellation.
]]
function Promise.prototype:_finalize()
	for _, callback in ipairs(self._queuedFinally) do
		-- Purposefully not passing values to callbacks here, as it could be the
		-- resolved values, or rejected errors. If the developer needs the values,
		-- they should use :andThen or :catch explicitly.
		coroutine.wrap(callback)(self._status)
	end

	self._queuedFinally = nil
	self._queuedReject = nil
	self._queuedResolve = nil

	-- Clear references to other Promises to allow gc
	if not Promise.TEST then
		self._parent = nil
		self._consumers = nil
	end

	task.defer(coroutine.close, self._thread)
end

--[=[
	Chains a Promise from this one that is resolved if this Promise is already resolved, and rejected if it is not resolved at the time of calling `:now()`. This can be used to ensure your `andThen` handler occurs on the same frame as the root Promise execution.

	```lua
	doSomething()
		:now()
		:andThen(function(value)
			print("Got", value, "synchronously.")
		end)
	```

	If this Promise is still running, Rejected, or Cancelled, the Promise returned from `:now()` will reject with the `rejectionValue` if passed, otherwise with a `Promise.Error(Promise.Error.Kind.NotResolvedInTime)`. This can be checked with [[Error.isKind]].

	@param rejectionValue? any -- The value to reject with if the Promise isn't resolved
	@return Promise
]=]
function Promise.prototype:now(rejectionValue)
	local traceback = debug.traceback(nil, 2)
	if self._status == Promise.Status.Resolved then
		return self:_andThen(traceback, function(...)
			return ...
		end)
	else
		return Promise.reject(rejectionValue == nil and Error.new({
			kind = Error.Kind.NotResolvedInTime,
			error = "This Promise was not resolved in time for :now()",
			context = ":now() was called at:\n\n" .. traceback,
		}) or rejectionValue)
	end
end

--[=[
	Repeatedly calls a Promise-returning function up to `times` number of times, until the returned Promise resolves.

	If the amount of retries is exceeded, the function will return the latest rejected Promise.

	```lua
	local function canFail(a, b, c)
		return Promise.new(function(resolve, reject)
			-- do something that can fail

			local failed, thing = doSomethingThatCanFail(a, b, c)

			if failed then
				reject("it failed")
			else
				resolve(thing)
			end
		end)
	end

	local MAX_RETRIES = 10
	local value = Promise.retry(canFail, MAX_RETRIES, "foo", "bar", "baz") -- args to send to canFail
	```

	@since 3.0.0
	@param callback (...: P) -> Promise<T>
	@param times number
	@param ...? P
	@return Promise<T>
]=]
function Promise.retry(callback, times, ...)
	assert(isCallable(callback), "Parameter #1 to Promise.retry must be a function")
	assert(type(times) == "number", "Parameter #2 to Promise.retry must be a number")

	local args, length = { ... }, select("#", ...)

	return Promise.resolve(callback(...)):catch(function(...)
		if times > 0 then
			return Promise.retry(callback, times - 1, unpack(args, 1, length))
		else
			return Promise.reject(...)
		end
	end)
end

--[=[
	Repeatedly calls a Promise-returning function up to `times` number of times, waiting `seconds` seconds between each
	retry, until the returned Promise resolves.

	If the amount of retries is exceeded, the function will return the latest rejected Promise.

	@since v3.2.0
	@param callback (...: P) -> Promise<T>
	@param times number
	@param seconds number
	@param ...? P
	@return Promise<T>
]=]
function Promise.retryWithDelay(callback, times, seconds, ...)
	assert(isCallable(callback), "Parameter #1 to Promise.retry must be a function")
	assert(type(times) == "number", "Parameter #2 (times) to Promise.retry must be a number")
	assert(type(seconds) == "number", "Parameter #3 (seconds) to Promise.retry must be a number")

	local args, length = { ... }, select("#", ...)

	return Promise.resolve(callback(...)):catch(function(...)
		if times > 0 then
			Promise.delay(seconds):await()

			return Promise.retryWithDelay(callback, times - 1, seconds, unpack(args, 1, length))
		else
			return Promise.reject(...)
		end
	end)
end

--[=[
	Converts an event into a Promise which resolves the next time the event fires.

	The optional `predicate` callback, if passed, will receive the event arguments and should return `true` or `false`, based on if this fired event should resolve the Promise or not. If `true`, the Promise resolves. If `false`, nothing happens and the predicate will be rerun the next time the event fires.

	The Promise will resolve with the event arguments.

	:::tip
	This function will work given any object with a `Connect` method. This includes all Roblox events.
	:::

	```lua
	-- Creates a Promise which only resolves when `somePart` is touched
	-- by a part named `"Something specific"`.
	return Promise.fromEvent(somePart.Touched, function(part)
		return part.Name == "Something specific"
	end)
	```

	@since 3.0.0
	@param event Event -- Any object with a `Connect` method. This includes all Roblox events.
	@param predicate? (...: P) -> boolean -- A function which determines if the Promise should resolve with the given value, or wait for the next event to check again.
	@return Promise<P>
]=]
function Promise.fromEvent(event, predicate)
	predicate = predicate or function()
		return true
	end

	return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
		local connection
		local shouldDisconnect = false

		local function disconnect()
			connection:Disconnect()
			connection = nil
		end

		-- We use shouldDisconnect because if the callback given to Connect is called before
		-- Connect returns, connection will still be nil. This happens with events that queue up
		-- events when there's nothing connected, such as RemoteEvents

		connection = event:Connect(function(...)
			local callbackValue = predicate(...)

			if callbackValue == true then
				resolve(...)

				if connection then
					disconnect()
				else
					shouldDisconnect = true
				end
			elseif type(callbackValue) ~= "boolean" then
				error("Promise.fromEvent predicate should always return a boolean")
			end
		end)

		if shouldDisconnect and connection then
			return disconnect()
		end

		onCancel(disconnect)
	end)
end

--[=[
	Registers a callback that runs when an unhandled rejection happens. An unhandled rejection happens when a Promise
	is rejected, and the rejection is not observed with `:catch`.

	The callback is called with the actual promise that rejected, followed by the rejection values.

	@since v3.2.0
	@param callback (promise: Promise, ...: any) -- A callback that runs when an unhandled rejection happens.
	@return () -> () -- Function that unregisters the `callback` when called
]=]
function Promise.onUnhandledRejection(callback)
	table.insert(Promise._unhandledRejectionCallbacks, callback)

	return function()
		local index = table.find(Promise._unhandledRejectionCallbacks, callback)

		if index then
			table.remove(Promise._unhandledRejectionCallbacks, index)
		end
	end
end

return Promise
]]></string>
            </Properties>
            <Item class="ModuleScript" referent="10">
              <Properties>
                <string name="Name">init.spec</string>
                <string name="Source"><![CDATA[return function()
	local Promise = require(script.Parent)
	Promise.TEST = true

	local timeEvent = Instance.new("BindableEvent")
	Promise._timeEvent = timeEvent.Event

	local advanceTime
	do
		local injectedPromiseTime = 0

		Promise._getTime = function()
			return injectedPromiseTime
		end

		function advanceTime(delta)
			delta = delta or (1 / 60)

			injectedPromiseTime = injectedPromiseTime + delta
			timeEvent:Fire(delta)
		end
	end

	local function pack(...)
		local len = select("#", ...)

		return len, { ... }
	end

	describe("Promise.Status", function()
		it("should error if indexing nil value", function()
			expect(function()
				local _ = Promise.Status.wrong
			end).to.throw()
		end)
	end)

	describe("Unhandled rejection signal", function()
		it("should call unhandled rejection callbacks", function()
			local badPromise = Promise.new(function(_resolve, reject)
				reject(1, 2)
			end)

			local callCount = 0

			local function callback(promise, rejectionA, rejectionB)
				callCount += 1

				expect(promise).to.equal(badPromise)
				expect(rejectionA).to.equal(1)
				expect(rejectionB).to.equal(2)
			end

			local unregister = Promise.onUnhandledRejection(callback)

			advanceTime()

			expect(callCount).to.equal(1)

			unregister()

			Promise.new(function(_resolve, reject)
				reject(3, 4)
			end)

			advanceTime()

			expect(callCount).to.equal(1)
		end)
	end)

	describe("Promise.new", function()
		it("should instantiate with a callback", function()
			local promise = Promise.new(function() end)

			expect(promise).to.be.ok()
		end)

		it("should invoke the given callback with resolve and reject", function()
			local callCount = 0
			local resolveArg
			local rejectArg

			local promise = Promise.new(function(resolve, reject)
				callCount = callCount + 1
				resolveArg = resolve
				rejectArg = reject
			end)

			expect(promise).to.be.ok()

			expect(callCount).to.equal(1)
			expect(resolveArg).to.be.a("function")
			expect(rejectArg).to.be.a("function")
			expect(promise:getStatus()).to.equal(Promise.Status.Started)
		end)

		it("should resolve promises on resolve()", function()
			local callCount = 0

			local promise = Promise.new(function(resolve)
				callCount = callCount + 1
				resolve()
			end)

			expect(promise).to.be.ok()
			expect(callCount).to.equal(1)
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
		end)

		it("should reject promises on reject()", function()
			local callCount = 0

			local promise = Promise.new(function(resolve, reject)
				callCount = callCount + 1
				reject()
			end)

			expect(promise).to.be.ok()
			expect(callCount).to.equal(1)
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
		end)

		it("should reject on error in callback", function()
			local callCount = 0

			local promise = Promise.new(function()
				callCount = callCount + 1
				error("hahah")
			end)

			expect(promise).to.be.ok()
			expect(callCount).to.equal(1)
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(tostring(promise._values[1]):find("hahah")).to.be.ok()

			-- Loosely check for the pieces of the stack trace we expect
			expect(tostring(promise._values[1]):find("init.spec")).to.be.ok()
			expect(tostring(promise._values[1]):find("runExecutor")).to.be.ok()
		end)

		it("should work with C functions", function()
			expect(function()
				Promise.new(tick):andThen(tick)
			end).to.never.throw()
		end)

		it("should have a nice tostring", function()
			expect(tostring(Promise.resolve()):gmatch("Promise(Resolved)")).to.be.ok()
		end)

		it("should allow yielding", function()
			local bindable = Instance.new("BindableEvent")
			local promise = Promise.new(function(resolve)
				bindable.Event:Wait()
				resolve(5)
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			bindable:Fire()
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal(5)
		end)

		it("should preserve stack traces of resolve-chained promises", function()
			local function nestedCall(text)
				error(text)
			end

			local promise = Promise.new(function(resolve)
				resolve(Promise.new(function()
					nestedCall("sample text")
				end))
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)

			local trace = tostring(promise._values[1])
			expect(trace:find("sample text")).to.be.ok()
			expect(trace:find("nestedCall")).to.be.ok()
			expect(trace:find("runExecutor")).to.be.ok()
			expect(trace:find("runPlanNode")).to.be.ok()
			expect(
				trace:find("...Rejected because it was chained to the following Promise, which encountered an error:")
			).to.be.ok()
		end)

		it("should report errors from Promises with _error (< v2)", function()
			local oldPromise = Promise.reject()
			oldPromise._error = "Sample error"

			local newPromise = Promise.resolve():andThenReturn(oldPromise)

			expect(newPromise:getStatus()).to.equal(Promise.Status.Rejected)

			local trace = tostring(newPromise._values[1])
			expect(trace:find("Sample error")).to.be.ok()
			expect(
				trace:find("...Rejected because it was chained to the following Promise, which encountered an error:")
			).to.be.ok()
			expect(trace:find("%[No stack trace available")).to.be.ok()
		end)

		it("should allow callable tables", function()
			local promise = Promise.new(setmetatable({}, {
				__call = function(_, resolve)
					resolve(1)
				end,
			}))

			local called = false
			promise:andThen(setmetatable({}, {
				__call = function(_, var)
					expect(var).to.equal(1)
					called = true
				end,
			}))

			expect(called).to.equal(true)
		end)

		itSKIP("should close the thread after resolve", function()
			local count = 0
			Promise.new(function(resolve)
				count += 1
				resolve()
				Promise.delay(1):await()
				count += 1
			end)

			task.wait(1)

			expect(count).to.equal(1)
		end)
	end)

	describe("Promise.defer", function()
		it("should execute after the time event", function()
			local callCount = 0
			local promise = Promise.defer(function(resolve, reject, onCancel, nothing)
				expect(type(resolve)).to.equal("function")
				expect(type(reject)).to.equal("function")
				expect(type(onCancel)).to.equal("function")
				expect(type(nothing)).to.equal("nil")

				callCount = callCount + 1

				resolve("foo")
			end)

			expect(callCount).to.equal(0)
			expect(promise:getStatus()).to.equal(Promise.Status.Started)

			advanceTime()
			expect(callCount).to.equal(1)
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)

			advanceTime()
			expect(callCount).to.equal(1)
		end)
	end)

	describe("Promise.delay", function()
		it("should schedule promise resolution", function()
			local promise = Promise.delay(1)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)

			advanceTime()
			expect(promise:getStatus()).to.equal(Promise.Status.Started)

			advanceTime(1)
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
		end)

		it("should allow for delays to be cancelled", function()
			local promise = Promise.delay(2)

			Promise.delay(1):andThen(function()
				promise:cancel()
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			advanceTime()
			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			advanceTime(1)
			expect(promise:getStatus()).to.equal(Promise.Status.Cancelled)
			advanceTime(1)
		end)
	end)

	describe("Promise.resolve", function()
		it("should immediately resolve with a value", function()
			local promise = Promise.resolve(5, 6)

			expect(promise).to.be.ok()
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal(5)
			expect(promise._values[2]).to.equal(6)
		end)

		it("should chain onto passed promises", function()
			local promise = Promise.resolve(Promise.new(function(_, reject)
				reject(7)
			end))

			expect(promise).to.be.ok()
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(promise._values[1]).to.equal(7)
		end)
	end)

	describe("Promise.reject", function()
		it("should immediately reject with a value", function()
			local promise = Promise.reject(6, 7)

			expect(promise).to.be.ok()
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(promise._values[1]).to.equal(6)
			expect(promise._values[2]).to.equal(7)
		end)

		it("should pass a promise as-is as an error", function()
			local innerPromise = Promise.new(function(resolve)
				resolve(6)
			end)

			local promise = Promise.reject(innerPromise)

			expect(promise).to.be.ok()
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(promise._values[1]).to.equal(innerPromise)
		end)
	end)

	describe("Promise:andThen", function()
		it("should allow yielding", function()
			local bindable = Instance.new("BindableEvent")
			local promise = Promise.resolve():andThen(function()
				bindable.Event:Wait()
				return 5
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			bindable:Fire()
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal(5)
		end)

		it("should run andThens on a new thread", function()
			local bindable = Instance.new("BindableEvent")

			local resolve
			local parentPromise = Promise.new(function(_resolve)
				resolve = _resolve
			end)

			local deadlockedPromise = parentPromise:andThen(function()
				bindable.Event:Wait()
				return 5
			end)

			local successfulPromise = parentPromise:andThen(function()
				return "foo"
			end)

			expect(parentPromise:getStatus()).to.equal(Promise.Status.Started)
			resolve()
			expect(successfulPromise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(successfulPromise._values[1]).to.equal("foo")
			expect(deadlockedPromise:getStatus()).to.equal(Promise.Status.Started)
		end)

		it("should chain onto resolved promises", function()
			local args
			local argsLength
			local callCount = 0
			local badCallCount = 0

			local promise = Promise.resolve(5)

			local chained = promise:andThen(function(...)
				argsLength, args = pack(...)
				callCount = callCount + 1
			end, function()
				badCallCount = badCallCount + 1
			end)

			expect(badCallCount).to.equal(0)

			expect(callCount).to.equal(1)
			expect(argsLength).to.equal(1)
			expect(args[1]).to.equal(5)

			expect(promise).to.be.ok()
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal(5)

			expect(chained).to.be.ok()
			expect(chained).never.to.equal(promise)
			expect(chained:getStatus()).to.equal(Promise.Status.Resolved)
			expect(#chained._values).to.equal(0)
		end)

		it("should chain onto rejected promises", function()
			local args
			local argsLength
			local callCount = 0
			local badCallCount = 0

			local promise = Promise.reject(5)

			local chained = promise:andThen(function(...)
				badCallCount = badCallCount + 1
			end, function(...)
				argsLength, args = pack(...)
				callCount = callCount + 1
			end)

			expect(badCallCount).to.equal(0)

			expect(callCount).to.equal(1)
			expect(argsLength).to.equal(1)
			expect(args[1]).to.equal(5)

			expect(promise).to.be.ok()
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(promise._values[1]).to.equal(5)

			expect(chained).to.be.ok()
			expect(chained).never.to.equal(promise)
			expect(chained:getStatus()).to.equal(Promise.Status.Resolved)
			expect(#chained._values).to.equal(0)
		end)

		it("should reject on error in callback", function()
			local callCount = 0

			local promise = Promise.resolve(1):andThen(function()
				callCount = callCount + 1
				error("hahah")
			end)

			expect(promise).to.be.ok()
			expect(callCount).to.equal(1)
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(tostring(promise._values[1]):find("hahah")).to.be.ok()

			-- Loosely check for the pieces of the stack trace we expect
			expect(tostring(promise._values[1]):find("init.spec")).to.be.ok()
			expect(tostring(promise._values[1]):find("runExecutor")).to.be.ok()
		end)

		it("should chain onto asynchronously resolved promises", function()
			local args
			local argsLength
			local callCount = 0
			local badCallCount = 0

			local startResolution
			local promise = Promise.new(function(resolve)
				startResolution = resolve
			end)

			local chained = promise:andThen(function(...)
				args = { ... }
				argsLength = select("#", ...)
				callCount = callCount + 1
			end, function()
				badCallCount = badCallCount + 1
			end)

			expect(callCount).to.equal(0)
			expect(badCallCount).to.equal(0)

			startResolution(6)

			expect(badCallCount).to.equal(0)

			expect(callCount).to.equal(1)
			expect(argsLength).to.equal(1)
			expect(args[1]).to.equal(6)

			expect(promise).to.be.ok()
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal(6)

			expect(chained).to.be.ok()
			expect(chained).never.to.equal(promise)
			expect(chained:getStatus()).to.equal(Promise.Status.Resolved)
			expect(#chained._values).to.equal(0)
		end)

		it("should chain onto asynchronously rejected promises", function()
			local args
			local argsLength
			local callCount = 0
			local badCallCount = 0

			local startResolution
			local promise = Promise.new(function(_, reject)
				startResolution = reject
			end)

			local chained = promise:andThen(function()
				badCallCount = badCallCount + 1
			end, function(...)
				args = { ... }
				argsLength = select("#", ...)
				callCount = callCount + 1
			end)

			expect(callCount).to.equal(0)
			expect(badCallCount).to.equal(0)

			startResolution(6)

			expect(badCallCount).to.equal(0)

			expect(callCount).to.equal(1)
			expect(argsLength).to.equal(1)
			expect(args[1]).to.equal(6)

			expect(promise).to.be.ok()
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(promise._values[1]).to.equal(6)

			expect(chained).to.be.ok()
			expect(chained).never.to.equal(promise)
			expect(chained:getStatus()).to.equal(Promise.Status.Resolved)
			expect(#chained._values).to.equal(0)
		end)

		it("should propagate errors through multiple levels", function()
			local x, y, z
			Promise.new(function(resolve, reject)
				reject(1, 2, 3)
			end):andThen(function() end):catch(function(a, b, c)
				x, y, z = a, b, c
			end)

			expect(x).to.equal(1)
			expect(y).to.equal(2)
			expect(z).to.equal(3)
		end)

		it("should not call queued callbacks from a cancelled sub-promise", function()
			local resolve
			local count = 0

			local root = Promise.new(function(r)
				resolve = r
			end)

			root:andThen(function()
				count += 1
			end)

			root
				:andThen(function()
					count += 1
				end)
				:cancel()

			resolve("foo")

			expect(count).to.equal(1)
		end)
	end)

	describe("Promise:cancel", function()
		it("should mark promises as cancelled and not resolve or reject them", function()
			local callCount = 0
			local finallyCallCount = 0
			local promise = Promise.new(function() end)
				:andThen(function()
					callCount = callCount + 1
				end)
				:finally(function()
					finallyCallCount = finallyCallCount + 1
				end)

			promise:cancel()
			promise:cancel() -- Twice to check call counts

			expect(callCount).to.equal(0)
			expect(finallyCallCount).to.equal(1)
			expect(promise:getStatus()).to.equal(Promise.Status.Cancelled)
		end)

		it("should call the cancellation hook once", function()
			local callCount = 0

			local promise = Promise.new(function(resolve, reject, onCancel)
				onCancel(function()
					callCount = callCount + 1
				end)
			end)

			promise:cancel()
			promise:cancel() -- Twice to check call count

			expect(callCount).to.equal(1)
		end)

		it("should propagate cancellations", function()
			local promise = Promise.new(function() end)

			local consumer1 = promise:andThen()
			local consumer2 = promise:andThen()

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			expect(consumer1:getStatus()).to.equal(Promise.Status.Started)
			expect(consumer2:getStatus()).to.equal(Promise.Status.Started)

			consumer1:cancel()

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			expect(consumer1:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(consumer2:getStatus()).to.equal(Promise.Status.Started)

			consumer2:cancel()

			expect(promise:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(consumer1:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(consumer2:getStatus()).to.equal(Promise.Status.Cancelled)
		end)

		it("should affect downstream promises", function()
			local promise = Promise.new(function() end)
			local consumer = promise:andThen()

			promise:cancel()

			expect(consumer:getStatus()).to.equal(Promise.Status.Cancelled)
		end)

		it("should track consumers", function()
			local pending = Promise.new(function() end)
			local p0 = Promise.resolve()
			local p1 = p0:andThen(function()
				return pending
			end)
			local p2 = Promise.new(function(resolve)
				resolve(p1)
			end)
			local p3 = p2:andThen(function() end)

			expect(p1._parent).to.never.equal(p0)
			expect(p2._parent).to.never.equal(p1)
			expect(p2._consumers[p3]).to.be.ok()
			expect(p3._parent).to.equal(p2)
		end)

		it("should cancel resolved pending promises", function()
			local p1 = Promise.new(function() end)

			local p2 = Promise.new(function(resolve)
				resolve(p1)
			end):finally(function() end)

			p2:cancel()

			expect(p1._status).to.equal(Promise.Status.Cancelled)
			expect(p2._status).to.equal(Promise.Status.Cancelled)
		end)

		it("should close the promise thread", function()
			local count = 0
			local promise = Promise.new(function()
				count += 1
				Promise.delay(1):await()
				count += 1
			end)

			promise:cancel()
			advanceTime(2)

			expect(count).to.equal(1)
		end)
	end)

	describe("Promise:finally", function()
		it("should be called upon resolve, reject, or cancel", function()
			local callCount = 0

			local function finally()
				callCount = callCount + 1
			end

			-- Resolved promise
			Promise.new(function(resolve, reject)
				resolve()
			end):finally(finally)

			-- Chained promise
			Promise.resolve():andThen(function() end):finally(finally):finally(finally)

			-- Rejected promise
			Promise.reject():finally(finally)

			local cancelledPromise = Promise.new(function() end):finally(finally)
			cancelledPromise:cancel()

			expect(callCount).to.equal(5)
		end)

		it("should not forward return values", function()
			local value

			Promise.resolve(2)
				:finally(function()
					return 1
				end)
				:andThen(function(v)
					value = v
				end)

			expect(value).to.equal(2)
		end)

		it("should not consume rejections", function()
			local catchRan = false
			local thenRan = false
			Promise.reject(5)
				:finally(function()
					return 42
				end)
				:andThen(function()
					thenRan = true
				end)
				:catch(function(value)
					catchRan = true
					expect(value).to.equal(5)
				end)

			expect(catchRan).to.equal(true)
			expect(thenRan).to.equal(false)
		end)

		it("should wait for returned promises", function()
			local resolve
			local promise = Promise.reject("foo"):finally(function()
				return Promise.new(function(r)
					resolve = r
				end)
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)

			resolve()

			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			local _, value = promise:_unwrap()
			expect(value).to.equal("foo")
		end)

		it("should reject with a returned rejected promise's value", function()
			local reject
			local promise = Promise.reject("foo"):finally(function()
				return Promise.new(function(_, r)
					reject = r
				end)
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)

			reject("bar")

			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			local _, value = promise:_unwrap()
			expect(value).to.equal("bar")
		end)

		it("should reject when handler errors", function()
			local errorValue = {}
			local promise = Promise.reject("bar"):finally(function()
				error(errorValue)
			end)

			local ok, value = promise:_unwrap()

			expect(ok).to.equal(false)
			expect(value).to.equal(errorValue)
		end)

		it("should not prevent cancellation", function()
			local promise = Promise.new(function() end)

			local finallyRan = false
			promise:finally(function()
				finallyRan = true
			end)

			local consumer = promise:andThen(function() end)

			consumer:cancel()

			expect(promise:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(finallyRan).to.equal(true)
		end)

		it("should propagate cancellation downwards", function()
			local finallyRan = false
			local andThenRan = false
			local root = Promise.new(function() end)

			local consumer = root:finally(function()
				finallyRan = true
			end)

			root:cancel()

			expect(root:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(consumer:getStatus()).to.equal(Promise.Status.Cancelled)

			expect(finallyRan).to.equal(true)
			expect(andThenRan).to.equal(false)
		end)

		it("should propagate cancellation upwards", function()
			local finallyRan = false
			local andThenRan = false
			local root = Promise.new(function() end)

			local consumer = root:finally(function()
				finallyRan = true
			end)

			consumer:cancel()

			expect(root:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(consumer:getStatus()).to.equal(Promise.Status.Cancelled)

			expect(finallyRan).to.equal(true)
			expect(andThenRan).to.equal(false)
		end)

		it("should cancel returned promise if cancelled", function()
			local internal = Promise.new(function() end)

			local promise = Promise.resolve():finally(function()
				return internal
			end)

			promise:cancel()

			expect(internal:getStatus()).to.equal(Promise.Status.Cancelled)
		end)
	end)

	describe("Promise.all", function()
		it("should error if given something other than a table", function()
			expect(function()
				Promise.all(1)
			end).to.throw()
		end)

		it("should resolve instantly with an empty table if given no promises", function()
			local promise = Promise.all({})
			local success, value = promise:_unwrap()

			expect(success).to.equal(true)
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(value).to.be.a("table")
			expect(next(value)).to.equal(nil)
		end)

		it("should error if given non-promise values", function()
			expect(function()
				Promise.all({ {}, {}, {} })
			end).to.throw()
		end)

		it("should wait for all promises to be resolved and return their values", function()
			local resolveFunctions = {}

			local testValuesLength, testValues = pack(1, "A string", nil, false)

			local promises = {}

			for i = 1, testValuesLength do
				promises[i] = Promise.new(function(resolve)
					resolveFunctions[i] = { resolve, testValues[i] }
				end)
			end

			local combinedPromise = Promise.all(promises)

			for _, resolve in ipairs(resolveFunctions) do
				expect(combinedPromise:getStatus()).to.equal(Promise.Status.Started)
				resolve[1](resolve[2])
			end

			local resultLength, result = pack(combinedPromise:_unwrap())
			local success, resolved = unpack(result, 1, resultLength)

			expect(resultLength).to.equal(2)
			expect(success).to.equal(true)
			expect(resolved).to.be.a("table")
			expect(#resolved).to.equal(#promises)

			for i = 1, testValuesLength do
				expect(resolved[i]).to.equal(testValues[i])
			end
		end)

		it("should reject if any individual promise rejected", function()
			local rejectA
			local resolveB

			local a = Promise.new(function(_, reject)
				rejectA = reject
			end)

			local b = Promise.new(function(resolve)
				resolveB = resolve
			end)

			local combinedPromise = Promise.all({ a, b })

			expect(combinedPromise:getStatus()).to.equal(Promise.Status.Started)

			rejectA("baz", "qux")
			resolveB("foo", "bar")

			local resultLength, result = pack(combinedPromise:_unwrap())
			local success, first, second = unpack(result, 1, resultLength)

			expect(resultLength).to.equal(3)
			expect(success).to.equal(false)
			expect(first).to.equal("baz")
			expect(second).to.equal("qux")
			expect(b:getStatus()).to.equal(Promise.Status.Cancelled)
		end)

		it("should not resolve if resolved after rejecting", function()
			local rejectA
			local resolveB

			local a = Promise.new(function(_, reject)
				rejectA = reject
			end)

			local b = Promise.new(function(resolve)
				resolveB = resolve
			end)

			local combinedPromise = Promise.all({ a, b })

			expect(combinedPromise:getStatus()).to.equal(Promise.Status.Started)

			rejectA("baz", "qux")
			resolveB("foo", "bar")

			local resultLength, result = pack(combinedPromise:_unwrap())
			local success, first, second = unpack(result, 1, resultLength)

			expect(resultLength).to.equal(3)
			expect(success).to.equal(false)
			expect(first).to.equal("baz")
			expect(second).to.equal("qux")
		end)

		it("should only reject once", function()
			local rejectA
			local rejectB

			local a = Promise.new(function(_, reject)
				rejectA = reject
			end)

			local b = Promise.new(function(_, reject)
				rejectB = reject
			end)

			local combinedPromise = Promise.all({ a, b })

			expect(combinedPromise:getStatus()).to.equal(Promise.Status.Started)

			rejectA("foo", "bar")

			expect(combinedPromise:getStatus()).to.equal(Promise.Status.Rejected)

			rejectB("baz", "qux")

			local resultLength, result = pack(combinedPromise:_unwrap())
			local success, first, second = unpack(result, 1, resultLength)

			expect(resultLength).to.equal(3)
			expect(success).to.equal(false)
			expect(first).to.equal("foo")
			expect(second).to.equal("bar")
		end)

		it("should error if a non-array table is passed in", function()
			local ok, err = pcall(function()
				Promise.all(Promise.new(function() end))
			end)

			expect(ok).to.be.ok()
			expect(err:find("Non%-promise")).to.be.ok()
		end)

		it("should cancel pending promises if one rejects", function()
			local p = Promise.new(function() end)
			expect(Promise.all({
				Promise.resolve(),
				Promise.reject(),
				p,
			}):getStatus()).to.equal(Promise.Status.Rejected)
			expect(p:getStatus()).to.equal(Promise.Status.Cancelled)
		end)

		it("should cancel promises if it is cancelled", function()
			local p = Promise.new(function() end)
			p:andThen(function() end)

			local promises = {
				Promise.new(function() end),
				Promise.new(function() end),
				p,
			}

			Promise.all(promises):cancel()

			expect(promises[1]:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
		end)
	end)

	describe("Promise.fold", function()
		it("should return the initial value in a promise when the list is empty", function()
			local initialValue = {}
			local result = Promise.fold({}, function()
				error("should not be called")
			end, initialValue)

			expect(Promise.is(result)).to.equal(true)
			expect(result:getStatus()).to.equal(Promise.Status.Resolved)
			expect(result:expect()).to.equal(initialValue)
		end)

		it("should accept promises in the list", function()
			local resolve

			local sum = Promise.fold({ Promise.new(function(r)
				resolve = r
			end), 2, 3 }, function(sum, element)
				return sum + element
			end, 0)

			resolve(1)

			expect(Promise.is(sum)).to.equal(true)
			expect(sum:getStatus()).to.equal(Promise.Status.Resolved)
			expect(sum:expect()).to.equal(6)
		end)

		it("should always return a promise even if the list or reducer don't use them", function()
			local sum = Promise.fold({ 1, 2, 3 }, function(sum, element, index)
				if index == 2 then
					return Promise.delay(1):andThenReturn(sum + element)
				else
					return sum + element
				end
			end, 0)
			expect(Promise.is(sum)).to.equal(true)
			expect(sum:getStatus()).to.equal(Promise.Status.Started)
			advanceTime(2)
			expect(sum:getStatus()).to.equal(Promise.Status.Resolved)
			expect(sum:expect()).to.equal(6)
		end)

		it("should return the first rejected promise", function()
			local errorMessage = "foo"
			local sum = Promise.fold({ 1, 2, 3 }, function(sum, element, index)
				if index == 2 then
					return Promise.reject(errorMessage)
				else
					return sum + element
				end
			end, 0)
			expect(Promise.is(sum)).to.equal(true)
			local status, rejection = sum:awaitStatus()
			expect(status).to.equal(Promise.Status.Rejected)
			expect(rejection).to.equal(errorMessage)
		end)

		it("should return the first canceled promise", function()
			local secondPromise
			local sum = Promise.fold({ 1, 2, 3 }, function(sum, element, index)
				if index == 1 then
					return sum + element
				elseif index == 2 then
					secondPromise = Promise.delay(1):andThenReturn(sum + element)
					return secondPromise
				else
					error("this should not run if the promise is cancelled")
				end
			end, 0)
			expect(Promise.is(sum)).to.equal(true)
			expect(sum:getStatus()).to.equal(Promise.Status.Started)
			secondPromise:cancel()
			expect(sum:getStatus()).to.equal(Promise.Status.Cancelled)
		end)
	end)

	describe("Promise.race", function()
		it("should resolve with the first settled value", function()
			local promise = Promise.race({
				Promise.resolve(1),
				Promise.resolve(2),
			}):andThen(function(value)
				expect(value).to.equal(1)
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
		end)

		it("should cancel other promises", function()
			local promise = Promise.new(function() end)
			promise:andThen(function() end)
			local promises = {
				promise,
				Promise.new(function() end),
				Promise.new(function(resolve)
					resolve(2)
				end),
			}

			local promise = Promise.race(promises)

			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal(2)
			expect(promises[1]:getStatus()).to.equal(Promise.Status.Started)
			expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(promises[3]:getStatus()).to.equal(Promise.Status.Resolved)

			local p = Promise.new(function() end)
			expect(Promise.race({
				Promise.reject(),
				Promise.resolve(),
				p,
			}):getStatus()).to.equal(Promise.Status.Rejected)
			expect(p:getStatus()).to.equal(Promise.Status.Cancelled)
		end)

		it("should error if a non-array table is passed in", function()
			local ok, err = pcall(function()
				Promise.race(Promise.new(function() end))
			end)

			expect(ok).to.be.ok()
			expect(err:find("Non%-promise")).to.be.ok()
		end)

		it("should cancel promises if it is cancelled", function()
			local p = Promise.new(function() end)
			p:andThen(function() end)

			local promises = {
				Promise.new(function() end),
				Promise.new(function() end),
				p,
			}

			Promise.race(promises):cancel()

			expect(promises[1]:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
		end)
	end)

	describe("Promise.promisify", function()
		it("should wrap functions", function()
			local function test(n)
				return n + 1
			end

			local promisified = Promise.promisify(test)
			local promise = promisified(1)
			local success, result = promise:_unwrap()

			expect(success).to.equal(true)
			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(result).to.equal(2)
		end)

		it("should catch errors after a yield", function()
			local bindable = Instance.new("BindableEvent")
			local test = Promise.promisify(function()
				bindable.Event:Wait()
				error("errortext")
			end)

			local promise = test()

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			bindable:Fire()
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(tostring(promise._values[1]):find("errortext")).to.be.ok()
		end)
	end)

	describe("Promise.tap", function()
		it("should thread through values", function()
			local first, second

			Promise.resolve(1)
				:andThen(function(v)
					return v + 1
				end)
				:tap(function(v)
					first = v
					return v + 1
				end)
				:andThen(function(v)
					second = v
				end)

			expect(first).to.equal(2)
			expect(second).to.equal(2)
		end)

		it("should chain onto promises", function()
			local resolveInner, finalValue

			local promise = Promise.resolve(1)
				:tap(function()
					return Promise.new(function(resolve)
						resolveInner = resolve
					end)
				end)
				:andThen(function(v)
					finalValue = v
				end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			expect(finalValue).to.never.be.ok()

			resolveInner(1)

			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(finalValue).to.equal(1)
		end)
	end)

	describe("Promise.try", function()
		it("should catch synchronous errors", function()
			local errorText
			Promise.try(function()
				error("errortext")
			end):catch(function(e)
				errorText = tostring(e)
			end)

			expect(errorText:find("errortext")).to.be.ok()
		end)

		it("should reject with error objects", function()
			local object = {}
			local success, value = Promise.try(function()
				error(object)
			end):_unwrap()

			expect(success).to.equal(false)
			expect(value).to.equal(object)
		end)

		it("should catch asynchronous errors", function()
			local bindable = Instance.new("BindableEvent")
			local promise = Promise.try(function()
				bindable.Event:Wait()
				error("errortext")
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			bindable:Fire()
			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(tostring(promise._values[1]):find("errortext")).to.be.ok()
		end)
	end)

	describe("Promise:andThenReturn", function()
		it("should return the given values", function()
			local value1, value2

			Promise.resolve():andThenReturn(1, 2):andThen(function(one, two)
				value1 = one
				value2 = two
			end)

			expect(value1).to.equal(1)
			expect(value2).to.equal(2)
		end)
	end)

	describe("Promise:andThenCall", function()
		it("should call the given function with arguments", function()
			local value1, value2
			Promise.resolve():andThenCall(function(a, b)
				value1 = a
				value2 = b
			end, 3, 4)

			expect(value1).to.equal(3)
			expect(value2).to.equal(4)
		end)
	end)

	describe("Promise.some", function()
		it("should resolve once the goal is reached", function()
			local p = Promise.some({
				Promise.resolve(1),
				Promise.reject(),
				Promise.resolve(2),
			}, 2)
			expect(p:getStatus()).to.equal(Promise.Status.Resolved)
			expect(p._values[1][1]).to.equal(1)
			expect(p._values[1][2]).to.equal(2)
		end)

		it("should error if the goal can't be reached", function()
			expect(Promise.some({
				Promise.resolve(),
				Promise.reject(),
			}, 2):getStatus()).to.equal(Promise.Status.Rejected)

			local reject
			local p = Promise.some({
				Promise.resolve(),
				Promise.new(function(_, r)
					reject = r
				end),
			}, 2)

			expect(p:getStatus()).to.equal(Promise.Status.Started)
			reject("foo")
			expect(p:getStatus()).to.equal(Promise.Status.Rejected)
			expect(p._values[1]).to.equal("foo")
		end)

		it("should cancel pending Promises once the goal is reached", function()
			local resolve
			local pending1 = Promise.new(function() end)
			local pending2 = Promise.new(function(r)
				resolve = r
			end)

			local some = Promise.some({
				pending1,
				pending2,
				Promise.resolve(),
			}, 2)

			expect(some:getStatus()).to.equal(Promise.Status.Started)
			expect(pending1:getStatus()).to.equal(Promise.Status.Started)
			expect(pending2:getStatus()).to.equal(Promise.Status.Started)

			resolve()

			expect(some:getStatus()).to.equal(Promise.Status.Resolved)
			expect(pending1:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(pending2:getStatus()).to.equal(Promise.Status.Resolved)
		end)

		it("should error if passed a non-number", function()
			expect(function()
				Promise.some({}, "non-number")
			end).to.throw()
		end)

		it("should return an empty array if amount is 0", function()
			local p = Promise.some({
				Promise.resolve(2),
			}, 0)

			expect(p:getStatus()).to.equal(Promise.Status.Resolved)
			expect(#p._values[1]).to.equal(0)
		end)

		it("should not return extra values", function()
			local p = Promise.some({
				Promise.resolve(1),
				Promise.resolve(2),
				Promise.resolve(3),
				Promise.resolve(4),
			}, 2)

			expect(p:getStatus()).to.equal(Promise.Status.Resolved)
			expect(#p._values[1]).to.equal(2)
			expect(p._values[1][1]).to.equal(1)
			expect(p._values[1][2]).to.equal(2)
		end)

		it("should cancel promises if it is cancelled", function()
			local p = Promise.new(function() end)
			p:andThen(function() end)

			local promises = {
				Promise.new(function() end),
				Promise.new(function() end),
				p,
			}

			Promise.some(promises, 3):cancel()

			expect(promises[1]:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
		end)

		describe("Promise.any", function()
			it("should return the value directly", function()
				local p = Promise.any({
					Promise.reject(),
					Promise.reject(),
					Promise.resolve(1),
				})

				expect(p:getStatus()).to.equal(Promise.Status.Resolved)
				expect(p._values[1]).to.equal(1)
			end)

			it("should error if all are rejected", function()
				expect(Promise.any({
					Promise.reject(),
					Promise.reject(),
					Promise.reject(),
				}):getStatus()).to.equal(Promise.Status.Rejected)
			end)
		end)
	end)

	describe("Promise.allSettled", function()
		it("should resolve with an array of PromiseStatuses", function()
			local reject
			local p = Promise.allSettled({
				Promise.resolve(),
				Promise.reject(),
				Promise.resolve(),
				Promise.new(function(_, r)
					reject = r
				end),
			})

			expect(p:getStatus()).to.equal(Promise.Status.Started)
			reject()
			expect(p:getStatus()).to.equal(Promise.Status.Resolved)
			expect(p._values[1][1]).to.equal(Promise.Status.Resolved)
			expect(p._values[1][2]).to.equal(Promise.Status.Rejected)
			expect(p._values[1][3]).to.equal(Promise.Status.Resolved)
			expect(p._values[1][4]).to.equal(Promise.Status.Rejected)
		end)

		it("should cancel promises if it is cancelled", function()
			local p = Promise.new(function() end)
			p:andThen(function() end)

			local promises = {
				Promise.new(function() end),
				Promise.new(function() end),
				p,
			}

			Promise.allSettled(promises):cancel()

			expect(promises[1]:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
		end)
	end)

	describe("Promise:await", function()
		it("should return the correct values", function()
			local promise = Promise.resolve(5, 6, nil, 7)

			local a, b, c, d, e = promise:await()

			expect(a).to.equal(true)
			expect(b).to.equal(5)
			expect(c).to.equal(6)
			expect(d).to.equal(nil)
			expect(e).to.equal(7)
		end)

		it("should work if yielding is needed", function()
			local ran = false
			task.spawn(function()
				local _, actualTime = Promise.delay(1):await()
				expect(type(actualTime)).to.equal("number")
				ran = true
			end)

			advanceTime(2)
			expect(ran).to.equal(true)
		end)
	end)

	describe("Promise:expect", function()
		it("should throw the correct values", function()
			local rejectionValue = {}
			local promise = Promise.reject(rejectionValue)

			local success, value = pcall(function()
				promise:expect()
			end)

			expect(success).to.equal(false)
			expect(value).to.equal(rejectionValue)
		end)
	end)

	describe("Promise:now", function()
		it("should resolve if the Promise is resolved", function()
			local success, value = Promise.resolve("foo"):now():_unwrap()

			expect(success).to.equal(true)
			expect(value).to.equal("foo")
		end)

		it("should reject if the Promise is not resolved", function()
			local success, value = Promise.new(function() end):now():_unwrap()

			expect(success).to.equal(false)
			expect(Promise.Error.isKind(value, "NotResolvedInTime")).to.equal(true)
		end)

		it("should reject with a custom rejection value", function()
			local success, value = Promise.new(function() end):now("foo"):_unwrap()

			expect(success).to.equal(false)
			expect(value).to.equal("foo")
		end)
	end)

	describe("Promise.each", function()
		it("should iterate", function()
			local ok, result = Promise.each({
				"foo",
				"bar",
				"baz",
				"qux",
			}, function(...)
				return { ... }
			end):_unwrap()

			expect(ok).to.equal(true)
			expect(result[1][1]).to.equal("foo")
			expect(result[1][2]).to.equal(1)
			expect(result[2][1]).to.equal("bar")
			expect(result[2][2]).to.equal(2)
			expect(result[3][1]).to.equal("baz")
			expect(result[3][2]).to.equal(3)
			expect(result[4][1]).to.equal("qux")
			expect(result[4][2]).to.equal(4)
		end)

		it("should iterate serially", function()
			local resolves = {}
			local callCounts = {}

			local promise = Promise.each({
				"foo",
				"bar",
				"baz",
			}, function(value, index)
				callCounts[index] = (callCounts[index] or 0) + 1

				return Promise.new(function(resolve)
					table.insert(resolves, function()
						resolve(value:upper())
					end)
				end)
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			expect(#resolves).to.equal(1)
			expect(callCounts[1]).to.equal(1)
			expect(callCounts[2]).to.never.be.ok()

			table.remove(resolves, 1)()

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			expect(#resolves).to.equal(1)
			expect(callCounts[1]).to.equal(1)
			expect(callCounts[2]).to.equal(1)
			expect(callCounts[3]).to.never.be.ok()

			table.remove(resolves, 1)()

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			expect(callCounts[1]).to.equal(1)
			expect(callCounts[2]).to.equal(1)
			expect(callCounts[3]).to.equal(1)

			table.remove(resolves, 1)()

			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(type(promise._values[1])).to.equal("table")
			expect(type(promise._values[2])).to.equal("nil")

			local result = promise._values[1]

			expect(result[1]).to.equal("FOO")
			expect(result[2]).to.equal("BAR")
			expect(result[3]).to.equal("BAZ")
		end)

		it("should reject with the value if the predicate promise rejects", function()
			local promise = Promise.each({ 1, 2, 3 }, function()
				return Promise.reject("foobar")
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(promise._values[1]).to.equal("foobar")
		end)

		it("should allow Promises to be in the list and wait when it gets to them", function()
			local innerResolve
			local innerPromise = Promise.new(function(resolve)
				innerResolve = resolve
			end)

			local promise = Promise.each({
				innerPromise,
			}, function(value)
				return value * 2
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)

			innerResolve(2)

			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1][1]).to.equal(4)
		end)

		it("should reject with the value if a Promise from the list rejects", function()
			local called = false
			local promise = Promise.each({ 1, 2, Promise.reject("foobar") }, function(value)
				called = true
				return "never"
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(promise._values[1]).to.equal("foobar")
			expect(called).to.equal(false)
		end)

		it("should reject immediately if there's a cancelled Promise in the list initially", function()
			local cancelled = Promise.new(function() end)
			cancelled:cancel()

			local called = false
			local promise = Promise.each({ 1, 2, cancelled }, function()
				called = true
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(called).to.equal(false)
			expect(promise._values[1].kind).to.equal(Promise.Error.Kind.AlreadyCancelled)
		end)

		it("should stop iteration if Promise.each is cancelled", function()
			local callCounts = {}

			local promise = Promise.each({
				"foo",
				"bar",
				"baz",
			}, function(value, index)
				callCounts[index] = (callCounts[index] or 0) + 1

				return Promise.new(function() end)
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)
			expect(callCounts[1]).to.equal(1)
			expect(callCounts[2]).to.never.be.ok()

			promise:cancel()

			expect(promise:getStatus()).to.equal(Promise.Status.Cancelled)
			expect(callCounts[1]).to.equal(1)
			expect(callCounts[2]).to.never.be.ok()
		end)

		it("should cancel the Promise returned from the predicate if Promise.each is cancelled", function()
			local innerPromise

			local promise = Promise.each({
				"foo",
				"bar",
				"baz",
			}, function(value, index)
				innerPromise = Promise.new(function() end)
				return innerPromise
			end)

			promise:cancel()

			expect(innerPromise:getStatus()).to.equal(Promise.Status.Cancelled)
		end)

		it("should cancel Promises in the list if Promise.each is cancelled", function()
			local innerPromise = Promise.new(function() end)

			local promise = Promise.each({ innerPromise }, function() end)

			promise:cancel()

			expect(innerPromise:getStatus()).to.equal(Promise.Status.Cancelled)
		end)
	end)

	describe("Promise.retry", function()
		it("should retry N times", function()
			local counter = 0

			local promise = Promise.retry(function(parameter)
				expect(parameter).to.equal("foo")

				counter = counter + 1

				if counter == 5 then
					return Promise.resolve("ok")
				end

				return Promise.reject("fail")
			end, 5, "foo")

			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal("ok")
		end)

		it("should reject if threshold is exceeded", function()
			local promise = Promise.retry(function()
				return Promise.reject("fail")
			end, 5)

			expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
			expect(promise._values[1]).to.equal("fail")
		end)
	end)

	describe("Promise.retryWithDelay", function()
		it("should retry after a delay", function()
			local counter = 0

			local promise = Promise.retryWithDelay(function(parameter)
				expect(parameter).to.equal("foo")

				counter = counter + 1

				if counter == 3 then
					return Promise.resolve("ok")
				end

				return Promise.reject("fail")
			end, 3, 10, "foo")

			expect(counter).to.equal(1)

			advanceTime(11)

			expect(counter).to.equal(2)

			advanceTime(11)

			expect(counter).to.equal(3)

			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal("ok")
		end)
	end)

	describe("Promise.fromEvent", function()
		it("should convert a Promise into an event", function()
			local event = Instance.new("BindableEvent")

			local promise = Promise.fromEvent(event.Event)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)

			event:Fire("foo")

			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal("foo")
		end)

		it("should convert a Promise into an event with the predicate", function()
			local event = Instance.new("BindableEvent")

			local promise = Promise.fromEvent(event.Event, function(param)
				return param == "foo"
			end)

			expect(promise:getStatus()).to.equal(Promise.Status.Started)

			event:Fire("bar")

			expect(promise:getStatus()).to.equal(Promise.Status.Started)

			event:Fire("foo")

			expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
			expect(promise._values[1]).to.equal("foo")
		end)
	end)

	describe("Promise.is", function()
		it("should work with current version", function()
			local promise = Promise.resolve(1)

			expect(Promise.is(promise)).to.equal(true)
		end)

		it("should work with any object with an andThen", function()
			local obj = {
				andThen = function()
					return 1
				end,
			}

			expect(Promise.is(obj)).to.equal(true)
		end)

		it("should work with older promises", function()
			local OldPromise = {}
			OldPromise.prototype = {}
			OldPromise.__index = OldPromise.prototype

			function OldPromise.prototype:andThen() end

			local oldPromise = setmetatable({}, OldPromise)

			expect(Promise.is(oldPromise)).to.equal(true)
		end)
	end)
end
]]></string>
              </Properties>
            </Item>
          </Item>
        </Item>
        <Item class="Folder" referent="11">
          <Properties>
            <string name="Name">jakebball_profileservice@1.5.0</string>
          </Properties>
          <Item class="ModuleScript" referent="12">
            <Properties>
              <string name="Name">profileservice</string>
              <string name="Source">-- local Madwork = _G.Madwork
--[[
{Madwork}

-[ProfileService]---------------------------------------
	(STANDALONE VERSION)
	DataStore profiles - universal session-locked savable table API
	
	Official documentation:
		https://madstudioroblox.github.io/ProfileService/

	DevForum discussion:
		https://devforum.roblox.com/t/ProfileService/667805
	
	WARNINGS FOR "Profile.Data" VALUES:
	 	! Do not create numeric tables with gaps - attempting to replicate such tables will result in an error;
		! Do not create mixed tables (some values indexed by number and others by string key), as only
		     the data indexed by number will be replicated.
		! Do not index tables by anything other than numbers and strings.
		! Do not reference Roblox Instances
		! Do not reference userdata (Vector3, Color3, CFrame...) - Serialize userdata before referencing
		! Do not reference functions
		
	WARNING: Calling ProfileStore:LoadProfileAsync() with a "profile_key" which wasn't released in the SAME SESSION will result
		in an error! If you want to "ProfileStore:LoadProfileAsync()" instead of using the already loaded profile, :Release()
		the old Profile object.
		
	Members:
	
		ProfileService.ServiceLocked         [bool]
		
		ProfileService.IssueSignal           [ScriptSignal] (error_message, profile_store_name, profile_key)
		ProfileService.CorruptionSignal      [ScriptSignal] (profile_store_name, profile_key)
		ProfileService.CriticalStateSignal   [ScriptSignal] (is_critical_state)
	
	Functions:
	
		ProfileService.GetProfileStore(profile_store_index, profile_template) --> [ProfileStore]
			profile_store_index   [string] -- DataStore name
			OR
			profile_store_index   [table]: -- Allows the developer to define more GlobalDataStore variables
				{
					Name = "StoreName", -- [string] -- DataStore name
					-- Optional arguments:
					Scope = "StoreScope", -- [string] -- DataStore scope
				}
			profile_template      [table] -- Profiles will default to given table (hard-copy) when no data was saved previously

		ProfileService.IsLive() --> [bool] -- (CAN YIELD!!!)
			-- Returns true if ProfileService is connected to live Roblox DataStores
				
	Members [ProfileStore]:
	
		ProfileStore.Mock   [ProfileStore] -- Reflection of ProfileStore methods, but the methods will use a mock DataStore
		
	Methods [ProfileStore]:
	
		ProfileStore:LoadProfileAsync(profile_key, not_released_handler) --> [Profile] or nil -- not_released_handler(place_id, game_job_id)
			profile_key            [string] -- DataStore key
			not_released_handler   nil or []: -- Defaults to "ForceLoad"
				[string] "ForceLoad" -- Force loads profile on first call
				OR
				[string] "Steal" -- Steals the profile ignoring it's session lock
				OR
				[function] (place_id, game_job_id) --> [string] "Repeat", "Cancel", "ForceLoad" or "Steal"
					place_id      [number] or nil
					game_job_id   [string] or nil

				-- not_released_handler [function] will be triggered in cases where the profile is not released by a session. This
				--	function may yield for as long as desirable and must return one of three string values:

						["Repeat"] - ProfileService will repeat the profile loading proccess and may trigger the release handler again
						["Cancel"] - ProfileStore:LoadProfileAsync() will immediately return nil
						["ForceLoad"] - ProfileService will repeat the profile loading call, but will return Profile object afterwards
							and release the profile for another session that has loaded the profile
						["Steal"] - The profile will usually be loaded immediately, ignoring an existing remote session lock and applying
							a session lock for this session.

		ProfileStore:GlobalUpdateProfileAsync(profile_key, update_handler) --> [GlobalUpdates] or nil
			-- Returns GlobalUpdates object if update was successful, otherwise returns nil
			profile_key      [string] -- DataStore key
			update_handler   [function] (global_updates [GlobalUpdates])
			
		ProfileStore:ViewProfileAsync(profile_key, version) --> [Profile] or nil
			-- Reads profile without requesting a session lock; Data will not be saved and profile doesn't need to be released
			profile_key   [string] -- DataStore key
			version       nil or [string] -- DataStore key version

		ProfileStore:ProfileVersionQuery(profile_key, sort_direction, min_date, max_date) --> [ProfileVersionQuery]
			profile_key      [string]
			sort_direction   nil or [Enum.SortDirection]
			min_date         nil or [DateTime]
			max_date         nil or [DateTime]
			
		ProfileStore:WipeProfileAsync(profile_key) --> is_wipe_successful [bool]
			-- Completely wipes out profile data from the DataStore / mock DataStore with no way to recover it.
						
		* Parameter description for "ProfileStore:GlobalUpdateProfileAsync()":
		
			profile_key      [string] -- DataStore key
			update_handler   [function] (GlobalUpdates) -- This function gains access to GlobalUpdates object methods
				(update_handler can't yield)

	Methods [ProfileVersionQuery]:

		ProfileVersionQuery:NextAsync() --> [Profile] or nil -- (Yields)
			-- Returned profile has the same rules as profile returned by :ViewProfileAsync()
		
	Members [Profile]:
	
		Profile.Data              [table] -- Writable table that gets saved automatically and once the profile is released
		Profile.MetaData          [table] (Read-only) -- Information about this profile
		
			Profile.MetaData.ProfileCreateTime   [number] (Read-only) -- os.time() timestamp of profile creation
			Profile.MetaData.SessionLoadCount    [number] (Read-only) -- Amount of times the profile was loaded
			Profile.MetaData.ActiveSession       [table] (Read-only) {place_id, game_job_id} / nil -- Set to a session link if a
				game session is currently having this profile loaded; nil if released
			Profile.MetaData.MetaTags            [table] {["tag_name"] = tag_value, ...} -- Saved and auto-saved just like Profile.Data
			Profile.MetaData.MetaTagsLatest      [table] (Read-only) -- Latest version of MetaData.MetaTags that was definetly saved to DataStore
				(You can use Profile.MetaData.MetaTagsLatest for product purchase save confirmation, but create a system to clear old tags after
				they pile up)

		Profile.MetaTagsUpdated   [ScriptSignal] (meta_tags_latest) -- Fires after every auto-save, after
			--	Profile.MetaData.MetaTagsLatest has been updated with the version that's guaranteed to be saved;
			--  .MetaTagsUpdated will fire regardless of whether .MetaTagsLatest changed after update;
			--	.MetaTagsUpdated may fire after the Profile is released - changes to Profile.Data are not saved
			--	after release.

		Profile.RobloxMetaData    [table] -- Writable table that gets saved automatically and once the profile is released
		Profile.UserIds           [table] -- (Read-only) -- {user_id [number], ...} -- User ids associated with this profile

		Profile.KeyInfo           [DataStoreKeyInfo]
		Profile.KeyInfoUpdated    [ScriptSignal] (key_info [DataStoreKeyInfo])
		
		Profile.GlobalUpdates     [GlobalUpdates]
		
	Methods [Profile]:
	
		-- SAFE METHODS - Will not error after profile expires:
		Profile:IsActive() --> [bool] -- Returns true while the profile is active and can be written to
			
		Profile:GetMetaTag(tag_name) --> value [any]
			tag_name   [string]
		
		Profile:Reconcile() -- Fills in missing (nil) [string_key] = [value] pairs to the Profile.Data structure
		
		Profile:ListenToRelease(listener) --> [ScriptConnection] (place_id / nil, game_job_id / nil)
			-- WARNING: Profiles can be released externally if another session force-loads
			--	this profile - use :ListenToRelease() to handle player leaving cleanup.
			
		Profile:Release() -- Call after the session has finished working with this profile
			e.g., after the player leaves (Profile object will become expired) (Does not yield)

		Profile:ListenToHopReady(listener) --> [ScriptConnection] () -- Passed listener will be executed after the releasing UpdateAsync call finishes;
			--	Wrap universe teleport requests with this method AFTER releasing the profile to improve session lock sharing between universe places;
			--  :ListenToHopReady() will usually call the listener in around a second, but may ocassionally take up to 7 seconds when a release happens
			--	next to an auto-update in regular usage scenarios.

		Profile:AddUserId(user_id) -- Associates user_id with profile (GDPR compliance)
			user_id   [number]

		Profile:RemoveUserId(user_id) -- Unassociates user_id with profile (safe function)
			user_id   [number]

		Profile:Identify() --> [string] -- Returns a string containing DataStore name, scope and key; Used for debug;
			-- Example return: "[Store:"GameData";Scope:"Live";Key:"Player_2312310"]"
		
		Profile:SetMetaTag(tag_name, value) -- Equivalent of Profile.MetaData.MetaTags[tag_name] = value
			tag_name   [string]
			value      [any]
		
		Profile:Save() -- Call to quickly progress global update state or to speed up save validation processes (Does not yield)

		-- VIEW-MODE ONLY:

		Profile:ClearGlobalUpdates() -- Clears all global updates data from a profile payload

		Profile:OverwriteAsync() -- (Yields) Saves the profile payload to the DataStore and removes the session lock
		
	Methods [GlobalUpdates]:
	
	-- ALWAYS PUBLIC:
		GlobalUpdates:GetActiveUpdates() --> [table] {{update_id, update_data [table]}, ...}
		GlobalUpdates:GetLockedUpdates() --> [table] {{update_id, update_data [table]}, ...}
		
	-- ONLY WHEN FROM "Profile.GlobalUpdates":
		GlobalUpdates:ListenToNewActiveUpdate(listener) --> [ScriptConnection] (update_id, update_data)
			update_data   [table]
		GlobalUpdates:ListenToNewLockedUpdate(listener) --> [ScriptConnection] (update_id, update_data)
			update_data   [table]
		GlobalUpdates:LockActiveUpdate(update_id)  -- WARNING: will error after profile expires
		GlobalUpdates:ClearLockedUpdate(update_id) -- WARNING: will error after profile expires
		
	-- EXPOSED TO "update_handler" DURING ProfileStore:GlobalUpdateProfileAsync() CALL
		GlobalUpdates:AddActiveUpdate(update_data)
			update_data   [table]
		GlobalUpdates:ChangeActiveUpdate(update_id, update_data)
			update_data   [table]
		GlobalUpdates:ClearActiveUpdate(update_id)
		
--]]

local SETTINGS = {

	AutoSaveProfiles = 30, -- Seconds (This value may vary - ProfileService will split the auto save load evenly in the given time)
	RobloxWriteCooldown = 7, -- Seconds between successive DataStore calls for the same key
	ForceLoadMaxSteps = 8, -- Steps taken before ForceLoad request steals the active session for a profile
	AssumeDeadSessionLock = 30 * 60, -- (seconds) If a profile hasn't been updated for 30 minutes, assume the session lock is dead
		-- As of writing, os.time() is not completely reliable, so we can only assume session locks are dead after a significant amount of time.
	
	IssueCountForCriticalState = 5, -- Issues to collect to announce critical state
	IssueLast = 120, -- Seconds
	CriticalStateLast = 120, -- Seconds
	
	MetaTagsUpdatedValues = { -- Technical stuff - do not alter
		ProfileCreateTime = true,
		SessionLoadCount = true,
		ActiveSession = true,
		ForceLoadSession = true,
		LastUpdate = true,
	},
	
}

local Madwork -- Standalone Madwork reference for portable version of ProfileService
do

	local MadworkScriptSignal = {}

	local FreeRunnerThread = nil
	
	local function AcquireRunnerThreadAndCallEventHandler(fn, ...)
		local acquired_runner_thread = FreeRunnerThread
		FreeRunnerThread = nil
		fn(...)
		FreeRunnerThread = acquired_runner_thread
	end
	
	local function RunEventHandlerInFreeThread(...)
		AcquireRunnerThreadAndCallEventHandler(...)
		while true do
			AcquireRunnerThreadAndCallEventHandler(coroutine.yield())
		end
	end
	
	-- ScriptConnection object:

	local ScriptConnection = {
		--[[
			_listener = listener,
			_script_signal = script_signal,
			_disconnect_listener = disconnect_listener,
			_disconnect_param = disconnect_param,
			
			_next = next_script_connection,
			_is_connected = is_connected,
		--]]
	}
	ScriptConnection.__index = ScriptConnection

	function ScriptConnection:Disconnect()

		if self._is_connected == false then
			return
		end

		self._is_connected = false
		self._script_signal._listener_count -= 1

		if self._script_signal._head == self then
			self._script_signal._head = self._next
		else
			local prev = self._script_signal._head
			while prev ~= nil and prev._next ~= self do
				prev = prev._next
			end
			if prev ~= nil then
				prev._next = self._next
			end
		end

		if self._disconnect_listener ~= nil then
			if not FreeRunnerThread then
				FreeRunnerThread = coroutine.create(RunEventHandlerInFreeThread)
			end
			task.spawn(FreeRunnerThread, self._disconnect_listener, self._disconnect_param)
			self._disconnect_listener = nil
		end

	end
	
	-- ScriptSignal object:

	local ScriptSignal = {
		--[[
			_head = nil,
			_listener_count = 0,
		--]]
	}
	ScriptSignal.__index = ScriptSignal

	function ScriptSignal:Connect(listener, disconnect_listener, disconnect_param) --> [ScriptConnection]

		local script_connection = {
			_listener = listener,
			_script_signal = self,
			_disconnect_listener = disconnect_listener,
			_disconnect_param = disconnect_param,

			_next = self._head,
			_is_connected = true,
		}
		setmetatable(script_connection, ScriptConnection)

		self._head = script_connection
		self._listener_count += 1

		return script_connection

	end

	function ScriptSignal:GetListenerCount() --> [number]
		return self._listener_count
	end

	function ScriptSignal:Fire(...)
		local item = self._head
		while item ~= nil do
			if item._is_connected == true then
				if not FreeRunnerThread then
					FreeRunnerThread = coroutine.create(RunEventHandlerInFreeThread)
				end
				task.spawn(FreeRunnerThread, item._listener, ...)
			end
			item = item._next
		end
	end

	function ScriptSignal:FireUntil(continue_callback, ...)
		local item = self._head
		while item ~= nil do
			if item._is_connected == true then
				item._listener(...)
				if continue_callback() ~= true then
					return
				end
			end
			item = item._next
		end
	end

	function MadworkScriptSignal.NewScriptSignal() --> [ScriptSignal]
		return {
			_head = nil,
			_listener_count = 0,
			Connect = ScriptSignal.Connect,
			GetListenerCount = ScriptSignal.GetListenerCount,
			Fire = ScriptSignal.Fire,
			FireUntil = ScriptSignal.FireUntil,
		}
	end

	-- Madwork framework namespace:
	
	Madwork = {
		NewScriptSignal = MadworkScriptSignal.NewScriptSignal,
		ConnectToOnClose = function(task, run_in_studio_mode)
			if game:GetService("RunService"):IsStudio() == false or run_in_studio_mode == true then
				game:BindToClose(task)
			end
		end,
	}

end

----- Service Table -----

local ProfileService = {

	ServiceLocked = false, -- Set to true once the server is shutting down

	IssueSignal = Madwork.NewScriptSignal(), -- (error_message, profile_store_name, profile_key) -- Fired when a DataStore API call throws an error
	CorruptionSignal = Madwork.NewScriptSignal(), -- (profile_store_name, profile_key) -- Fired when DataStore key returns a value that has
	-- all or some of it's profile components set to invalid data types. E.g., accidentally setting Profile.Data to a noon table value

	CriticalState = false, -- Set to true while DataStore service is throwing too many errors
	CriticalStateSignal = Madwork.NewScriptSignal(), -- (is_critical_state) -- Fired when CriticalState is set to true
	-- (You may alert players with this, or set up analytics)

	ServiceIssueCount = 0,

	_active_profile_stores = {}, -- {profile_store, ...}

	_auto_save_list = {}, -- {profile, ...} -- loaded profile table which will be circularly auto-saved

	_issue_queue = {}, -- [table] {issue_time, ...}
	_critical_state_start = 0, -- [number] 0 = no critical state / os.clock() = critical state start

	-- Debug:
	_mock_data_store = {},
	_user_mock_data_store = {},

	_use_mock_data_store = false,

}

--[[
	Saved profile structure:
	
	DataStoreProfile = {
		Data = {},
		MetaData = {
			ProfileCreateTime = 0,
			SessionLoadCount = 0,
			ActiveSession = {place_id, game_job_id} / nil,
			ForceLoadSession = {place_id, game_job_id} / nil,
			MetaTags = {},
			LastUpdate = 0, -- os.time()
		},
		RobloxMetaData = {},
		UserIds = {},
		GlobalUpdates = {
			update_index,
			{
				{update_id, version_id, update_locked, update_data},
				...
			}
		},
	}
	
	OR
	
	DataStoreProfile = {
		GlobalUpdates = {
			update_index,
			{
				{update_id, version_id, update_locked, update_data},
				...
			}
		},
	}
--]]

----- Private Variables -----

local ActiveProfileStores = ProfileService._active_profile_stores
local AutoSaveList = ProfileService._auto_save_list
local IssueQueue = ProfileService._issue_queue

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

local PlaceId = game.PlaceId
local JobId = game.JobId

local AutoSaveIndex = 1 -- Next profile to auto save
local LastAutoSave = os.clock()

local LoadIndex = 0

local ActiveProfileLoadJobs = 0 -- Number of active threads that are loading in profiles
local ActiveProfileSaveJobs = 0 -- Number of active threads that are saving profiles

local CriticalStateStart = 0 -- os.clock()

local IsStudio = RunService:IsStudio()
local IsLiveCheckActive = false

local UseMockDataStore = false
local MockDataStore = ProfileService._mock_data_store -- Mock data store used when API access is disabled

local UserMockDataStore = ProfileService._user_mock_data_store -- Separate mock data store accessed via ProfileStore.Mock
local UseMockTag = {}

local CustomWriteQueue = {
	--[[
		[store] = {
			[key] = {
				LastWrite = os.clock(),
				Queue = {callback, ...},
				CleanupJob = nil,
			},
			...
		},
		...
	--]]
}

----- Utils -----

local function DeepCopyTable(t)
	local copy = {}
	for key, value in pairs(t) do
		if type(value) == "table" then
			copy[key] = DeepCopyTable(value)
		else
			copy[key] = value
		end
	end
	return copy
end

local function ReconcileTable(target, template)
	for k, v in pairs(template) do
		if type(k) == "string" then -- Only string keys will be reconciled
			if target[k] == nil then
				if type(v) == "table" then
					target[k] = DeepCopyTable(v)
				else
					target[k] = v
				end
			elseif type(target[k]) == "table" and type(v) == "table" then
				ReconcileTable(target[k], v)
			end
		end
	end
end

----- Private functions -----

local function IdentifyProfile(store_name, store_scope, key)
	return string.format(
		"[Store:\"%s\";%sKey:\"%s\"]",
		store_name,
		store_scope ~= nil and string.format("Scope:\"%s\";", store_scope) or "",
		key
	)
end

local function CustomWriteQueueCleanup(store, key)
	if CustomWriteQueue[store] ~= nil then
		CustomWriteQueue[store][key] = nil
		if next(CustomWriteQueue[store]) == nil then
			CustomWriteQueue[store] = nil
		end
	end
end

local function CustomWriteQueueMarkForCleanup(store, key)
	if CustomWriteQueue[store] ~= nil then
		if CustomWriteQueue[store][key] ~= nil then

			local queue_data = CustomWriteQueue[store][key]
			local queue = queue_data.Queue

			if queue_data.CleanupJob == nil then

				queue_data.CleanupJob = RunService.Heartbeat:Connect(function()
					if os.clock() - queue_data.LastWrite > SETTINGS.RobloxWriteCooldown and #queue == 0 then
						queue_data.CleanupJob:Disconnect()
						CustomWriteQueueCleanup(store, key)
					end
				end)

			end

		elseif next(CustomWriteQueue[store]) == nil then
			CustomWriteQueue[store] = nil
		end
	end
end

local function CustomWriteQueueAsync(callback, store, key) --> ... -- Passed return from callback

	if CustomWriteQueue[store] == nil then
		CustomWriteQueue[store] = {}
	end
	if CustomWriteQueue[store][key] == nil then
		CustomWriteQueue[store][key] = {LastWrite = 0, Queue = {}, CleanupJob = nil}
	end

	local queue_data = CustomWriteQueue[store][key]
	local queue = queue_data.Queue

	-- Cleanup job:

	if queue_data.CleanupJob ~= nil then
		queue_data.CleanupJob:Disconnect()
		queue_data.CleanupJob = nil
	end

	-- Queue logic:

	if os.clock() - queue_data.LastWrite > SETTINGS.RobloxWriteCooldown and #queue == 0 then
		queue_data.LastWrite = os.clock()
		return callback()
	else
		table.insert(queue, callback)
		while true do
			if os.clock() - queue_data.LastWrite > SETTINGS.RobloxWriteCooldown and queue[1] == callback then
				table.remove(queue, 1)
				queue_data.LastWrite = os.clock()
				return callback()
			end
			task.wait()
		end
	end

end

local function IsCustomWriteQueueEmptyFor(store, key) --> is_empty [bool]
	local lookup = CustomWriteQueue[store]
	if lookup ~= nil then
		lookup = lookup[key]
		return lookup == nil or #lookup.Queue == 0
	end
	return true
end

local function WaitForLiveAccessCheck() -- This function was created to prevent the ProfileService module yielding execution when required
	while IsLiveCheckActive == true do
		task.wait()
	end
end

local function WaitForPendingProfileStore(profile_store)
	while profile_store._is_pending == true do
		task.wait()
	end
end

local function RegisterIssue(error_message, store_name, store_scope, profile_key) -- Called when a DataStore API call errors
	warn("[ProfileService]: DataStore API error " .. IdentifyProfile(store_name, store_scope, profile_key) .. " - \"" .. tostring(error_message) .. "\"")
	table.insert(IssueQueue, os.clock()) -- Adding issue time to queue
	ProfileService.IssueSignal:Fire(tostring(error_message), store_name, profile_key)
end

local function RegisterCorruption(store_name, store_scope, profile_key) -- Called when a corrupted profile is loaded
	warn("[ProfileService]: Resolved profile corruption " .. IdentifyProfile(store_name, store_scope, profile_key))
	ProfileService.CorruptionSignal:Fire(store_name, profile_key)
end

local function NewMockDataStoreKeyInfo(params)

	local version_id_string = tostring(params.VersionId or 0)
	local meta_data = params.MetaData or {}
	local user_ids = params.UserIds or {}

	return {
		CreatedTime = params.CreatedTime,
		UpdatedTime = params.UpdatedTime,
		Version = string.rep("0", 16) .. "."
			.. string.rep("0", 10 - string.len(version_id_string)) .. version_id_string
			.. "." .. string.rep("0", 16) .. "." .. "01",

		GetMetadata = function()
			return DeepCopyTable(meta_data)
		end,

		GetUserIds = function()
			return DeepCopyTable(user_ids)
		end,
	}

end

local function MockUpdateAsync(mock_data_store, profile_store_name, key, transform_function, is_get_call) --> loaded_data, key_info

	local profile_store = mock_data_store[profile_store_name]

	if profile_store == nil then
		profile_store = {}
		mock_data_store[profile_store_name] = profile_store
	end

	local epoch_time = math.floor(os.time() * 1000)
	local mock_entry = profile_store[key]
	local mock_entry_was_nil = false

	if mock_entry == nil then
		mock_entry_was_nil = true
		if is_get_call ~= true then
			mock_entry = {
				Data = nil,
				CreatedTime = epoch_time,
				UpdatedTime = epoch_time,
				VersionId = 0,
				UserIds = {},
				MetaData = {},
			}
			profile_store[key] = mock_entry
		end
	end

	local mock_key_info = mock_entry_was_nil == false and NewMockDataStoreKeyInfo(mock_entry) or nil

	local transform, user_ids, roblox_meta_data = transform_function(mock_entry and mock_entry.Data, mock_key_info)

	if transform == nil then
		return nil
	else
		if mock_entry ~= nil and is_get_call ~= true then
			mock_entry.Data = transform
			mock_entry.UserIds = DeepCopyTable(user_ids or {})
			mock_entry.MetaData = DeepCopyTable(roblox_meta_data or {})
			mock_entry.VersionId += 1
			mock_entry.UpdatedTime = epoch_time
		end

		return DeepCopyTable(transform), mock_entry ~= nil and NewMockDataStoreKeyInfo(mock_entry) or nil
	end

end

local function IsThisSession(session_tag)
	return session_tag[1] == PlaceId and session_tag[2] == JobId
end

--[[
update_settings = {
	ExistingProfileHandle = function(latest_data),
	MissingProfileHandle = function(latest_data),
	EditProfile = function(lastest_data),
}
--]]
local function StandardProfileUpdateAsyncDataStore(profile_store, profile_key, update_settings, is_user_mock, is_get_call, version) --> loaded_data, key_info
	local loaded_data, key_info
	local success, error_message = pcall(function()
		local transform_function = function(latest_data)

			local missing_profile = false
			local data_corrupted = false
			local global_updates_data = {0, {}}

			if latest_data == nil then
				missing_profile = true
			elseif type(latest_data) ~= "table" then
				missing_profile = true
				data_corrupted = true
			end

			if type(latest_data) == "table" then
				-- Case #1: Profile was loaded
				if type(latest_data.Data) == "table"
					and type(latest_data.MetaData) == "table"
					and type(latest_data.GlobalUpdates) == "table" then

					latest_data.WasCorrupted = false -- Must be set to false if set previously
					global_updates_data = latest_data.GlobalUpdates
					if update_settings.ExistingProfileHandle ~= nil then
						update_settings.ExistingProfileHandle(latest_data)
					end
					-- Case #2: Profile was not loaded but GlobalUpdate data exists
				elseif latest_data.Data == nil
					and latest_data.MetaData == nil
					and type(latest_data.GlobalUpdates) == "table" then

					latest_data.WasCorrupted = false -- Must be set to false if set previously
					global_updates_data = latest_data.GlobalUpdates or global_updates_data
					missing_profile = true
				else
					missing_profile = true
					data_corrupted = true
				end
			end

			-- Case #3: Profile was not created or corrupted and no GlobalUpdate data exists
			if missing_profile == true then
				latest_data = {
					-- Data = nil,
					-- MetaData = nil,
					GlobalUpdates = global_updates_data,
				}
				if update_settings.MissingProfileHandle ~= nil then
					update_settings.MissingProfileHandle(latest_data)
				end
			end

			-- Editing profile:
			if update_settings.EditProfile ~= nil then
				update_settings.EditProfile(latest_data)
			end

			-- Data corruption handling (Silently override with empty profile) (Also run Case #1)
			if data_corrupted == true then
				latest_data.WasCorrupted = true -- Temporary tag that will be removed on first save
			end

			return latest_data, latest_data.UserIds, latest_data.RobloxMetaData
		end
		if is_user_mock == true then -- Used when the profile is accessed through ProfileStore.Mock
			loaded_data, key_info = MockUpdateAsync(UserMockDataStore, profile_store._profile_store_lookup, profile_key, transform_function, is_get_call)
			task.wait() -- Simulate API call yield
		elseif UseMockDataStore == true then -- Used when API access is disabled
			loaded_data, key_info = MockUpdateAsync(MockDataStore, profile_store._profile_store_lookup, profile_key, transform_function, is_get_call)
			task.wait() -- Simulate API call yield
		else
			loaded_data, key_info = CustomWriteQueueAsync(
				function() -- Callback
					if is_get_call == true then
						local get_data, get_key_info
						if version ~= nil then
							local success, error_message = pcall(function()
								get_data, get_key_info = profile_store._global_data_store:GetVersionAsync(profile_key, version)
							end)
							if success == false and type(error_message) == "string" and string.find(error_message, "not valid") ~= nil then
								warn("[ProfileService]: Passed version argument is not valid; Traceback:\n" .. debug.traceback())
							end
						else
							get_data, get_key_info = profile_store._global_data_store:GetAsync(profile_key)
						end
						get_data = transform_function(get_data)
						return get_data, get_key_info
					else
						return profile_store._global_data_store:UpdateAsync(profile_key, transform_function)
					end
				end,
				profile_store._profile_store_lookup, -- Store
				profile_key -- Key
			)
		end
	end)
	if success == true and type(loaded_data) == "table" then
		-- Corruption handling:
		if loaded_data.WasCorrupted == true and is_get_call ~= true then
			RegisterCorruption(
				profile_store._profile_store_name,
				profile_store._profile_store_scope,
				profile_key
			)
		end
		-- Return loaded_data:
		return loaded_data, key_info
	else
		RegisterIssue(
			(error_message ~= nil) and error_message or "Undefined error",
			profile_store._profile_store_name,
			profile_store._profile_store_scope,
			profile_key
		)
		-- Return nothing:
		return nil
	end
end

local function RemoveProfileFromAutoSave(profile)
	local auto_save_index = table.find(AutoSaveList, profile)
	if auto_save_index ~= nil then
		table.remove(AutoSaveList, auto_save_index)
		if auto_save_index &lt; AutoSaveIndex then
			AutoSaveIndex = AutoSaveIndex - 1 -- Table contents were moved left before AutoSaveIndex so move AutoSaveIndex left as well
		end
		if AutoSaveList[AutoSaveIndex] == nil then -- AutoSaveIndex was at the end of the AutoSaveList - reset to 1
			AutoSaveIndex = 1
		end
	end
end

local function AddProfileToAutoSave(profile) -- Notice: Makes sure this profile isn't auto-saved too soon
	-- Add at AutoSaveIndex and move AutoSaveIndex right:
	table.insert(AutoSaveList, AutoSaveIndex, profile)
	if #AutoSaveList > 1 then
		AutoSaveIndex = AutoSaveIndex + 1
	elseif #AutoSaveList == 1 then
		-- First profile created - make sure it doesn't get immediately auto saved:
		LastAutoSave = os.clock()
	end
end

local function ReleaseProfileInternally(profile)
	-- 1) Remove profile object from ProfileService references: --
	-- Clear reference in ProfileStore:
	local profile_store = profile._profile_store
	local loaded_profiles = profile._is_user_mock == true and profile_store._mock_loaded_profiles or profile_store._loaded_profiles
	loaded_profiles[profile._profile_key] = nil
	if next(profile_store._loaded_profiles) == nil and next(profile_store._mock_loaded_profiles) == nil then -- ProfileStore has turned inactive
		local index = table.find(ActiveProfileStores, profile_store)
		if index ~= nil then
			table.remove(ActiveProfileStores, index)
		end
	end
	-- Clear auto update reference:
	RemoveProfileFromAutoSave(profile)
	-- 2) Trigger release listeners: --
	local place_id
	local game_job_id
	local active_session = profile.MetaData.ActiveSession
	if active_session ~= nil then
		place_id = active_session[1]
		game_job_id = active_session[2]
	end
	profile._release_listeners:Fire(place_id, game_job_id)
end

local function CheckForNewGlobalUpdates(profile, old_global_updates_data, new_global_updates_data)
	local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates]
	local pending_update_lock = global_updates_object._pending_update_lock -- {update_id, ...}
	local pending_update_clear = global_updates_object._pending_update_clear -- {update_id, ...}
	-- "old_" or "new_" global_updates_data = {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
	for _, new_global_update in ipairs(new_global_updates_data[2]) do
		-- Find old global update with the same update_id:
		local old_global_update
		for _, global_update in ipairs(old_global_updates_data[2]) do
			if global_update[1] == new_global_update[1] then
				old_global_update = global_update
				break
			end
		end
		-- A global update is new when it didn't exist before or its version_id or update_locked state changed:
		local is_new = false
		if old_global_update == nil or new_global_update[2] > old_global_update[2] or new_global_update[3] ~= old_global_update[3] then
			is_new = true
		end
		if is_new == true then
			-- Active global updates:
			if new_global_update[3] == false then
				-- Check if update is not pending to be locked: (Preventing firing new active update listeners more than necessary)
				local is_pending_lock = false
				for _, update_id in ipairs(pending_update_lock) do
					if new_global_update[1] == update_id then
						is_pending_lock = true
						break
					end
				end
				if is_pending_lock == false then
					-- Trigger new active update listeners:
					global_updates_object._new_active_update_listeners:Fire(new_global_update[1], new_global_update[4])
				end
			end
			-- Locked global updates:
			if new_global_update[3] == true then
				-- Check if update is not pending to be cleared: (Preventing firing new locked update listeners after marking a locked update for clearing)
				local is_pending_clear = false
				for _, update_id in ipairs(pending_update_clear) do
					if new_global_update[1] == update_id then
						is_pending_clear = true
						break
					end
				end
				if is_pending_clear == false then
					-- Trigger new locked update listeners:

					global_updates_object._new_locked_update_listeners:FireUntil(
						function()
							-- Check if listener marked the update to be cleared:
							-- Normally there should be only one listener per profile for new locked global updates, but
							-- in case several listeners are connected we will not trigger more listeners after one listener
							-- marks the locked global update to be cleared.
							return table.find(pending_update_clear, new_global_update[1]) == nil
						end,
						new_global_update[1], new_global_update[4]
					)

				end
			end
		end
	end
end

local function SaveProfileAsync(profile, release_from_session, is_overwriting)
	if type(profile.Data) ~= "table" then
		RegisterCorruption(
			profile._profile_store._profile_store_name,
			profile._profile_store._profile_store_scope,
			profile._profile_key
		)
		error("[ProfileService]: PROFILE DATA CORRUPTED DURING RUNTIME! Profile: " .. profile:Identify())
	end
	if release_from_session == true and is_overwriting ~= true then
		ReleaseProfileInternally(profile)
	end
	ActiveProfileSaveJobs = ActiveProfileSaveJobs + 1
	local last_session_load_count = profile.MetaData.SessionLoadCount
	-- Compare "SessionLoadCount" when writing to profile to prevent a rare case of repeat last save when the profile is loaded on the same server again
	local repeat_save_flag = true -- Released Profile save calls have to repeat until they succeed
	while repeat_save_flag == true do
		if release_from_session ~= true then
			repeat_save_flag = false
		end
		local loaded_data, key_info = StandardProfileUpdateAsyncDataStore(
			profile._profile_store,
			profile._profile_key,
			{
				ExistingProfileHandle = nil,
				MissingProfileHandle = nil,
				EditProfile = function(latest_data)

					local session_owns_profile = false
					local force_load_pending = false

					if is_overwriting ~= true then
						-- 1) Check if this session still owns the profile: --
						local active_session = latest_data.MetaData.ActiveSession
						local force_load_session = latest_data.MetaData.ForceLoadSession
						local session_load_count = latest_data.MetaData.SessionLoadCount

						if type(active_session) == "table" then
							session_owns_profile = IsThisSession(active_session) and session_load_count == last_session_load_count
						end
						if type(force_load_session) == "table" then
							force_load_pending = not IsThisSession(force_load_session)
						end
					else
						session_owns_profile = true
					end

					if session_owns_profile == true then -- We may only edit the profile if this session has ownership of the profile

						if is_overwriting ~= true then
							-- 2) Manage global updates: --
							local latest_global_updates_data = latest_data.GlobalUpdates -- {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
							local latest_global_updates_list = latest_global_updates_data[2]

							local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates]
							local pending_update_lock = global_updates_object._pending_update_lock -- {update_id, ...}
							local pending_update_clear = global_updates_object._pending_update_clear -- {update_id, ...}
							-- Active update locking:
							for i = 1, #latest_global_updates_list do
								for _, lock_id in ipairs(pending_update_lock) do
									if latest_global_updates_list[i][1] == lock_id then
										latest_global_updates_list[i][3] = true
										break
									end
								end
							end
							-- Locked update clearing:
							for _, clear_id in ipairs(pending_update_clear) do
								for i = 1, #latest_global_updates_list do
									if latest_global_updates_list[i][1] == clear_id and latest_global_updates_list[i][3] == true then
										table.remove(latest_global_updates_list, i)
										break
									end
								end
							end
						end

						-- 3) Save profile data: --
						latest_data.Data = profile.Data
						latest_data.RobloxMetaData = profile.RobloxMetaData
						latest_data.UserIds = profile.UserIds

						if is_overwriting ~= true then
							latest_data.MetaData.MetaTags = profile.MetaData.MetaTags -- MetaData.MetaTags is the only actively savable component of MetaData
							latest_data.MetaData.LastUpdate = os.time()
							if release_from_session == true or force_load_pending == true then
								latest_data.MetaData.ActiveSession = nil
							end
						else
							latest_data.MetaData = profile.MetaData
							latest_data.MetaData.ActiveSession = nil
							latest_data.MetaData.ForceLoadSession = nil
							latest_data.GlobalUpdates = profile.GlobalUpdates._updates_latest
						end

					end
				end,
			},
			profile._is_user_mock
		)
		if loaded_data ~= nil and key_info ~= nil then
			if is_overwriting == true then
				break
			end
			repeat_save_flag = false
			-- 4) Set latest data in profile: --
			-- Updating DataStoreKeyInfo:
			profile.KeyInfo = key_info
			-- Setting global updates:
			local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates]
			local old_global_updates_data = global_updates_object._updates_latest
			local new_global_updates_data = loaded_data.GlobalUpdates
			global_updates_object._updates_latest = new_global_updates_data
			-- Setting MetaData:
			local session_meta_data = profile.MetaData
			local latest_meta_data = loaded_data.MetaData
			for key in pairs(SETTINGS.MetaTagsUpdatedValues) do
				session_meta_data[key] = latest_meta_data[key]
			end
			session_meta_data.MetaTagsLatest = latest_meta_data.MetaTags
			-- 5) Check if session still owns the profile: --
			local active_session = loaded_data.MetaData.ActiveSession
			local session_load_count = loaded_data.MetaData.SessionLoadCount
			local session_owns_profile = false
			if type(active_session) == "table" then
				session_owns_profile = IsThisSession(active_session) and session_load_count == last_session_load_count
			end
			local is_active = profile:IsActive()
			if session_owns_profile == true then
				-- 6) Check for new global updates: --
				if is_active == true then -- Profile could've been released before the saving thread finished
					CheckForNewGlobalUpdates(profile, old_global_updates_data, new_global_updates_data)
				end
			else
				-- Session no longer owns the profile:
				-- 7) Release profile if it hasn't been released yet: --
				if is_active == true then
					ReleaseProfileInternally(profile)
				end
				-- Cleanup reference in custom write queue:
				CustomWriteQueueMarkForCleanup(profile._profile_store._profile_store_lookup, profile._profile_key)
				-- Hop ready listeners:
				if profile._hop_ready == false then
					profile._hop_ready = true
					profile._hop_ready_listeners:Fire()
				end
			end
			-- Signaling MetaTagsUpdated listeners after a possible external profile release was handled:
			profile.MetaTagsUpdated:Fire(profile.MetaData.MetaTagsLatest)
			-- Signaling KeyInfoUpdated listeners:
			profile.KeyInfoUpdated:Fire(key_info)
		elseif repeat_save_flag == true then
			task.wait() -- Prevent infinite loop in case DataStore API does not yield
		end
	end
	ActiveProfileSaveJobs = ActiveProfileSaveJobs - 1
end

----- Public functions -----

-- GlobalUpdates object:

local GlobalUpdates = {
	--[[
		_updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
		_pending_update_lock = {update_id, ...} / nil, -- [table / nil]
		_pending_update_clear = {update_id, ...} / nil, -- [table / nil]
		
		_new_active_update_listeners = [ScriptSignal] / nil, -- [table / nil]
		_new_locked_update_listeners = [ScriptSignal] / nil, -- [table / nil]
		
		_profile = Profile / nil, -- [Profile / nil]
		
		_update_handler_mode = true / nil, -- [bool / nil]
	--]]
}
GlobalUpdates.__index = GlobalUpdates

-- ALWAYS PUBLIC:
function GlobalUpdates:GetActiveUpdates() --> [table] {{update_id, update_data}, ...}
	local query_list = {}
	for _, global_update in ipairs(self._updates_latest[2]) do
		if global_update[3] == false then
			local is_pending_lock = false
			if self._pending_update_lock ~= nil then
				for _, update_id in ipairs(self._pending_update_lock) do
					if global_update[1] == update_id then
						is_pending_lock = true -- Exclude global updates pending to be locked
						break
					end
				end
			end
			if is_pending_lock == false then
				table.insert(query_list, {global_update[1], global_update[4]})
			end
		end
	end
	return query_list
end

function GlobalUpdates:GetLockedUpdates() --> [table] {{update_id, update_data}, ...}
	local query_list = {}
	for _, global_update in ipairs(self._updates_latest[2]) do
		if global_update[3] == true then
			local is_pending_clear = false
			if self._pending_update_clear ~= nil then
				for _, update_id in ipairs(self._pending_update_clear) do
					if global_update[1] == update_id then
						is_pending_clear = true -- Exclude global updates pending to be cleared
						break
					end
				end
			end
			if is_pending_clear == false then
				table.insert(query_list, {global_update[1], global_update[4]})
			end
		end
	end
	return query_list
end

-- ONLY WHEN FROM "Profile.GlobalUpdates":
function GlobalUpdates:ListenToNewActiveUpdate(listener) --> [ScriptConnection] listener(update_id, update_data)
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in GlobalUpdates:ListenToNewActiveUpdate()")
	end
	local profile = self._profile
	if self._update_handler_mode == true then
		error("[ProfileService]: Can't listen to new global updates in ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._new_active_update_listeners == nil then
		error("[ProfileService]: Can't listen to new global updates in view mode")
	elseif profile:IsActive() == false then -- Check if profile is expired
		return { -- Do not connect listener if the profile is expired
			Disconnect = function() end,
		}
	end
	-- Connect listener:
	return self._new_active_update_listeners:Connect(listener)
end

function GlobalUpdates:ListenToNewLockedUpdate(listener) --> [ScriptConnection] listener(update_id, update_data)
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in GlobalUpdates:ListenToNewLockedUpdate()")
	end
	local profile = self._profile
	if self._update_handler_mode == true then
		error("[ProfileService]: Can't listen to new global updates in ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._new_locked_update_listeners == nil then
		error("[ProfileService]: Can't listen to new global updates in view mode")
	elseif profile:IsActive() == false then -- Check if profile is expired
		return { -- Do not connect listener if the profile is expired
			Disconnect = function() end,
		}
	end
	-- Connect listener:
	return self._new_locked_update_listeners:Connect(listener)
end

function GlobalUpdates:LockActiveUpdate(update_id)
	if type(update_id) ~= "number" then
		error("[ProfileService]: Invalid update_id")
	end
	local profile = self._profile
	if self._update_handler_mode == true then
		error("[ProfileService]: Can't lock active global updates in ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._pending_update_lock == nil then
		error("[ProfileService]: Can't lock active global updates in view mode")
	elseif profile:IsActive() == false then -- Check if profile is expired
		error("[ProfileService]: PROFILE EXPIRED - Can't lock active global updates")
	end
	-- Check if global update exists with given update_id
	local global_update_exists = nil
	for _, global_update in ipairs(self._updates_latest[2]) do
		if global_update[1] == update_id then
			global_update_exists = global_update
			break
		end
	end
	if global_update_exists ~= nil then
		local is_pending_lock = false
		for _, lock_update_id in ipairs(self._pending_update_lock) do
			if update_id == lock_update_id then
				is_pending_lock = true -- Exclude global updates pending to be locked
				break
			end
		end
		if is_pending_lock == false and global_update_exists[3] == false then -- Avoid id duplicates in _pending_update_lock
			table.insert(self._pending_update_lock, update_id)
		end
	else
		error("[ProfileService]: Passed non-existant update_id")
	end
end

function GlobalUpdates:ClearLockedUpdate(update_id)
	if type(update_id) ~= "number" then
		error("[ProfileService]: Invalid update_id")
	end
	local profile = self._profile
	if self._update_handler_mode == true then
		error("[ProfileService]: Can't clear locked global updates in ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._pending_update_clear == nil then
		error("[ProfileService]: Can't clear locked global updates in view mode")
	elseif profile:IsActive() == false then -- Check if profile is expired
		error("[ProfileService]: PROFILE EXPIRED - Can't clear locked global updates")
	end
	-- Check if global update exists with given update_id
	local global_update_exists = nil
	for _, global_update in ipairs(self._updates_latest[2]) do
		if global_update[1] == update_id then
			global_update_exists = global_update
			break
		end
	end
	if global_update_exists ~= nil then
		local is_pending_clear = false
		for _, clear_update_id in ipairs(self._pending_update_clear) do
			if update_id == clear_update_id then
				is_pending_clear = true -- Exclude global updates pending to be cleared
				break
			end
		end
		if is_pending_clear == false and global_update_exists[3] == true then -- Avoid id duplicates in _pending_update_clear
			table.insert(self._pending_update_clear, update_id)
		end
	else
		error("[ProfileService]: Passed non-existant update_id")
	end
end

-- EXPOSED TO "update_handler" DURING ProfileStore:GlobalUpdateProfileAsync() CALL
function GlobalUpdates:AddActiveUpdate(update_data)
	if type(update_data) ~= "table" then
		error("[ProfileService]: Invalid update_data")
	end
	if self._new_active_update_listeners ~= nil then
		error("[ProfileService]: Can't add active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._update_handler_mode ~= true then
		error("[ProfileService]: Can't add active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()")
	end
	-- self._updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
	local updates_latest = self._updates_latest
	local update_index = updates_latest[1] + 1 -- Incrementing global update index
	updates_latest[1] = update_index
	-- Add new active global update:
	table.insert(updates_latest[2], {update_index, 1, false, update_data})
end

function GlobalUpdates:ChangeActiveUpdate(update_id, update_data)
	if type(update_id) ~= "number" then
		error("[ProfileService]: Invalid update_id")
	end
	if type(update_data) ~= "table" then
		error("[ProfileService]: Invalid update_data")
	end
	if self._new_active_update_listeners ~= nil then
		error("[ProfileService]: Can't change active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._update_handler_mode ~= true then
		error("[ProfileService]: Can't change active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()")
	end
	-- self._updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
	local updates_latest = self._updates_latest
	local get_global_update = nil
	for _, global_update in ipairs(updates_latest[2]) do
		if update_id == global_update[1] then
			get_global_update = global_update
			break
		end
	end
	if get_global_update ~= nil then
		if get_global_update[3] == true then
			error("[ProfileService]: Can't change locked global update")
		end
		get_global_update[2] = get_global_update[2] + 1 -- Increment version id
		get_global_update[4] = update_data -- Set new global update data
	else
		error("[ProfileService]: Passed non-existant update_id")
	end
end

function GlobalUpdates:ClearActiveUpdate(update_id)
	if type(update_id) ~= "number" then
		error("[ProfileService]: Invalid update_id argument")
	end
	if self._new_active_update_listeners ~= nil then
		error("[ProfileService]: Can't clear active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()")
	elseif self._update_handler_mode ~= true then
		error("[ProfileService]: Can't clear active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()")
	end
	-- self._updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
	local updates_latest = self._updates_latest
	local get_global_update_index = nil
	local get_global_update = nil
	for index, global_update in ipairs(updates_latest[2]) do
		if update_id == global_update[1] then
			get_global_update_index = index
			get_global_update = global_update
			break
		end
	end
	if get_global_update ~= nil then
		if get_global_update[3] == true then
			error("[ProfileService]: Can't clear locked global update")
		end
		table.remove(updates_latest[2], get_global_update_index) -- Remove active global update
	else
		error("[ProfileService]: Passed non-existant update_id")
	end
end

-- Profile object:

local Profile = {
	--[[
		Data = {}, -- [table] -- Loaded once after ProfileStore:LoadProfileAsync() finishes
		MetaData = {}, -- [table] -- Updated with every auto-save
		GlobalUpdates = GlobalUpdates, -- [GlobalUpdates]
		
		_profile_store = ProfileStore, -- [ProfileStore]
		_profile_key = "", -- [string]
		
		_release_listeners = [ScriptSignal] / nil, -- [table / nil]
		_hop_ready_listeners = [ScriptSignal] / nil, -- [table / nil]
		_hop_ready = false,
		
		_view_mode = true / nil, -- [bool] or nil
		
		_load_timestamp = os.clock(),
		
		_is_user_mock = false, -- ProfileStore.Mock
		_mock_key_info = {},
	--]]
}
Profile.__index = Profile

function Profile:IsActive() --> [bool]
	local loaded_profiles = self._is_user_mock == true and self._profile_store._mock_loaded_profiles or self._profile_store._loaded_profiles
	return loaded_profiles[self._profile_key] == self
end

function Profile:GetMetaTag(tag_name) --> value
	local meta_data = self.MetaData
	if meta_data == nil then
		return nil
		-- error("[ProfileService]: This Profile hasn't been loaded before - MetaData not available")
	end
	return self.MetaData.MetaTags[tag_name]
end

function Profile:SetMetaTag(tag_name, value)
	if type(tag_name) ~= "string" then
		error("[ProfileService]: tag_name must be a string")
	elseif string.len(tag_name) == 0 then
		error("[ProfileService]: Invalid tag_name")
	end
	self.MetaData.MetaTags[tag_name] = value
end

function Profile:Reconcile()
	ReconcileTable(self.Data, self._profile_store._profile_template)
end

function Profile:ListenToRelease(listener) --> [ScriptConnection] (place_id / nil, game_job_id / nil)
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in Profile:ListenToRelease()")
	end
	if self._view_mode == true then
		return {Disconnect = function() end}
	end
	if self:IsActive() == false then
		-- Call release listener immediately if profile is expired
		local place_id
		local game_job_id
		local active_session = self.MetaData.ActiveSession
		if active_session ~= nil then
			place_id = active_session[1]
			game_job_id = active_session[2]
		end
		listener(place_id, game_job_id)
		return {Disconnect = function() end}
	else
		return self._release_listeners:Connect(listener)
	end
end

function Profile:Save()
	if self._view_mode == true then
		error("[ProfileService]: Can't save Profile in view mode - Should you be calling :OverwriteAsync() instead?")
	end
	if self:IsActive() == false then
		warn("[ProfileService]: Attempted saving an inactive profile "
			.. self:Identify() .. "; Traceback:\n" .. debug.traceback())
		return
	end
	-- Reject save request if a save is already pending in the queue - this will prevent the user from
	--	unecessary API request spam which we could not meaningfully execute anyways!
	if IsCustomWriteQueueEmptyFor(self._profile_store._profile_store_lookup, self._profile_key) == true then
		-- We don't want auto save to trigger too soon after manual saving - this will reset the auto save timer:
		RemoveProfileFromAutoSave(self)
		AddProfileToAutoSave(self)
		-- Call save function in a new thread:
		task.spawn(SaveProfileAsync, self)
	end
end

function Profile:Release()
	if self._view_mode == true then
		return
	end
	if self:IsActive() == true then
		task.spawn(SaveProfileAsync, self, true) -- Call save function in a new thread with release_from_session = true
	end
end

function Profile:ListenToHopReady(listener) --> [ScriptConnection] ()
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in Profile:ListenToHopReady()")
	end
	if self._view_mode == true then
		return {Disconnect = function() end}
	end
	if self._hop_ready == true then
		task.spawn(listener)
		return {Disconnect = function() end}
	else
		return self._hop_ready_listeners:Connect(listener)
	end
end

function Profile:AddUserId(user_id) -- Associates user_id with profile (GDPR compliance)

	if type(user_id) ~= "number" or user_id % 1 ~= 0 then
		warn("[ProfileService]: Invalid UserId argument for :AddUserId() ("
			.. tostring(user_id) .. "); Traceback:\n" .. debug.traceback())
		return
	end

	if user_id &lt; 0 and self._is_user_mock ~= true and UseMockDataStore ~= true then
		return -- Avoid giving real Roblox APIs negative UserId's
	end

	if table.find(self.UserIds, user_id) == nil then
		table.insert(self.UserIds, user_id)
	end
	
end

function Profile:RemoveUserId(user_id) -- Unassociates user_id with profile (safe function)

	if type(user_id) ~= "number" or user_id % 1 ~= 0 then
		warn("[ProfileService]: Invalid UserId argument for :RemoveUserId() ("
			.. tostring(user_id) .. "); Traceback:\n" .. debug.traceback())
		return
	end
	
	local index = table.find(self.UserIds, user_id)

	if index ~= nil then
		table.remove(self.UserIds, index)
	end

end

function Profile:Identify() --> [string]
	return IdentifyProfile(
		self._profile_store._profile_store_name,
		self._profile_store._profile_store_scope,
		self._profile_key
	)
end

function Profile:ClearGlobalUpdates() -- Clears all global updates data from a profile payload

	if self._view_mode ~= true then
		error("[ProfileService]: :ClearGlobalUpdates() can only be used in view mode")
	end

	local global_updates_object = {
		_updates_latest = {0, {}},
		_profile = self,
	}
	setmetatable(global_updates_object, GlobalUpdates)

	self.GlobalUpdates = global_updates_object

end

function Profile:OverwriteAsync() -- Saves the profile to the DataStore and removes the session lock

	if self._view_mode ~= true then
		error("[ProfileService]: :OverwriteAsync() can only be used in view mode")
	end

	SaveProfileAsync(self, nil, true)

end

-- ProfileVersionQuery object:

local ProfileVersionQuery = {
	--[[
		_profile_store = profile_store,
		_profile_key = profile_key,
		_sort_direction = sort_direction,
		_min_date = min_date,
		_max_date = max_date,

		_query_pages = pages, -- [DataStoreVersionPages]
		_query_index = index, -- [number]
		_query_failure = false,

		_is_query_yielded = false,
		_query_queue = {},
	--]]
}
ProfileVersionQuery.__index = ProfileVersionQuery

function ProfileVersionQuery:_MoveQueue()
	while #self._query_queue > 0 do
		local queue_entry = table.remove(self._query_queue, 1)
		task.spawn(queue_entry)
		if self._is_query_yielded == true then
			break
		end
	end
end

function ProfileVersionQuery:NextAsync(_is_stacking) --> [Profile] or nil

	if self._profile_store == nil then
		return nil
	end

	local profile
	local is_finished = false

	local function query_job()

		if self._query_failure == true then
			is_finished = true
			return
		end

		-- First "next" call loads version pages:

		if self._query_pages == nil then

			self._is_query_yielded = true
			task.spawn(function()
				profile = self:NextAsync(true)
				is_finished = true
			end)
			
			local list_success, error_message = pcall(function()
				self._query_pages = self._profile_store._global_data_store:ListVersionsAsync(
					self._profile_key,
					self._sort_direction,
					self._min_date,
					self._max_date
				)
				self._query_index = 0
			end)

			if list_success == false or self._query_pages == nil then
				warn("[ProfileService]: Version query fail - " .. tostring(error_message))
				self._query_failure = true
			end

			self._is_query_yielded = false
			self:_MoveQueue()

			return

		end

		local current_page = self._query_pages:GetCurrentPage()
		local next_item = current_page[self._query_index + 1]

		-- No more entries:
		
		if self._query_pages.IsFinished == true and next_item == nil then
			is_finished = true
			return
		end

		-- Load next page when this page is over:

		if next_item == nil then

			self._is_query_yielded = true
			task.spawn(function()
				profile = self:NextAsync(true)
				is_finished = true
			end)

			local success = pcall(function()
				self._query_pages:AdvanceToNextPageAsync()
				self._query_index = 0
			end)

			if success == false or #self._query_pages:GetCurrentPage() == 0 then
				self._query_failure = true
			end

			self._is_query_yielded = false
			self:_MoveQueue()

			return

		end

		-- Next page item:

		self._query_index += 1
		profile = self._profile_store:ViewProfileAsync(self._profile_key, next_item.Version)
		is_finished = true

	end

	if self._is_query_yielded == false then
		query_job()
	else
		if _is_stacking == true then
			table.insert(self._query_queue, 1, query_job)
		else
			table.insert(self._query_queue, query_job)
		end
	end

	while is_finished == false do
		task.wait()
	end

	return profile

end

-- ProfileStore object:

local ProfileStore = {
	--[[
		Mock = {},
	
		_profile_store_name = "", -- [string] -- DataStore name
		_profile_store_scope = nil, -- [string] or [nil] -- DataStore scope
		_profile_store_lookup = "", -- [string] -- _profile_store_name .. "\0" .. (_profile_store_scope or "")
		
		_profile_template = {}, -- [table]
		_global_data_store = global_data_store, -- [GlobalDataStore] -- Object returned by DataStoreService:GetDataStore(_profile_store_name)
		
		_loaded_profiles = {[profile_key] = Profile, ...},
		_profile_load_jobs = {[profile_key] = {load_id, loaded_data}, ...},
		
		_mock_loaded_profiles = {[profile_key] = Profile, ...},
		_mock_profile_load_jobs = {[profile_key] = {load_id, loaded_data}, ...},
	--]]
}
ProfileStore.__index = ProfileStore

function ProfileStore:LoadProfileAsync(profile_key, not_released_handler, _use_mock) --> [Profile / nil] not_released_handler(place_id, game_job_id)

	not_released_handler = not_released_handler or "ForceLoad"

	if self._profile_template == nil then
		error("[ProfileService]: Profile template not set - ProfileStore:LoadProfileAsync() locked for this ProfileStore")
	end
	if type(profile_key) ~= "string" then
		error("[ProfileService]: profile_key must be a string")
	elseif string.len(profile_key) == 0 then
		error("[ProfileService]: Invalid profile_key")
	end
	if type(not_released_handler) ~= "function" and not_released_handler ~= "ForceLoad" and not_released_handler ~= "Steal" then
		error("[ProfileService]: Invalid not_released_handler")
	end

	if ProfileService.ServiceLocked == true then
		return nil
	end

	WaitForPendingProfileStore(self)

	local is_user_mock = _use_mock == UseMockTag

	-- Check if profile with profile_key isn't already loaded in this session:
	for _, profile_store in ipairs(ActiveProfileStores) do
		if profile_store._profile_store_lookup == self._profile_store_lookup then
			local loaded_profiles = is_user_mock == true and profile_store._mock_loaded_profiles or profile_store._loaded_profiles
			if loaded_profiles[profile_key] ~= nil then
				error("[ProfileService]: Profile " .. IdentifyProfile(self._profile_store_name, self._profile_store_scope, profile_key) .. " is already loaded in this session")
				-- Are you using Profile:Release() properly?
			end
		end
	end

	ActiveProfileLoadJobs = ActiveProfileLoadJobs + 1
	local force_load = not_released_handler == "ForceLoad"
	local force_load_steps = 0
	local request_force_load = force_load -- First step of ForceLoad
	local steal_session = false -- Second step of ForceLoad
	local aggressive_steal = not_released_handler == "Steal" -- Developer invoked steal
	while ProfileService.ServiceLocked == false do
		-- Load profile:
		-- SPECIAL CASE - If LoadProfileAsync is called for the same key before another LoadProfileAsync finishes,
		-- yoink the DataStore return for the new call. The older call will return nil. This would prevent very rare
		-- game breaking errors where a player rejoins the server super fast.
		local profile_load_jobs = is_user_mock == true and self._mock_profile_load_jobs or self._profile_load_jobs
		local loaded_data, key_info
		local load_id = LoadIndex + 1
		LoadIndex = load_id
		local profile_load_job = profile_load_jobs[profile_key] -- {load_id, {loaded_data, key_info} or nil}
		if profile_load_job ~= nil then
			profile_load_job[1] = load_id -- Yoink load job
			while profile_load_job[2] == nil do -- Wait for job to finish
				task.wait()
			end
			if profile_load_job[1] == load_id then -- Load job hasn't been double-yoinked
				loaded_data, key_info = table.unpack(profile_load_job[2])
				profile_load_jobs[profile_key] = nil
			else
				ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
				return nil
			end
		else
			profile_load_job = {load_id, nil}
			profile_load_jobs[profile_key] = profile_load_job
			profile_load_job[2] = table.pack(StandardProfileUpdateAsyncDataStore(
				self,
				profile_key,
				{
					ExistingProfileHandle = function(latest_data)
						if ProfileService.ServiceLocked == false then
							local active_session = latest_data.MetaData.ActiveSession
							local force_load_session = latest_data.MetaData.ForceLoadSession
							-- IsThisSession(active_session)
							if active_session == nil then
								latest_data.MetaData.ActiveSession = {PlaceId, JobId}
								latest_data.MetaData.ForceLoadSession = nil
							elseif type(active_session) == "table" then
								if IsThisSession(active_session) == false then
									local last_update = latest_data.MetaData.LastUpdate
									if last_update ~= nil then
										if os.time() - last_update > SETTINGS.AssumeDeadSessionLock then
											latest_data.MetaData.ActiveSession = {PlaceId, JobId}
											latest_data.MetaData.ForceLoadSession = nil
											return
										end
									end
									if steal_session == true or aggressive_steal == true then
										local force_load_uninterrupted = false
										if force_load_session ~= nil then
											force_load_uninterrupted = IsThisSession(force_load_session)
										end
										if force_load_uninterrupted == true or aggressive_steal == true then
											latest_data.MetaData.ActiveSession = {PlaceId, JobId}
											latest_data.MetaData.ForceLoadSession = nil
										end
									elseif request_force_load == true then
										latest_data.MetaData.ForceLoadSession = {PlaceId, JobId}
									end
								else
									latest_data.MetaData.ForceLoadSession = nil
								end
							end
						end
					end,
					MissingProfileHandle = function(latest_data)
						latest_data.Data = DeepCopyTable(self._profile_template)
						latest_data.MetaData = {
							ProfileCreateTime = os.time(),
							SessionLoadCount = 0,
							ActiveSession = {PlaceId, JobId},
							ForceLoadSession = nil,
							MetaTags = {},
						}
					end,
					EditProfile = function(latest_data)
						if ProfileService.ServiceLocked == false then
							local active_session = latest_data.MetaData.ActiveSession
							if active_session ~= nil and IsThisSession(active_session) == true then
								latest_data.MetaData.SessionLoadCount = latest_data.MetaData.SessionLoadCount + 1
								latest_data.MetaData.LastUpdate = os.time()
							end
						end
					end,
				},
				is_user_mock
			))
			if profile_load_job[1] == load_id then -- Load job hasn't been yoinked
				loaded_data, key_info = table.unpack(profile_load_job[2])
				profile_load_jobs[profile_key] = nil
			else
				ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
				return nil -- Load job yoinked
			end
		end
		-- Handle load_data:
		if loaded_data ~= nil and key_info ~= nil then
			local active_session = loaded_data.MetaData.ActiveSession
			if type(active_session) == "table" then
				if IsThisSession(active_session) == true then
					-- Special component in MetaTags:
					loaded_data.MetaData.MetaTagsLatest = DeepCopyTable(loaded_data.MetaData.MetaTags)
					-- Case #1: Profile is now taken by this session:
					-- Create Profile object:
					local global_updates_object = {
						_updates_latest = loaded_data.GlobalUpdates,
						_pending_update_lock = {},
						_pending_update_clear = {},

						_new_active_update_listeners = Madwork.NewScriptSignal(),
						_new_locked_update_listeners = Madwork.NewScriptSignal(),

						_profile = nil,
					}
					setmetatable(global_updates_object, GlobalUpdates)
					local profile = {
						Data = loaded_data.Data,
						MetaData = loaded_data.MetaData,
						MetaTagsUpdated = Madwork.NewScriptSignal(),

						RobloxMetaData = loaded_data.RobloxMetaData or {},
						UserIds = loaded_data.UserIds or {},
						KeyInfo = key_info,
						KeyInfoUpdated = Madwork.NewScriptSignal(),

						GlobalUpdates = global_updates_object,

						_profile_store = self,
						_profile_key = profile_key,

						_release_listeners = Madwork.NewScriptSignal(),
						_hop_ready_listeners = Madwork.NewScriptSignal(),
						_hop_ready = false,

						_load_timestamp = os.clock(),

						_is_user_mock = is_user_mock,
					}
					setmetatable(profile, Profile)
					global_updates_object._profile = profile
					-- Referencing Profile object in ProfileStore:
					if next(self._loaded_profiles) == nil and next(self._mock_loaded_profiles) == nil then -- ProfileStore object was inactive
						table.insert(ActiveProfileStores, self)
					end
					if is_user_mock == true then
						self._mock_loaded_profiles[profile_key] = profile
					else
						self._loaded_profiles[profile_key] = profile
					end
					-- Adding profile to AutoSaveList;
					AddProfileToAutoSave(profile)
					-- Special case - finished loading profile, but session is shutting down:
					if ProfileService.ServiceLocked == true then
						SaveProfileAsync(profile, true) -- Release profile and yield until the DataStore call is finished
						profile = nil -- nil will be returned by this call
					end
					-- Return Profile object:
					ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
					return profile
				else
					-- Case #2: Profile is taken by some other session:
					if force_load == true then
						local force_load_session = loaded_data.MetaData.ForceLoadSession
						local force_load_uninterrupted = false
						if force_load_session ~= nil then
							force_load_uninterrupted = IsThisSession(force_load_session)
						end
						if force_load_uninterrupted == true then
							if request_force_load == false then
								force_load_steps = force_load_steps + 1
								if force_load_steps == SETTINGS.ForceLoadMaxSteps then
									steal_session = true
								end
							end
							task.wait() -- Overload prevention
						else
							-- Another session tried to force load this profile:
							ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
							return nil
						end
						request_force_load = false -- Only request a force load once
					elseif aggressive_steal == true then
						task.wait() -- Overload prevention
					else
						local handler_result = not_released_handler(active_session[1], active_session[2])
						if handler_result == "Repeat" then
							task.wait() -- Overload prevention
						elseif handler_result == "Cancel" then
							ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
							return nil
						elseif handler_result == "ForceLoad" then
							force_load = true
							request_force_load = true
							task.wait() -- Overload prevention
						elseif handler_result == "Steal" then
							aggressive_steal = true
							task.wait() -- Overload prevention
						else
							error(
								"[ProfileService]: Invalid return from not_released_handler (\"" .. tostring(handler_result) .. "\")(" .. type(handler_result) .. ");" ..
									"\n" .. IdentifyProfile(self._profile_store_name, self._profile_store_scope, profile_key) ..
									" Traceback:\n" .. debug.traceback()
							)
						end
					end
				end
			else
				ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
				return nil -- In this scenario it is likely the ProfileService.ServiceLocked flag was raised
			end
		else
			task.wait() -- Overload prevention
		end
	end
	ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
	return nil -- If loop breaks return nothing
end

function ProfileStore:GlobalUpdateProfileAsync(profile_key, update_handler, _use_mock) --> [GlobalUpdates / nil] (update_handler(GlobalUpdates))
	if type(profile_key) ~= "string" or string.len(profile_key) == 0 then
		error("[ProfileService]: Invalid profile_key")
	end
	if type(update_handler) ~= "function" then
		error("[ProfileService]: Invalid update_handler")
	end

	if ProfileService.ServiceLocked == true then
		return nil
	end

	WaitForPendingProfileStore(self)

	while ProfileService.ServiceLocked == false do
		-- Updating profile:
		local loaded_data = StandardProfileUpdateAsyncDataStore(
			self,
			profile_key,
			{
				ExistingProfileHandle = nil,
				MissingProfileHandle = nil,
				EditProfile = function(latest_data)
					-- Running update_handler:
					local global_updates_object = {
						_updates_latest = latest_data.GlobalUpdates,
						_update_handler_mode = true,
					}
					setmetatable(global_updates_object, GlobalUpdates)
					update_handler(global_updates_object)
				end,
			},
			_use_mock == UseMockTag
		)
		CustomWriteQueueMarkForCleanup(self._profile_store_lookup, profile_key)
		-- Handling loaded_data:
		if loaded_data ~= nil then
			-- Return GlobalUpdates object (Update successful):
			local global_updates_object = {
				_updates_latest = loaded_data.GlobalUpdates,
			}
			setmetatable(global_updates_object, GlobalUpdates)
			return global_updates_object
		else
			task.wait() -- Overload prevention
		end
	end
	return nil -- Return nothing (Update unsuccessful)
end

function ProfileStore:ViewProfileAsync(profile_key, version, _use_mock) --> [Profile / nil]
	if type(profile_key) ~= "string" or string.len(profile_key) == 0 then
		error("[ProfileService]: Invalid profile_key")
	end

	if ProfileService.ServiceLocked == true then
		return nil
	end

	WaitForPendingProfileStore(self)

	if version ~= nil and (_use_mock == UseMockTag or UseMockDataStore == true) then
		return nil -- No version support in mock mode
	end

	while ProfileService.ServiceLocked == false do
		-- Load profile:
		local loaded_data, key_info = StandardProfileUpdateAsyncDataStore(
			self,
			profile_key,
			{
				ExistingProfileHandle = nil,
				MissingProfileHandle = function(latest_data)
					latest_data.Data = DeepCopyTable(self._profile_template)
					latest_data.MetaData = {
						ProfileCreateTime = os.time(),
						SessionLoadCount = 0,
						ActiveSession = nil,
						ForceLoadSession = nil,
						MetaTags = {},
					}
				end,
				EditProfile = nil,
			},
			_use_mock == UseMockTag,
			true, -- Use :GetAsync()
			version -- DataStore key version
		)
		CustomWriteQueueMarkForCleanup(self._profile_store_lookup, profile_key)
		-- Handle load_data:
		if loaded_data ~= nil then
			if key_info == nil then
				return nil -- Load was successful, but the key was empty - return no profile object
			end
			-- Create Profile object:
			local global_updates_object = {
				_updates_latest = loaded_data.GlobalUpdates, -- {0, {}}
				_profile = nil,
			}
			setmetatable(global_updates_object, GlobalUpdates)
			local profile = {
				Data = loaded_data.Data,
				MetaData = loaded_data.MetaData,
				MetaTagsUpdated = Madwork.NewScriptSignal(),

				RobloxMetaData = loaded_data.RobloxMetaData or {},
				UserIds = loaded_data.UserIds or {},
				KeyInfo = key_info,
				KeyInfoUpdated = Madwork.NewScriptSignal(),

				GlobalUpdates = global_updates_object,

				_profile_store = self,
				_profile_key = profile_key,

				_view_mode = true,

				_load_timestamp = os.clock(),
			}
			setmetatable(profile, Profile)
			global_updates_object._profile = profile
			-- Returning Profile object:
			return profile
		else
			task.wait() -- Overload prevention
		end
	end
	return nil -- If loop breaks return nothing
end

function ProfileStore:ProfileVersionQuery(profile_key, sort_direction, min_date, max_date, _use_mock) --> [ProfileVersionQuery]
	if type(profile_key) ~= "string" or string.len(profile_key) == 0 then
		error("[ProfileService]: Invalid profile_key")
	end

	if ProfileService.ServiceLocked == true then
		return setmetatable({}, ProfileVersionQuery) -- Silently fail :Next() requests
	end

	WaitForPendingProfileStore(self)

	if _use_mock == UseMockTag or UseMockDataStore == true then
		error("[ProfileService]: :ProfileVersionQuery() is not supported in mock mode")
	end

	-- Type check:
	if sort_direction ~= nil and (typeof(sort_direction) ~= "EnumItem"
		or sort_direction.EnumType ~= Enum.SortDirection) then
		error("[ProfileService]: Invalid sort_direction (" .. tostring(sort_direction) .. ")")
	end

	if min_date ~= nil and typeof(min_date) ~= "DateTime" and typeof(min_date) ~= "number" then
		error("[ProfileService]: Invalid min_date (" .. tostring(min_date) .. ")")
	end

	if max_date ~= nil and typeof(max_date) ~= "DateTime" and typeof(max_date) ~= "number" then
		error("[ProfileService]: Invalid max_date (" .. tostring(max_date) .. ")")
	end

	min_date = typeof(min_date) == "DateTime" and min_date.UnixTimestampMillis or min_date
	max_date = typeof(max_date) == "DateTime" and max_date.UnixTimestampMillis or max_date

	local profile_version_query = {
		_profile_store = self,
		_profile_key = profile_key,
		_sort_direction = sort_direction,
		_min_date = min_date,
		_max_date = max_date,

		_query_pages = nil,
		_query_index = 0,
		_query_failure = false,

		_is_query_yielded = false,
		_query_queue = {},
	}
	setmetatable(profile_version_query, ProfileVersionQuery)

	return profile_version_query

end

function ProfileStore:WipeProfileAsync(profile_key, _use_mock) --> is_wipe_successful [bool]
	if type(profile_key) ~= "string" or string.len(profile_key) == 0 then
		error("[ProfileService]: Invalid profile_key")
	end

	if ProfileService.ServiceLocked == true then
		return false
	end

	WaitForPendingProfileStore(self)

	local wipe_status = false

	if _use_mock == UseMockTag then -- Used when the profile is accessed through ProfileStore.Mock
		local mock_data_store = UserMockDataStore[self._profile_store_lookup]
		if mock_data_store ~= nil then
			mock_data_store[profile_key] = nil
		end
		wipe_status = true
		task.wait() -- Simulate API call yield
	elseif UseMockDataStore == true then -- Used when API access is disabled
		local mock_data_store = MockDataStore[self._profile_store_lookup]
		if mock_data_store ~= nil then
			mock_data_store[profile_key] = nil
		end
		wipe_status = true
		task.wait() -- Simulate API call yield
	else
		wipe_status = pcall(function()
			self._global_data_store:RemoveAsync(profile_key)
		end)
	end

	CustomWriteQueueMarkForCleanup(self._profile_store_lookup, profile_key)

	return wipe_status
end

-- New ProfileStore:

function ProfileService.GetProfileStore(profile_store_index, profile_template) --> [ProfileStore]

	local profile_store_name
	local profile_store_scope = nil

	-- Parsing profile_store_index:
	if type(profile_store_index) == "string" then
		-- profile_store_index as string:
		profile_store_name = profile_store_index
	elseif type(profile_store_index) == "table" then
		-- profile_store_index as table:
		profile_store_name = profile_store_index.Name
		profile_store_scope = profile_store_index.Scope
	else
		error("[ProfileService]: Invalid or missing profile_store_index")
	end

	-- Type checking:
	if profile_store_name == nil or type(profile_store_name) ~= "string" then
		error("[ProfileService]: Missing or invalid \"Name\" parameter")
	elseif string.len(profile_store_name) == 0 then
		error("[ProfileService]: ProfileStore name cannot be an empty string")
	end

	if profile_store_scope ~= nil and (type(profile_store_scope) ~= "string" or string.len(profile_store_scope) == 0) then
		error("[ProfileService]: Invalid \"Scope\" parameter")
	end

	if type(profile_template) ~= "table" then
		error("[ProfileService]: Invalid profile_template")
	end

	local profile_store
	profile_store = {
		Mock = {
			LoadProfileAsync = function(_, profile_key, not_released_handler)
				return profile_store:LoadProfileAsync(profile_key, not_released_handler, UseMockTag)
			end,
			GlobalUpdateProfileAsync = function(_, profile_key, update_handler)
				return profile_store:GlobalUpdateProfileAsync(profile_key, update_handler, UseMockTag)
			end,
			ViewProfileAsync = function(_, profile_key, version)
				return profile_store:ViewProfileAsync(profile_key, version, UseMockTag)
			end,
			FindProfileVersionAsync = function(_, profile_key, sort_direction, min_date, max_date)
				return profile_store:FindProfileVersionAsync(profile_key, sort_direction, min_date, max_date, UseMockTag)
			end,
			WipeProfileAsync = function(_, profile_key)
				return profile_store:WipeProfileAsync(profile_key, UseMockTag)
			end
		},

		_profile_store_name = profile_store_name,
		_profile_store_scope = profile_store_scope,
		_profile_store_lookup = profile_store_name .. "\0" .. (profile_store_scope or ""),

		_profile_template = profile_template,
		_global_data_store = nil,
		_loaded_profiles = {},
		_profile_load_jobs = {},
		_mock_loaded_profiles = {},
		_mock_profile_load_jobs = {},
		_is_pending = false,
	}
	setmetatable(profile_store, ProfileStore)

	if IsLiveCheckActive == true then
		profile_store._is_pending = true
		task.spawn(function()
			WaitForLiveAccessCheck()
			if UseMockDataStore == false then
				profile_store._global_data_store = DataStoreService:GetDataStore(profile_store_name, profile_store_scope)
			end
			profile_store._is_pending = false
		end)
	else
		if UseMockDataStore == false then
			profile_store._global_data_store = DataStoreService:GetDataStore(profile_store_name, profile_store_scope)
		end
	end

	return profile_store
end

function ProfileService.IsLive() --> [bool] -- (CAN YIELD!!!)

	WaitForLiveAccessCheck()

	return UseMockDataStore == false

end

----- Initialize -----

if IsStudio == true then
	IsLiveCheckActive = true
	task.spawn(function()
		local status, message = pcall(function()
			-- This will error if current instance has no Studio API access:
			DataStoreService:GetDataStore("____PS"):SetAsync("____PS", os.time())
		end)
		local no_internet_access = status == false and string.find(message, "ConnectFail", 1, true) ~= nil
		if no_internet_access == true then
			warn("[ProfileService]: No internet access - check your network connection")
		end
		if status == false and
			(string.find(message, "403", 1, true) ~= nil or -- Cannot write to DataStore from studio if API access is not enabled
				string.find(message, "must publish", 1, true) ~= nil or -- Game must be published to access live keys
				no_internet_access == true) then -- No internet access

			UseMockDataStore = true
			ProfileService._use_mock_data_store = true
			print("[ProfileService]: Roblox API services unavailable - data will not be saved")
		else
			print("[ProfileService]: Roblox API services available - data will be saved")
		end
		IsLiveCheckActive = false
	end)
end

----- Connections -----

-- Auto saving and issue queue managing:
RunService.Heartbeat:Connect(function()
	-- 1) Auto saving: --
	local auto_save_list_length = #AutoSaveList
	if auto_save_list_length > 0 then
		local auto_save_index_speed = SETTINGS.AutoSaveProfiles / auto_save_list_length
		local os_clock = os.clock()
		while os_clock - LastAutoSave > auto_save_index_speed do
			LastAutoSave = LastAutoSave + auto_save_index_speed
			local profile = AutoSaveList[AutoSaveIndex]
			if os_clock - profile._load_timestamp &lt; SETTINGS.AutoSaveProfiles then
				-- This profile is freshly loaded - auto-saving immediately after loading will cause a warning in the log:
				profile = nil
				for _ = 1, auto_save_list_length - 1 do
					-- Move auto save index to the right:
					AutoSaveIndex = AutoSaveIndex + 1
					if AutoSaveIndex > auto_save_list_length then
						AutoSaveIndex = 1
					end
					profile = AutoSaveList[AutoSaveIndex]
					if os_clock - profile._load_timestamp >= SETTINGS.AutoSaveProfiles then
						break
					else
						profile = nil
					end
				end
			end
			-- Move auto save index to the right:
			AutoSaveIndex = AutoSaveIndex + 1
			if AutoSaveIndex > auto_save_list_length then
				AutoSaveIndex = 1
			end
			-- Perform save call:
			if profile ~= nil then
				task.spawn(SaveProfileAsync, profile) -- Auto save profile in new thread
			end
		end
	end
	-- 2) Issue queue: --
	-- Critical state handling:
	if ProfileService.CriticalState == false then
		if #IssueQueue >= SETTINGS.IssueCountForCriticalState then
			ProfileService.CriticalState = true
			ProfileService.CriticalStateSignal:Fire(true)
			CriticalStateStart = os.clock()
			warn("[ProfileService]: Entered critical state")
		end
	else
		if #IssueQueue >= SETTINGS.IssueCountForCriticalState then
			CriticalStateStart = os.clock()
		elseif os.clock() - CriticalStateStart > SETTINGS.CriticalStateLast then
			ProfileService.CriticalState = false
			ProfileService.CriticalStateSignal:Fire(false)
			warn("[ProfileService]: Critical state ended")
		end
	end
	-- Issue queue:
	while true do
		local issue_time = IssueQueue[1]
		if issue_time == nil then
			break
		elseif os.clock() - issue_time > SETTINGS.IssueLast then
			table.remove(IssueQueue, 1)
		else
			break
		end
	end
end)

-- Release all loaded profiles when the server is shutting down:
task.spawn(function()
	WaitForLiveAccessCheck()
	Madwork.ConnectToOnClose(
		function()
			ProfileService.ServiceLocked = true
			-- 1) Release all active profiles: --
			-- Clone AutoSaveList to a new table because AutoSaveList changes when profiles are released:
			local on_close_save_job_count = 0
			local active_profiles = {}
			for index, profile in ipairs(AutoSaveList) do
				active_profiles[index] = profile
			end
			-- Release the profiles; Releasing profiles can trigger listeners that release other profiles, so check active state:
			for _, profile in ipairs(active_profiles) do
				if profile:IsActive() == true then
					on_close_save_job_count = on_close_save_job_count + 1
					task.spawn(function() -- Save profile on new thread
						SaveProfileAsync(profile, true)
						on_close_save_job_count = on_close_save_job_count - 1
					end)
				end
			end
			-- 2) Yield until all active profile jobs are finished: --
			while on_close_save_job_count > 0 or ActiveProfileLoadJobs > 0 or ActiveProfileSaveJobs > 0 do
				task.wait()
			end
			return -- We're done!
		end,
		UseMockDataStore == false -- Always run this OnClose task if using Roblox API services
	)
end)

return ProfileService</string>
            </Properties>
          </Item>
        </Item>
        <Item class="Folder" referent="13">
          <Properties>
            <string name="Name">sleitnick_comm@1.0.1</string>
          </Properties>
          <Item class="ModuleScript" referent="14">
            <Properties>
              <string name="Name">Option</string>
              <string name="Source"><![CDATA[return require(script.Parent.Parent["sleitnick_option@1.0.5"]["option"])
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="15">
            <Properties>
              <string name="Name">Promise</string>
              <string name="Source"><![CDATA[return require(script.Parent.Parent["evaera_promise@4.0.0"]["promise"])
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="16">
            <Properties>
              <string name="Name">Signal</string>
              <string name="Source"><![CDATA[return require(script.Parent.Parent["sleitnick_signal@2.0.3"]["signal"])
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="17">
            <Properties>
              <string name="Name">comm</string>
              <string name="Source"><![CDATA[-- Comm
-- Stephen Leitnick
-- August 05, 2021

--[=[
	@class Comm
	Remote communication library.

	This exposes the raw functions that are used by the `ServerComm` and `ClientComm` classes.
	Those two classes should be preferred over accessing the functions directly through this
	Comm library.

	```lua
	-- Server
	local ServerComm = require(ReplicatedStorage.Packages.Comm).ServerComm
	local serverComm = ServerComm.new(somewhere, "MyComm")
	serverComm:BindFunction("Hello", function(player: Player)
		return "Hi"
	end)
	
	-- Client
	local ClientComm = require(ReplicatedStorage.Packages.Comm).ClientComm
	local clientComm = ClientComm.new(somewhere, false, "MyComm")
	local comm = clientComm:BuildObject()
	print(comm:Hello()) --> Hi
	```
]=]
local Comm = {
	Server = require(script.Server),
	Client = require(script.Client),
	ServerComm = require(script.Server.ServerComm),
	ClientComm = require(script.Client.ClientComm),
}

return Comm
]]></string>
            </Properties>
            <Item class="ModuleScript" referent="18">
              <Properties>
                <string name="Name">Client</string>
                <string name="Source"><![CDATA[local Util = require(script.Parent.Util)
local Types = require(script.Parent.Types)
local Promise = require(script.Parent.Parent.Promise)
local ClientRemoteSignal = require(script.ClientRemoteSignal)
local ClientRemoteProperty = require(script.ClientRemoteProperty)

local Client = {}

function Client.GetFunction(
	parent: Instance,
	name: string,
	usePromise: boolean,
	inboundMiddleware: Types.ClientMiddleware?,
	outboundMiddleware: Types.ClientMiddleware?
)
	assert(not Util.IsServer, "GetFunction must be called from the client")
	local folder = Util.GetCommSubFolder(parent, "RF"):Expect("Failed to get Comm RF folder")
	local rf = folder:WaitForChild(name, Util.WaitForChildTimeout)
	assert(rf ~= nil, "Failed to find RemoteFunction: " .. name)
	local hasInbound = type(inboundMiddleware) == "table" and #inboundMiddleware > 0
	local hasOutbound = type(outboundMiddleware) == "table" and #outboundMiddleware > 0
	local function ProcessOutbound(args)
		for _, middlewareFunc in ipairs(outboundMiddleware) do
			local middlewareResult = table.pack(middlewareFunc(args))
			if not middlewareResult[1] then
				return table.unpack(middlewareResult, 2, middlewareResult.n)
			end
			args.n = #args
		end
		return table.unpack(args, 1, args.n)
	end
	if hasInbound then
		if usePromise then
			return function(...)
				local args = table.pack(...)
				return Promise.new(function(resolve, reject)
					local success, res = pcall(function()
						if hasOutbound then
							return table.pack(rf:InvokeServer(ProcessOutbound(args)))
						else
							return table.pack(rf:InvokeServer(table.unpack(args, 1, args.n)))
						end
					end)
					if success then
						for _, middlewareFunc in ipairs(inboundMiddleware) do
							local middlewareResult = table.pack(middlewareFunc(res))
							if not middlewareResult[1] then
								return table.unpack(middlewareResult, 2, middlewareResult.n)
							end
							res.n = #res
						end
						resolve(table.unpack(res, 1, res.n))
					else
						reject(res)
					end
				end)
			end
		else
			return function(...)
				local res
				if hasOutbound then
					res = table.pack(rf:InvokeServer(ProcessOutbound(table.pack(...))))
				else
					res = table.pack(rf:InvokeServer(...))
				end
				for _, middlewareFunc in ipairs(inboundMiddleware) do
					local middlewareResult = table.pack(middlewareFunc(res))
					if not middlewareResult[1] then
						return table.unpack(middlewareResult, 2, middlewareResult.n)
					end
					res.n = #res
				end
				return table.unpack(res, 1, res.n)
			end
		end
	else
		if usePromise then
			return function(...)
				local args = table.pack(...)
				return Promise.new(function(resolve, reject)
					local success, res = pcall(function()
						if hasOutbound then
							return table.pack(rf:InvokeServer(ProcessOutbound(args)))
						else
							return table.pack(rf:InvokeServer(table.unpack(args, 1, args.n)))
						end
					end)
					if success then
						resolve(table.unpack(res, 1, res.n))
					else
						reject(res)
					end
				end)
			end
		else
			if hasOutbound then
				return function(...)
					return rf:InvokeServer(ProcessOutbound(table.pack(...)))
				end
			else
				return function(...)
					return rf:InvokeServer(...)
				end
			end
		end
	end
end

function Client.GetSignal(
	parent: Instance,
	name: string,
	inboundMiddleware: Types.ClientMiddleware?,
	outboundMiddleware: Types.ClientMiddleware?
)
	assert(not Util.IsServer, "GetSignal must be called from the client")
	local folder = Util.GetCommSubFolder(parent, "RE"):Expect("Failed to get Comm RE folder")
	local re = folder:WaitForChild(name, Util.WaitForChildTimeout)
	assert(re ~= nil, "Failed to find RemoteEvent: " .. name)
	return ClientRemoteSignal.new(re, inboundMiddleware, outboundMiddleware)
end

function Client.GetProperty(
	parent: Instance,
	name: string,
	inboundMiddleware: Types.ClientMiddleware?,
	outboundMiddleware: Types.ClientMiddleware?
)
	assert(not Util.IsServer, "GetProperty must be called from the client")
	local folder = Util.GetCommSubFolder(parent, "RP"):Expect("Failed to get Comm RP folder")
	local re = folder:WaitForChild(name, Util.WaitForChildTimeout)
	assert(re ~= nil, "Failed to find RemoteEvent for RemoteProperty: " .. name)
	return ClientRemoteProperty.new(re, inboundMiddleware, outboundMiddleware)
end

return Client
]]></string>
              </Properties>
              <Item class="ModuleScript" referent="19">
                <Properties>
                  <string name="Name">ClientComm</string>
                  <string name="Source"><![CDATA[-- ClientComm
-- Stephen Leitnick
-- December 20, 2021

local Comm = require(script.Parent)
local Types = require(script.Parent.Parent.Types)
local Util = require(script.Parent.Parent.Util)

--[=[
	@class ClientComm
	@client
]=]
local ClientComm = {}
ClientComm.__index = ClientComm

--[=[
	@within ClientComm
	@type ClientMiddlewareFn (args: {any}) -> (shouldContinue: boolean, ...: any)
	The middleware function takes the arguments (as a table array), and should
	return `true|false` to indicate if the process should continue.

	If returning `false`, the optional varargs after the `false` are used as the new return values
	to whatever was calling the middleware.
]=]
--[=[
	@within ClientComm
	@type ClientMiddleware {ClientMiddlewareFn}
	Array of middleware functions.
]=]

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

	If `usePromise` is set to `true`, then `GetFunction` will generate a function that returns a Promise
	that resolves with the server response. If set to `false`, the function will act like a normal
	call to a RemoteFunction and yield until the function responds.

	```lua
	local clientComm = ClientComm.new(game:GetService("ReplicatedStorage"), true)

	-- If using a unique namespace with ServerComm, include it as second argument:
	local clientComm = ClientComm.new(game:GetService("ReplicatedStorage"), true, "MyNamespace")
	```
]=]
function ClientComm.new(parent: Instance, usePromise: boolean, namespace: string?)
	assert(not Util.IsServer, "ClientComm must be constructed from the client")
	assert(typeof(parent) == "Instance", "Parent must be of type Instance")
	local ns = Util.DefaultCommFolderName
	if namespace then
		ns = namespace
	end
	local folder: Instance? = parent:WaitForChild(ns, Util.WaitForChildTimeout)
	assert(folder ~= nil, "Could not find namespace for ClientComm in parent: " .. ns)
	local self = setmetatable({}, ClientComm)
	self._instancesFolder = folder
	self._usePromise = usePromise
	return self
end

--[=[
	@param name string
	@param inboundMiddleware ClientMiddleware?
	@param outboundMiddleware ClientMiddleware?
	@return (...: any) -> any

	Generates a function on the matching RemoteFunction generated with ServerComm. The function
	can then be called to invoke the server. If this `ClientComm` object was created with
	the `usePromise` parameter set to `true`, then this generated function will return
	a Promise when called.

	```lua
	-- Server-side:
	local serverComm = ServerComm.new(someParent)
	serverComm:BindFunction("MyFunction", function(player, msg)
		return msg:upper()
	end)

	-- Client-side:
	local clientComm = ClientComm.new(someParent)
	local myFunc = clientComm:GetFunction("MyFunction")
	local uppercase = myFunc("hello world")
	print(uppercase) --> HELLO WORLD

	-- Client-side, using promises:
	local clientComm = ClientComm.new(someParent, true)
	local myFunc = clientComm:GetFunction("MyFunction")
	myFunc("hi there"):andThen(function(msg)
		print(msg) --> HI THERE
	end):catch(function(err)
		print("Error:", err)
	end)
	```
]=]
function ClientComm:GetFunction(
	name: string,
	inboundMiddleware: Types.ClientMiddleware?,
	outboundMiddleware: Types.ClientMiddleware?
)
	return Comm.GetFunction(self._instancesFolder, name, self._usePromise, inboundMiddleware, outboundMiddleware)
end

--[=[
	@param name string
	@param inboundMiddleware ClientMiddleware?
	@param outboundMiddleware ClientMiddleware?
	@return ClientRemoteSignal
	Returns a new ClientRemoteSignal that mirrors the matching RemoteSignal created by
	ServerComm with the same matching `name`.

	```lua
	local mySignal = clientComm:GetSignal("MySignal")

	-- Listen for data from the server:
	mySignal:Connect(function(message)
		print("Received message from server:", message)
	end)

	-- Send data to the server:
	mySignal:Fire("Hello!")
	```
]=]
function ClientComm:GetSignal(
	name: string,
	inboundMiddleware: Types.ClientMiddleware?,
	outboundMiddleware: Types.ClientMiddleware?
)
	return Comm.GetSignal(self._instancesFolder, name, inboundMiddleware, outboundMiddleware)
end

--[=[
	@param name string
	@param inboundMiddleware ClientMiddleware?
	@param outboundMiddleware ClientMiddleware?
	@return ClientRemoteProperty
	Returns a new ClientRemoteProperty that mirrors the matching RemoteProperty created by
	ServerComm with the same matching `name`.

	Take a look at the ClientRemoteProperty documentation for more info, such as
	understanding how to wait for data to be ready.

	```lua
	local mapInfo = clientComm:GetProperty("MapInfo")

	-- Observe the initial value of mapInfo, and all subsequent changes:
	mapInfo:Observe(function(info)
		print("Current map info", info)
	end)

	-- Check to see if data is initially ready:
	if mapInfo:IsReady() then
		-- Get the data:
		local info = mapInfo:Get()
	end

	-- Get a promise that resolves once the data is ready (resolves immediately if already ready):
	mapInfo:OnReady():andThen(function(info)
		print("Map info is ready with info", info)
	end)

	-- Same as above, but yields thread:
	local success, info = mapInfo:OnReady():await()
	```
]=]
function ClientComm:GetProperty(
	name: string,
	inboundMiddleware: Types.ClientMiddleware?,
	outboundMiddleware: Types.ClientMiddleware?
)
	return Comm.GetProperty(self._instancesFolder, name, inboundMiddleware, outboundMiddleware)
end

--[=[
	@param inboundMiddleware ClientMiddleware?
	@param outboundMiddleware ClientMiddleware?
	@return table
	Returns an object which maps RemoteFunctions as methods
	and RemoteEvents as fields.
	```lua
	-- Server-side:
	serverComm:BindFunction("Test", function(player) end)
	serverComm:CreateSignal("MySignal")
	serverComm:CreateProperty("MyProperty", 10)

	-- Client-side
	local obj = clientComm:BuildObject()
	obj:Test()
	obj.MySignal:Connect(function(data) end)
	obj.MyProperty:Observe(function(value) end)
	```
]=]
function ClientComm:BuildObject(inboundMiddleware: Types.ClientMiddleware?, outboundMiddleware: Types.ClientMiddleware?)
	local obj = {}
	local rfFolder = self._instancesFolder:FindFirstChild("RF")
	local reFolder = self._instancesFolder:FindFirstChild("RE")
	local rpFolder = self._instancesFolder:FindFirstChild("RP")
	if rfFolder then
		for _, rf in rfFolder:GetChildren() do
			if not rf:IsA("RemoteFunction") then
				continue
			end
			local f = self:GetFunction(rf.Name, inboundMiddleware, outboundMiddleware)
			obj[rf.Name] = function(_self, ...)
				return f(...)
			end
		end
	end
	if reFolder then
		for _, re in reFolder:GetChildren() do
			if (not re:IsA("RemoteEvent")) and (not re:IsA("UnreliableRemoteEvent")) then
				continue
			end
			obj[re.Name] = self:GetSignal(re.Name, inboundMiddleware, outboundMiddleware)
		end
	end
	if rpFolder then
		for _, re in rpFolder:GetChildren() do
			if not re:IsA("RemoteEvent") then
				continue
			end
			obj[re.Name] = self:GetProperty(re.Name, inboundMiddleware, outboundMiddleware)
		end
	end
	return obj
end

--[=[
	Destroys the ClientComm object.
]=]
function ClientComm:Destroy() end

return ClientComm
]]></string>
                </Properties>
              </Item>
              <Item class="ModuleScript" referent="20">
                <Properties>
                  <string name="Name">ClientRemoteProperty</string>
                  <string name="Source"><![CDATA[-- ClientRemoteProperty
-- Stephen Leitnick
-- December 20, 2021

local Promise = require(script.Parent.Parent.Parent.Promise)
local Signal = require(script.Parent.Parent.Parent.Signal)
local ClientRemoteSignal = require(script.Parent.ClientRemoteSignal)
local Types = require(script.Parent.Parent.Types)

--[=[
	@within ClientRemoteProperty
	@prop Changed Signal<any>

	Fires when the property receives an updated value
	from the server.

	```lua
	clientRemoteProperty.Changed:Connect(function(value)
		print("New value", value)
	end)
	```
]=]

--[=[
	@class ClientRemoteProperty
	@client
	Created via `ClientComm:GetProperty()`.
]=]
local ClientRemoteProperty = {}
ClientRemoteProperty.__index = ClientRemoteProperty

function ClientRemoteProperty.new(
	re: RemoteEvent,
	inboundMiddleware: Types.ClientMiddleware?,
	outboudMiddleware: Types.ClientMiddleware?
)
	local self = setmetatable({}, ClientRemoteProperty)
	self._rs = ClientRemoteSignal.new(re, inboundMiddleware, outboudMiddleware)
	self._ready = false
	self._value = nil
	self.Changed = Signal.new()
	self._rs:Fire()

	local resolveOnReadyPromise
	self._readyPromise = Promise.new(function(resolve)
		resolveOnReadyPromise = resolve
	end)
	self._changed = self._rs:Connect(function(value)
		local changed = value ~= self._value
		self._value = value
		if not self._ready then
			self._ready = true
			resolveOnReadyPromise(value)
		end
		if changed then
			self.Changed:Fire(value)
		end
	end)

	return self
end

--[=[
	Gets the value of the property object.

	:::caution
	This value might not be ready right away. Use `OnReady()` or `IsReady()`
	before calling `Get()`. If not ready, this value will return `nil`.
	:::
]=]
function ClientRemoteProperty:Get(): any
	return self._value
end

--[=[
	@return Promise<any>
	Returns a Promise which resolves once the property object is
	ready to be used. The resolved promise will also contain the
	value of the property.

	```lua
	-- Use andThen clause:
	clientRemoteProperty:OnReady():andThen(function(initialValue)
		print(initialValue)
	end)

	-- Use await:
	local success, initialValue = clientRemoteProperty:OnReady():await()
	if success then
		print(initialValue)
	end
	```
]=]
function ClientRemoteProperty:OnReady()
	return self._readyPromise
end

--[=[
	Returns `true` if the property object is ready to be
	used. In other words, it has successfully gained
	connection to the server-side version and has synced
	in the initial value.

	```lua
	if clientRemoteProperty:IsReady() then
		local value = clientRemoteProperty:Get()
	end
	```
]=]
function ClientRemoteProperty:IsReady(): boolean
	return self._ready
end

--[=[
	@param observer (any) -> nil
	@return Connection
	Observes the value of the property. The observer will
	be called right when the value is first ready, and
	every time the value changes. This is safe to call
	immediately (i.e. no need to use `IsReady` or `OnReady`
	before using this method).

	Observing is essentially listening to `Changed`, but
	also sends the initial value right away (or at least
	once `OnReady` is completed).

	```lua
	local function ObserveValue(value)
		print(value)
	end

	clientRemoteProperty:Observe(ObserveValue)
	```
]=]
function ClientRemoteProperty:Observe(observer: (any) -> ())
	if self._ready then
		task.defer(observer, self._value)
	end
	return self.Changed:Connect(observer)
end

--[=[
	Destroys the ClientRemoteProperty object.
]=]
function ClientRemoteProperty:Destroy()
	self._rs:Destroy()
	if self._readyPromise then
		self._readyPromise:cancel()
	end
	if self._changed then
		self._changed:Disconnect()
	end
	self.Changed:Destroy()
end

return ClientRemoteProperty
]]></string>
                </Properties>
              </Item>
              <Item class="ModuleScript" referent="21">
                <Properties>
                  <string name="Name">ClientRemoteSignal</string>
                  <string name="Source"><![CDATA[-- ClientRemoteSignal
-- Stephen Leitnick
-- December 20, 2021

local Signal = require(script.Parent.Parent.Parent.Signal)
local Types = require(script.Parent.Parent.Types)

--[=[
	@class ClientRemoteSignal
	@client
	Created via `ClientComm:GetSignal()`.
]=]
local ClientRemoteSignal = {}
ClientRemoteSignal.__index = ClientRemoteSignal

--[=[
	@within ClientRemoteSignal
	@interface Connection
	.Disconnect () -> ()

	Represents a connection.
]=]

function ClientRemoteSignal.new(
	re: RemoteEvent | UnreliableRemoteEvent,
	inboundMiddleware: Types.ClientMiddleware?,
	outboudMiddleware: Types.ClientMiddleware?
)
	local self = setmetatable({}, ClientRemoteSignal)
	self._re = re
	if outboudMiddleware and #outboudMiddleware > 0 then
		self._hasOutbound = true
		self._outbound = outboudMiddleware
	else
		self._hasOutbound = false
	end
	if inboundMiddleware and #inboundMiddleware > 0 then
		self._directConnect = false
		self._signal = Signal.new()
		self._reConn = self._re.OnClientEvent:Connect(function(...)
			local args = table.pack(...)
			for _, middlewareFunc in inboundMiddleware do
				local middlewareResult = table.pack(middlewareFunc(args))
				if not middlewareResult[1] then
					return
				end
				args.n = #args
			end
			self._signal:Fire(table.unpack(args, 1, args.n))
		end)
	else
		self._directConnect = true
	end
	return self
end

function ClientRemoteSignal:_processOutboundMiddleware(...: any)
	local args = table.pack(...)
	for _, middlewareFunc in self._outbound do
		local middlewareResult = table.pack(middlewareFunc(args))
		if not middlewareResult[1] then
			return table.unpack(middlewareResult, 2, middlewareResult.n)
		end
		args.n = #args
	end
	return table.unpack(args, 1, args.n)
end

--[=[
	@param fn (...: any) -> ()
	@return Connection
	Connects a function to the remote signal. The function will be
	called anytime the equivalent server-side RemoteSignal is
	fired at this specific client that created this client signal.
]=]
function ClientRemoteSignal:Connect(fn: (...any) -> ())
	if self._directConnect then
		return self._re.OnClientEvent:Connect(fn)
	else
		return self._signal:Connect(fn)
	end
end

--[=[
	Fires the equivalent server-side signal with the given arguments.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware before being
	sent to the server.
	:::
]=]
function ClientRemoteSignal:Fire(...: any)
	if self._hasOutbound then
		self._re:FireServer(self:_processOutboundMiddleware(...))
	else
		self._re:FireServer(...)
	end
end

--[=[
	Destroys the ClientRemoteSignal object.
]=]
function ClientRemoteSignal:Destroy()
	if self._signal then
		self._signal:Destroy()
	end
end

return ClientRemoteSignal
]]></string>
                </Properties>
              </Item>
            </Item>
            <Item class="ModuleScript" referent="22">
              <Properties>
                <string name="Name">Server</string>
                <string name="Source"><![CDATA[local RemoteProperty = require(script.RemoteProperty)
local RemoteSignal = require(script.RemoteSignal)
local Types = require(script.Parent.Types)
local Util = require(script.Parent.Util)

local Server = {}

--[=[
	@within Comm
	@prop ServerComm ServerComm
]=]
--[=[
	@within Comm
	@prop ClientComm ClientComm
]=]

--[=[
	@within Comm
	@private
	@interface Server
	.BindFunction (parent: Instance, name: string, fn: FnBind, inboundMiddleware: ServerMiddleware?, outboundMiddleware: ServerMiddleware?): RemoteFunction
	.WrapMethod (parent: Instance, tbl: table, name: string, inboundMiddleware: ServerMiddleware?, outboundMiddleware: ServerMiddleware?): RemoteFunction
	.CreateSignal (parent: Instance, name: string, inboundMiddleware: ServerMiddleware?, outboundMiddleware: ServerMiddleware?): RemoteSignal
	.CreateProperty (parent: Instance, name: string, value: any, inboundMiddleware: ServerMiddleware?, outboundMiddleware: ServerMiddleware?): RemoteProperty
	Server Comm
]=]
--[=[
	@within Comm
	@private
	@interface Client
	.GetFunction (parent: Instance, name: string, usePromise: boolean, inboundMiddleware: ClientMiddleware?, outboundMiddleware: ClientMiddleware?): (...: any) -> any
	.GetSignal (parent: Instance, name: string, inboundMiddleware: ClientMiddleware?, outboundMiddleware: ClientMiddleware?): ClientRemoteSignal
	.GetProperty (parent: Instance, name: string, inboundMiddleware: ClientMiddleware?, outboundMiddleware: ClientMiddleware?): ClientRemoteProperty
	Client Comm
]=]

function Server.BindFunction(
	parent: Instance,
	name: string,
	func: Types.FnBind,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
): RemoteFunction
	assert(Util.IsServer, "BindFunction must be called from the server")
	local folder = Util.GetCommSubFolder(parent, "RF"):Expect("Failed to get Comm RF folder")
	local rf = Instance.new("RemoteFunction")
	rf.Name = name
	local hasInbound = type(inboundMiddleware) == "table" and #inboundMiddleware > 0
	local hasOutbound = type(outboundMiddleware) == "table" and #outboundMiddleware > 0
	local function ProcessOutbound(player, ...)
		local args = table.pack(...)
		for _, middlewareFunc in ipairs(outboundMiddleware) do
			local middlewareResult = table.pack(middlewareFunc(player, args))
			if not middlewareResult[1] then
				return table.unpack(middlewareResult, 2, middlewareResult.n)
			end
			args.n = #args
		end
		return table.unpack(args, 1, args.n)
	end
	if hasInbound and hasOutbound then
		local function OnServerInvoke(player, ...)
			local args = table.pack(...)
			for _, middlewareFunc in ipairs(inboundMiddleware) do
				local middlewareResult = table.pack(middlewareFunc(player, args))
				if not middlewareResult[1] then
					return table.unpack(middlewareResult, 2, middlewareResult.n)
				end
				args.n = #args
			end
			return ProcessOutbound(player, func(player, table.unpack(args, 1, args.n)))
		end
		rf.OnServerInvoke = OnServerInvoke
	elseif hasInbound then
		local function OnServerInvoke(player, ...)
			local args = table.pack(...)
			for _, middlewareFunc in ipairs(inboundMiddleware) do
				local middlewareResult = table.pack(middlewareFunc(player, args))
				if not middlewareResult[1] then
					return table.unpack(middlewareResult, 2, middlewareResult.n)
				end
				args.n = #args
			end
			return func(player, table.unpack(args, 1, args.n))
		end
		rf.OnServerInvoke = OnServerInvoke
	elseif hasOutbound then
		local function OnServerInvoke(player, ...)
			return ProcessOutbound(player, func(player, ...))
		end
		rf.OnServerInvoke = OnServerInvoke
	else
		rf.OnServerInvoke = func
	end
	rf.Parent = folder
	return rf
end

function Server.WrapMethod(
	parent: Instance,
	tbl: {},
	name: string,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
): RemoteFunction
	assert(Util.IsServer, "WrapMethod must be called from the server")
	local fn = tbl[name]
	assert(type(fn) == "function", "Value at index " .. name .. " must be a function; got " .. type(fn))
	return Server.BindFunction(parent, name, function(...)
		return fn(tbl, ...)
	end, inboundMiddleware, outboundMiddleware)
end

function Server.CreateSignal(
	parent: Instance,
	name: string,
	reliable: boolean?,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
)
	assert(Util.IsServer, "CreateSignal must be called from the server")
	local folder = Util.GetCommSubFolder(parent, "RE"):Expect("Failed to get Comm RE folder")
	local rs = RemoteSignal.new(folder, name, reliable, inboundMiddleware, outboundMiddleware)
	return rs
end

function Server.CreateProperty(
	parent: Instance,
	name: string,
	initialValue: any,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
)
	assert(Util.IsServer, "CreateProperty must be called from the server")
	local folder = Util.GetCommSubFolder(parent, "RP"):Expect("Failed to get Comm RP folder")
	local rp = RemoteProperty.new(folder, name, initialValue, inboundMiddleware, outboundMiddleware)
	return rp
end

return Server
]]></string>
              </Properties>
              <Item class="ModuleScript" referent="23">
                <Properties>
                  <string name="Name">RemoteProperty</string>
                  <string name="Source"><![CDATA[-- RemoteProperty
-- Stephen Leitnick
-- December 20, 2021

local Players = game:GetService("Players")

local RemoteSignal = require(script.Parent.RemoteSignal)
local Types = require(script.Parent.Parent.Types)
local Util = require(script.Parent.Parent.Util)

local None = Util.None

--[=[
	@class RemoteProperty
	@server
	Created via `ServerComm:CreateProperty()`.

	Values set can be anything that can pass through a
	[RemoteEvent](https://developer.roblox.com/en-us/articles/Remote-Functions-and-Events#parameter-limitations).

	Here is a cheat-sheet for the below methods:
	- Setting data
		- `Set`: Set "top" value for all current and future players. Overrides any custom-set data per player.
		- `SetTop`: Set the "top" value for all players, but does _not_ override any custom-set data per player.
		- `SetFor`: Set custom data for the given player. Overrides the "top" value. (_Can be nil_)
		- `SetForList`: Same as `SetFor`, but accepts a list of players.
		- `SetFilter`: Accepts a predicate function which checks for which players to set.
	- Clearing data
		- `ClearFor`: Clears the custom data set for a given player. Player will start using the "top" level value instead.
		- `ClearForList`: Same as `ClearFor`, but accepts a list of players.
		- `ClearFilter`: Accepts a predicate function which checks for which players to clear.
	- Getting data
		- `Get`: Retrieves the "top" value
		- `GetFor`: Gets the current value for the given player. If cleared, returns the top value.

	:::caution Network
	Calling any of the data setter methods (e.g. `Set()`) will
	fire the underlying RemoteEvent to replicate data to the
	clients. Therefore, setting data should only occur when it
	is necessary to change the data that the clients receive.
	:::

	:::caution Tables
	Tables _can_ be used with RemoteProperties. However, the
	RemoteProperty object will _not_ watch for changes within
	the table. Therefore, anytime changes are made to the table,
	the data must be set again using one of the setter methods.
	:::
]=]
local RemoteProperty = {}
RemoteProperty.__index = RemoteProperty

function RemoteProperty.new(
	parent: Instance,
	name: string,
	initialValue: any,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
)
	local self = setmetatable({}, RemoteProperty)
	self._rs = RemoteSignal.new(parent, name, false, inboundMiddleware, outboundMiddleware)
	self._value = initialValue
	self._perPlayer = {}
	self._playerRemoving = Players.PlayerRemoving:Connect(function(player)
		self._perPlayer[player] = nil
	end)
	self._rs:Connect(function(player)
		local playerValue = self._perPlayer[player]
		local value = if playerValue == nil then self._value elseif playerValue == None then nil else playerValue
		self._rs:Fire(player, value)
	end)
	return self
end

--[=[
	Sets the top-level value of all clients to the same value.
	
	:::note Override Per-Player Data
	This will override any per-player data that was set using
	`SetFor` or `SetFilter`. To avoid overriding this data,
	`SetTop` can be used instead.
	:::

	```lua
	-- Examples
	remoteProperty:Set(10)
	remoteProperty:Set({SomeData = 32})
	remoteProperty:Set("HelloWorld")
	```
]=]
function RemoteProperty:Set(value: any)
	self._value = value
	table.clear(self._perPlayer)
	self._rs:FireAll(value)
end

--[=[
	Set the top-level value of the property, but does not override
	any per-player data (e.g. set with `SetFor` or `SetFilter`).
	Any player without custom-set data will receive this new data.

	This is useful if certain players have specific values that
	should not be changed, but all other players should receive
	the same new value.

	```lua
	-- Using just 'Set' overrides per-player data:
	remoteProperty:SetFor(somePlayer, "CustomData")
	remoteProperty:Set("Data")
	print(remoteProperty:GetFor(somePlayer)) --> "Data"

	-- Using 'SetTop' does not override:
	remoteProperty:SetFor(somePlayer, "CustomData")
	remoteProperty:SetTop("Data")
	print(remoteProperty:GetFor(somePlayer)) --> "CustomData"
	```
]=]
function RemoteProperty:SetTop(value: any)
	self._value = value
	for _, player in ipairs(Players:GetPlayers()) do
		if self._perPlayer[player] == nil then
			self._rs:Fire(player, value)
		end
	end
end

--[=[
	@param value any -- Value to set for the clients (and to the predicate)
	Sets the value for specific clients that pass the `predicate`
	function test. This can be used to finely set the values
	based on more control logic (e.g. setting certain values
	per team).

	```lua
	-- Set the value of "NewValue" to players with a name longer than 10 characters:
	remoteProperty:SetFilter(function(player)
		return #player.Name > 10
	end, "NewValue")
	```
]=]
function RemoteProperty:SetFilter(predicate: (Player, any) -> boolean, value: any)
	for _, player in ipairs(Players:GetPlayers()) do
		if predicate(player, value) then
			self:SetFor(player, value)
		end
	end
end

--[=[
	Set the value of the property for a specific player. This
	will override the value used by `Set` (and the initial value
	set for the property when created).

	This value _can_ be `nil`. In order to reset the value for a
	given player and let the player use the top-level value held
	by this property, either use `Set` to set all players' data,
	or use `ClearFor`.

	```lua
	remoteProperty:SetFor(somePlayer, "CustomData")
	```
]=]
function RemoteProperty:SetFor(player: Player, value: any)
	if player.Parent then
		self._perPlayer[player] = if value == nil then None else value
	end
	self._rs:Fire(player, value)
end

--[=[
	Set the value of the property for specific players. This just
	loops through the players given and calls `SetFor`.

	```lua
	local players = {player1, player2, player3}
	remoteProperty:SetForList(players, "CustomData")
	```
]=]
function RemoteProperty:SetForList(players: { Player }, value: any)
	for _, player in ipairs(players) do
		self:SetFor(player, value)
	end
end

--[=[
	Clears the custom property value for the given player. When
	this occurs, the player will reset to use the top-level
	value held by this property (either the value set when the
	property was created, or the last value set by `Set`).

	```lua
	remoteProperty:Set("DATA")

	remoteProperty:SetFor(somePlayer, "CUSTOM_DATA")
	print(remoteProperty:GetFor(somePlayer)) --> "CUSTOM_DATA"

	-- DOES NOT CLEAR, JUST SETS CUSTOM DATA TO NIL:
	remoteProperty:SetFor(somePlayer, nil)
	print(remoteProperty:GetFor(somePlayer)) --> nil

	-- CLEAR:
	remoteProperty:ClearFor(somePlayer)
	print(remoteProperty:GetFor(somePlayer)) --> "DATA"
	```
]=]
function RemoteProperty:ClearFor(player: Player)
	if self._perPlayer[player] == nil then
		return
	end
	self._perPlayer[player] = nil
	self._rs:Fire(player, self._value)
end

--[=[
	Clears the custom value for the given players. This
	just loops through the list of players and calls
	the `ClearFor` method for each player.
]=]
function RemoteProperty:ClearForList(players: { Player })
	for _, player in ipairs(players) do
		self:ClearFor(player)
	end
end

--[=[
	The same as `SetFiler`, except clears the custom value
	for any player that passes the predicate.
]=]
function RemoteProperty:ClearFilter(predicate: (Player) -> boolean)
	for _, player in ipairs(Players:GetPlayers()) do
		if predicate(player) then
			self:ClearFor(player)
		end
	end
end

--[=[
	Returns the top-level value held by the property. This will
	either be the initial value set, or the last value set
	with `Set()`.

	```lua
	remoteProperty:Set("Data")
	print(remoteProperty:Get()) --> "Data"
	```
]=]
function RemoteProperty:Get(): any
	return self._value
end

--[=[
	Returns the current value for the given player. This value
	will depend on if `SetFor` or `SetFilter` has affected the
	custom value for the player. If so, that custom value will
	be returned. Otherwise, the top-level value will be used
	(e.g. value from `Set`).

	```lua
	-- Set top level data:
	remoteProperty:Set("Data")
	print(remoteProperty:GetFor(somePlayer)) --> "Data"

	-- Set custom data:
	remoteProperty:SetFor(somePlayer, "CustomData")
	print(remoteProperty:GetFor(somePlayer)) --> "CustomData"

	-- Set top level again, overriding custom data:
	remoteProperty:Set("NewData")
	print(remoteProperty:GetFor(somePlayer)) --> "NewData"

	-- Set custom data again, and set top level without overriding:
	remoteProperty:SetFor(somePlayer, "CustomData")
	remoteProperty:SetTop("Data")
	print(remoteProperty:GetFor(somePlayer)) --> "CustomData"

	-- Clear custom data to use top level data:
	remoteProperty:ClearFor(somePlayer)
	print(remoteProperty:GetFor(somePlayer)) --> "Data"
	```
]=]
function RemoteProperty:GetFor(player: Player): any
	local playerValue = self._perPlayer[player]
	local value = if playerValue == nil then self._value elseif playerValue == None then nil else playerValue
	return value
end

--[=[
	Destroys the RemoteProperty object.
]=]
function RemoteProperty:Destroy()
	self._rs:Destroy()
	self._playerRemoving:Disconnect()
end

return RemoteProperty
]]></string>
                </Properties>
              </Item>
              <Item class="ModuleScript" referent="24">
                <Properties>
                  <string name="Name">RemoteSignal</string>
                  <string name="Source"><![CDATA[-- RemoteSignal
-- Stephen Leitnick
-- December 20, 2021

local Players = game:GetService("Players")

local Signal = require(script.Parent.Parent.Parent.Signal)
local Types = require(script.Parent.Parent.Types)

--[=[
	@class RemoteSignal
	@server
	Created via `ServerComm:CreateSignal()`.
]=]
local RemoteSignal = {}
RemoteSignal.__index = RemoteSignal

--[=[
	@within RemoteSignal
	@interface Connection
	.Disconnect () -> nil
	.Connected boolean

	Represents a connection.
]=]

function RemoteSignal.new(
	parent: Instance,
	name: string,
	unreliable: boolean?,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
)
	local self = setmetatable({}, RemoteSignal)
	self._re = if unreliable == true then Instance.new("UnreliableRemoteEvent") else Instance.new("RemoteEvent")
	self._re.Name = name
	self._re.Parent = parent
	if outboundMiddleware and #outboundMiddleware > 0 then
		self._hasOutbound = true
		self._outbound = outboundMiddleware
	else
		self._hasOutbound = false
	end
	if inboundMiddleware and #inboundMiddleware > 0 then
		self._directConnect = false
		self._signal = Signal.new()
		self._re.OnServerEvent:Connect(function(player, ...)
			local args = table.pack(...)
			for _, middlewareFunc in inboundMiddleware do
				local middlewareResult = table.pack(middlewareFunc(player, args))
				if not middlewareResult[1] then
					return
				end
				args.n = #args
			end
			self._signal:Fire(player, table.unpack(args, 1, args.n))
		end)
	else
		self._directConnect = true
	end
	return self
end

--[=[
	@return boolean
	Returns `true` if the underlying RemoteSignal is bound to an
	UnreliableRemoteEvent object.
]=]
function RemoteSignal:IsUnreliable(): boolean
	return self._re:IsA("UnreliableRemoteEvent")
end

--[=[
	@param fn (player: Player, ...: any) -> nil -- The function to connect
	@return Connection
	Connect a function to the signal. Anytime a matching ClientRemoteSignal
	on a client fires, the connected function will be invoked with the
	arguments passed by the client.
]=]
function RemoteSignal:Connect(fn)
	if self._directConnect then
		return self._re.OnServerEvent:Connect(fn)
	else
		return self._signal:Connect(fn)
	end
end

function RemoteSignal:_processOutboundMiddleware(player: Player?, ...: any)
	if not self._hasOutbound then
		return ...
	end
	local args = table.pack(...)
	for _, middlewareFunc in self._outbound do
		local middlewareResult = table.pack(middlewareFunc(player, args))
		if not middlewareResult[1] then
			return table.unpack(middlewareResult, 2, middlewareResult.n)
		end
		args.n = #args
	end
	return table.unpack(args, 1, args.n)
end

--[=[
	@param player Player -- The target client
	@param ... any -- Arguments passed to the client
	Fires the signal at the specified client with any arguments.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::
]=]
function RemoteSignal:Fire(player: Player, ...: any)
	self._re:FireClient(player, self:_processOutboundMiddleware(player, ...))
end

--[=[
	@param ... any
	Fires the signal at _all_ clients with any arguments.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::
]=]
function RemoteSignal:FireAll(...: any)
	self._re:FireAllClients(self:_processOutboundMiddleware(nil, ...))
end

--[=[
	@param ignorePlayer Player -- The client to ignore
	@param ... any -- Arguments passed to the other clients
	Fires the signal to all clients _except_ the specified
	client.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::
]=]
function RemoteSignal:FireExcept(ignorePlayer: Player, ...: any)
	self:FireFilter(function(plr)
		return plr ~= ignorePlayer
	end, ...)
end

--[=[
	@param predicate (player: Player, argsFromFire: ...) -> boolean
	@param ... any -- Arguments to pass to the clients (and to the predicate)
	Fires the signal at any clients that pass the `predicate`
	function test. This can be used to fire signals with much
	more control logic.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::

	:::caution Predicate Before Middleware
	The arguments sent to the predicate are sent _before_ getting
	transformed by any middleware.
	:::

	```lua
	-- Fire signal to players of the same team:
	remoteSignal:FireFilter(function(player)
		return player.Team.Name == "Best Team"
	end)
	```
]=]
function RemoteSignal:FireFilter(predicate: (Player, ...any) -> boolean, ...: any)
	for _, player in Players:GetPlayers() do
		if predicate(player, ...) then
			self._re:FireClient(player, self:_processOutboundMiddleware(nil, ...))
		end
	end
end

--[=[
	Fires a signal at the clients within the `players` table. This is
	useful when signals need to fire for a specific set of players.

	For more complex firing, see `FireFilter`.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::

	```lua
	local players = {somePlayer1, somePlayer2, somePlayer3}
	remoteSignal:FireFor(players, "Hello, players!")
	```
]=]
function RemoteSignal:FireFor(players: { Player }, ...: any)
	for _, player in players do
		self._re:FireClient(player, self:_processOutboundMiddleware(nil, ...))
	end
end

--[=[
	Destroys the RemoteSignal object.
]=]
function RemoteSignal:Destroy()
	self._re:Destroy()
	if self._signal then
		self._signal:Destroy()
	end
end

return RemoteSignal
]]></string>
                </Properties>
              </Item>
              <Item class="ModuleScript" referent="25">
                <Properties>
                  <string name="Name">ServerComm</string>
                  <string name="Source"><![CDATA[-- ServerComm
-- Stephen Leitnick
-- December 20, 2021

local Comm = require(script.Parent)
local Types = require(script.Parent.Parent.Types)
local Util = require(script.Parent.Parent.Util)

--[=[
	@class ServerComm
	@server
]=]
local ServerComm = {}
ServerComm.__index = ServerComm

--[=[
	@within ServerComm
	@type ServerMiddlewareFn (player: Player, args: {any}) -> (shouldContinue: boolean, ...: any)
	The middleware function takes the client player and the arguments (as a table array), and should
	return `true|false` to indicate if the process should continue.

	If returning `false`, the optional varargs after the `false` are used as the new return values
	to whatever was calling the middleware.
]=]
--[=[
	@within ServerComm
	@type ServerMiddleware {ServerMiddlewareFn}
	Array of middleware functions.
]=]

--[=[
	@return ServerComm
	Constructs a ServerComm object. The `namespace` parameter is used
	in cases where more than one ServerComm object may be bound
	to the same object. Otherwise, a default namespace is used.

	```lua
	local serverComm = ServerComm.new(game:GetService("ReplicatedStorage"))

	-- If many might exist in the given parent, use a unique namespace:
	local serverComm = ServerComm.new(game:GetService("ReplicatedStorage"), "MyNamespace")
	```
]=]
function ServerComm.new(parent: Instance, namespace: string?)
	assert(Util.IsServer, "ServerComm must be constructed from the server")
	assert(typeof(parent) == "Instance", "Parent must be of type Instance")
	local ns = Util.DefaultCommFolderName
	if namespace then
		ns = namespace
	end
	assert(not parent:FindFirstChild(ns), "Parent already has another ServerComm bound to namespace " .. ns)
	local self = setmetatable({}, ServerComm)
	self._instancesFolder = Instance.new("Folder")
	self._instancesFolder.Name = ns
	self._instancesFolder.Parent = parent
	return self
end

--[=[
	@param name string
	@param fn (player: Player, ...: any) -> ...: any
	@param inboundMiddleware ServerMiddleware?
	@param outboundMiddleware ServerMiddleware?
	@return RemoteFunction
	Creates a RemoteFunction and binds the given function to it. Inbound
	and outbound middleware can be applied if desired.

	```lua
	local function GetSomething(player: Player)
		return "Something"
	end

	serverComm:BindFunction("GetSomething", GetSomething)
	```
]=]
function ServerComm:BindFunction(
	name: string,
	fn: Types.FnBind,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
): RemoteFunction
	return Comm.BindFunction(self._instancesFolder, name, fn, inboundMiddleware, outboundMiddleware)
end

--[=[
	@param tbl table
	@param name string
	@param inboundMiddleware ServerMiddleware?
	@param outboundMiddleware ServerMiddleware?
	@return RemoteFunction

	Binds a function to a table method. The name must match the
	name of the method in the table. The same name will be used
	on the client to access the given function.

	```lua
	local MyObject = {
		_Data = 10,
	}

	function MyObject:GetData(player: Player)
		return self._Data
	end

	serverComm:WrapMethod(MyObject, "GetData")
	```
]=]
function ServerComm:WrapMethod(
	tbl: {},
	name: string,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
): RemoteFunction
	return Comm.WrapMethod(self._instancesFolder, tbl, name, inboundMiddleware, outboundMiddleware)
end

--[=[
	@param name string
	@param unreliable boolean?
	@param inboundMiddleware ServerMiddleware?
	@param outboundMiddleware ServerMiddleware?
	@return RemoteSignal

	Creates a signal that can be used to fire data to the clients
	or receive data from the clients.

	By default, signals use RemoteEvents internally. However, if
	the `unreliable` argument is set to `true`, then an
	UnreliableRemoteEvent will be used instead.

	```lua
	local mySignal = serverComm:CreateSignal("MySignal")

	-- Examples of firing in different ways (see docs for RemoteSignal for further info):
	mySignal:Fire(somePlayer, "Hello world")
	mySignal:FireAll("Hi there")
	mySignal:FireExcept(somePlayer, "Hello everyone except " .. somePlayer.Name)
	mySignal:FireFilter(function(player) return player.Team == someCoolTeam end, "Hello cool team")

	-- Example of listening for clients to send data:
	mySignal:Connect(function(player, message)
		print("Got a message from " .. player.Name .. ":", message)
	end)
	```
]=]
function ServerComm:CreateSignal(
	name: string,
	unreliable: boolean?,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
)
	return Comm.CreateSignal(self._instancesFolder, name, unreliable, inboundMiddleware, outboundMiddleware)
end

--[=[
	@param name string
	@param initialValue any
	@param inboundMiddleware ServerMiddleware?
	@param outboundMiddleware ServerMiddleware?
	@return RemoteProperty

	Create a property object which will replicate its property value to
	the clients. Optionally, specific clients can be targeted with
	different property values.

	```lua
	local comm = Comm.ServerComm.new(game:GetService("ReplicatedStorage"))

	local mapInfo = comm:CreateProperty("MapInfo", {
		MapName = "TheAwesomeMap",
		MapDuration = 60,
	})

	-- Change the data:
	mapInfo:Set({
		MapName = "AnotherMap",
		MapDuration = 30,
	})

	-- Change the data for one player:
	mapInfo:SetFor(somePlayer, {
		MapName = "ASpecialMapForYou",
		MapDuration = 90,
	})

	-- Change data based on a predicate function:
	mapInfo:SetFilter(function(player)
		return player.Team == game.Teams.SomeSpecialTeam
	end, {
		MapName = "TeamMap",
		MapDuration = 20,
	})
	```
]=]
function ServerComm:CreateProperty(
	name: string,
	initialValue: any,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
)
	return Comm.CreateProperty(self._instancesFolder, name, initialValue, inboundMiddleware, outboundMiddleware)
end

--[=[
	Destroy the ServerComm object.
]=]
function ServerComm:Destroy()
	self._instancesFolder:Destroy()
end

return ServerComm
]]></string>
                </Properties>
              </Item>
            </Item>
            <Item class="ModuleScript" referent="26">
              <Properties>
                <string name="Name">Types</string>
                <string name="Source"><![CDATA[-- Types
-- Stephen Leitnick
-- December 20, 2021

export type Args = {
	n: number,
	[any]: any,
}

export type FnBind = (Instance, ...any) -> ...any

export type ServerMiddlewareFn = (Instance, Args) -> (boolean, ...any)
export type ServerMiddleware = { ServerMiddlewareFn }

export type ClientMiddlewareFn = (Args) -> (boolean, ...any)
export type ClientMiddleware = { ClientMiddlewareFn }

return nil
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="27">
              <Properties>
                <string name="Name">Util</string>
                <string name="Source"><![CDATA[local RunService = game:GetService("RunService")

local Option = require(script.Parent.Parent.Option)

local Util = {}

Util.IsServer = RunService:IsServer()
Util.WaitForChildTimeout = 60
Util.DefaultCommFolderName = "__comm__"
Util.None = newproxy()

function Util.GetCommSubFolder(parent: Instance, subFolderName: string): Option.Option
	local subFolder: Instance = nil
	if Util.IsServer then
		subFolder = parent:FindFirstChild(subFolderName)
		if not subFolder then
			subFolder = Instance.new("Folder")
			subFolder.Name = subFolderName
			subFolder.Parent = parent
		end
	else
		subFolder = parent:WaitForChild(subFolderName, Util.WaitForChildTimeout)
	end
	return Option.Wrap(subFolder)
end

return Util
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="28">
              <Properties>
                <string name="Name">wally</string>
                <string name="Source">return {
	dependencies = {
		Option = "sleitnick/option@1",
		Promise = "evaera/promise@4",
		Signal = "sleitnick/signal@2",
	},
	package = {
		authors = {"Stephen Leitnick"},
		description = "Comm library for remote communication",
		license = "MIT",
		name = "sleitnick/comm",
		realm = "shared",
		registry = "https://github.com/UpliftGames/wally-index",
		version = "1.0.1",
	},
}</string>
              </Properties>
            </Item>
          </Item>
        </Item>
        <Item class="Folder" referent="29">
          <Properties>
            <string name="Name">sleitnick_knit@1.7.0</string>
          </Properties>
          <Item class="ModuleScript" referent="30">
            <Properties>
              <string name="Name">Comm</string>
              <string name="Source"><![CDATA[return require(script.Parent.Parent["sleitnick_comm@1.0.1"]["comm"])
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="31">
            <Properties>
              <string name="Name">Promise</string>
              <string name="Source"><![CDATA[return require(script.Parent.Parent["evaera_promise@4.0.0"]["promise"])
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="32">
            <Properties>
              <string name="Name">knit</string>
              <string name="Source"><![CDATA[local RunService = game:GetService("RunService")

if RunService:IsServer() then
	return require(script.KnitServer)
else
	local KnitServer = script:FindFirstChild("KnitServer")
	if KnitServer and RunService:IsRunning() then
		KnitServer:Destroy()
	end

	return require(script.KnitClient)
end
]]></string>
            </Properties>
            <Item class="ModuleScript" referent="33">
              <Properties>
                <string name="Name">KnitClient</string>
                <string name="Source"><![CDATA[--[=[
	@interface Middleware
	.Inbound ClientMiddleware?
	.Outbound ClientMiddleware?
	@within KnitClient
]=]
type Middleware = {
	Inbound: ClientMiddleware?,
	Outbound: ClientMiddleware?,
}

--[=[
	@type ClientMiddlewareFn (args: {any}) -> (shouldContinue: boolean, ...: any)
	@within KnitClient

	For more info, see [ClientComm](https://sleitnick.github.io/RbxUtil/api/ClientComm/) documentation.
]=]
type ClientMiddlewareFn = (args: { any }) -> (boolean, ...any)

--[=[
	@type ClientMiddleware {ClientMiddlewareFn}
	@within KnitClient
	An array of client middleware functions.
]=]
type ClientMiddleware = { ClientMiddlewareFn }

--[=[
	@type PerServiceMiddleware {[string]: Middleware}
	@within KnitClient
]=]
type PerServiceMiddleware = { [string]: Middleware }

--[=[
	@interface ControllerDef
	.Name string
	.[any] any
	@within KnitClient
	Used to define a controller when creating it in `CreateController`.
]=]
type ControllerDef = {
	Name: string,
	[any]: any,
}

--[=[
	@interface Controller
	.Name string
	.[any] any
	@within KnitClient
]=]
type Controller = {
	Name: string,
	[any]: any,
}

--[=[
	@interface Service
	.[any] any
	@within KnitClient
]=]
type Service = {
	[any]: any,
}

--[=[
	@interface KnitOptions
	.ServicePromises boolean?
	.Middleware Middleware?
	.PerServiceMiddleware PerServiceMiddleware?
	@within KnitClient

	- `ServicePromises` defaults to `true` and indicates if service methods use promises.
	- Each service will go through the defined middleware, unless the service
	has middleware defined in `PerServiceMiddleware`.
]=]
type KnitOptions = {
	ServicePromises: boolean,
	Middleware: Middleware?,
	PerServiceMiddleware: PerServiceMiddleware?,
}

local defaultOptions: KnitOptions = {
	ServicePromises = true,
	Middleware = nil,
	PerServiceMiddleware = {},
}

local selectedOptions = nil

--[=[
	@class KnitClient
	@client
]=]
local KnitClient = {}

--[=[
	@prop Player Player
	@within KnitClient
	@readonly
	Reference to the LocalPlayer.
]=]
KnitClient.Player = game:GetService("Players").LocalPlayer

--[=[
	@prop Util Folder
	@within KnitClient
	@readonly
	References the Util folder. Should only be accessed when using Knit as
	a standalone module. If using Knit from Wally, modules should just be
	pulled in via Wally instead of relying on Knit's Util folder, as this
	folder only contains what is necessary for Knit to run in Wally mode.
]=]
KnitClient.Util = (script.Parent :: Instance).Parent

local Promise = require(KnitClient.Util.Promise)
local Comm = require(KnitClient.Util.Comm)
local ClientComm = Comm.ClientComm

local controllers: { [string]: Controller } = {}
local services: { [string]: Service } = {}
local servicesFolder = nil

local started = false
local startedComplete = false
local onStartedComplete = Instance.new("BindableEvent")

local function DoesControllerExist(controllerName: string): boolean
	local controller: Controller? = controllers[controllerName]

	return controller ~= nil
end

local function GetServicesFolder()
	if not servicesFolder then
		servicesFolder = (script.Parent :: Instance):WaitForChild("Services")
	end

	return servicesFolder
end

local function GetMiddlewareForService(serviceName: string)
	local knitMiddleware = if selectedOptions.Middleware ~= nil then selectedOptions.Middleware else {}
	local serviceMiddleware = selectedOptions.PerServiceMiddleware[serviceName]

	return if serviceMiddleware ~= nil then serviceMiddleware else knitMiddleware
end

local function BuildService(serviceName: string)
	local folder = GetServicesFolder()
	local middleware = GetMiddlewareForService(serviceName)
	local clientComm = ClientComm.new(folder, selectedOptions.ServicePromises, serviceName)
	local service = clientComm:BuildObject(middleware.Inbound, middleware.Outbound)

	services[serviceName] = service

	return service
end

--[=[
	Creates a new controller.

	:::caution
	Controllers must be created _before_ calling `Knit.Start()`.
	:::
	```lua
	-- Create a controller
	local MyController = Knit.CreateController {
		Name = "MyController",
	}

	function MyController:KnitStart()
		print("MyController started")
	end

	function MyController:KnitInit()
		print("MyController initialized")
	end
	```
]=]
function KnitClient.CreateController(controllerDef: ControllerDef): Controller
	assert(type(controllerDef) == "table", `Controller must be a table; got {type(controllerDef)}`)
	assert(type(controllerDef.Name) == "string", `Controller.Name must be a string; got {type(controllerDef.Name)}`)
	assert(#controllerDef.Name > 0, "Controller.Name must be a non-empty string")
	assert(not DoesControllerExist(controllerDef.Name), `Controller {controllerDef.Name} already exists`)
	assert(not started, `Controllers cannot be created after calling "Knit.Start()"`)

	local controller = controllerDef :: Controller
	controllers[controller.Name] = controller

	return controller
end

--[=[
	Requires all the modules that are children of the given parent. This is an easy
	way to quickly load all controllers that might be in a folder.
	```lua
	Knit.AddControllers(somewhere.Controllers)
	```
]=]
function KnitClient.AddControllers(parent: Instance): { Controller }
	assert(not started, `Controllers cannot be added after calling "Knit.Start()"`)

	local addedControllers = {}
	for _, v in parent:GetChildren() do
		if not v:IsA("ModuleScript") then
			continue
		end

		table.insert(addedControllers, require(v))
	end

	return addedControllers
end

--[=[
	Requires all the modules that are descendants of the given parent.
]=]
function KnitClient.AddControllersDeep(parent: Instance): { Controller }
	assert(not started, `Controllers cannot be added after calling "Knit.Start()"`)

	local addedControllers = {}
	for _, v in parent:GetDescendants() do
		if not v:IsA("ModuleScript") then
			continue
		end

		table.insert(addedControllers, require(v))
	end

	return addedControllers
end

--[=[
	Returns a Service object which is a reflection of the remote objects
	within the Client table of the given service. Throws an error if the
	service is not found.

	If a service's Client table contains RemoteSignals and/or RemoteProperties,
	these values are reflected as
	[ClientRemoteSignals](https://sleitnick.github.io/RbxUtil/api/ClientRemoteSignal) and
	[ClientRemoteProperties](https://sleitnick.github.io/RbxUtil/api/ClientRemoteProperty).

	```lua
	-- Server-side service creation:
	local MyService = Knit.CreateService {
		Name = "MyService",
		Client = {
			MySignal = Knit.CreateSignal(),
			MyProperty = Knit.CreateProperty("Hello"),
		},
	}
	function MyService:AddOne(player, number)
		return number + 1
	end

	-------------------------------------------------

	-- Client-side service reflection:
	local MyService = Knit.GetService("MyService")

	-- Call a method:
	local num = MyService:AddOne(5) --> 6

	-- Fire a signal to the server:
	MyService.MySignal:Fire("Hello")

	-- Listen for signals from the server:
	MyService.MySignal:Connect(function(message)
		print(message)
	end)

	-- Observe the initial value and changes to properties:
	MyService.MyProperty:Observe(function(value)
		print(value)
	end)
	```

	:::caution
	Services are only exposed to the client if the service has remote-based
	content in the Client table. If not, the service will not be visible
	to the client. `KnitClient.GetService` will only work on services that
	expose remote-based content on their Client tables.
	:::
]=]
function KnitClient.GetService(serviceName: string): Service
	local service = services[serviceName]
	if service then
		return service
	end

	assert(started, "Cannot call GetService until Knit has been started")
	assert(type(serviceName) == "string", `ServiceName must be a string; got {type(serviceName)}`)

	return BuildService(serviceName)
end

--[=[
	Gets the controller by name. Throws an error if the controller
	is not found.
]=]
function KnitClient.GetController(controllerName: string): Controller
	local controller = controllers[controllerName]
	if controller then
		return controller
	end

	assert(started, "Cannot call GetController until Knit has been started")
	assert(type(controllerName) == "string", `ControllerName must be a string; got {type(controllerName)}`)
	error(`Could not find controller "{controllerName}". Check to verify a controller with this name exists.`, 2)
end

--[=[
	Gets a table of all controllers.
]=]
function KnitClient.GetControllers(): { [string]: Controller }
	assert(started, "Cannot call GetControllers until Knit has been started")

	return controllers
end

--[=[
	@return Promise
	Starts Knit. Should only be called once per client.
	```lua
	Knit.Start():andThen(function()
		print("Knit started!")
	end):catch(warn)
	```

	By default, service methods exposed to the client will return promises.
	To change this behavior, set the `ServicePromises` option to `false`:
	```lua
	Knit.Start({ServicePromises = false}):andThen(function()
		print("Knit started!")
	end):catch(warn)
	```
]=]
function KnitClient.Start(options: KnitOptions?)
	if started then
		return Promise.reject("Knit already started")
	end

	started = true

	table.freeze(controllers)

	if options == nil then
		selectedOptions = defaultOptions
	else
		assert(typeof(options) == "table", `KnitOptions should be a table or nil; got {typeof(options)}`)
		selectedOptions = options
		for k, v in defaultOptions do
			if selectedOptions[k] == nil then
				selectedOptions[k] = v
			end
		end
	end
	if type(selectedOptions.PerServiceMiddleware) ~= "table" then
		selectedOptions.PerServiceMiddleware = {}
	end

	return Promise.new(function(resolve)
		-- Init:
		local promisesStartControllers = {}

		for _, controller in controllers do
			if type(controller.KnitInit) == "function" then
				table.insert(
					promisesStartControllers,
					Promise.new(function(r)
						debug.setmemorycategory(controller.Name)
						controller:KnitInit()
						r()
					end)
				)
			end
		end

		resolve(Promise.all(promisesStartControllers))
	end):andThen(function()
		-- Start:
		for _, controller in controllers do
			if type(controller.KnitStart) == "function" then
				task.spawn(function()
					debug.setmemorycategory(controller.Name)
					controller:KnitStart()
				end)
			end
		end

		startedComplete = true
		onStartedComplete:Fire()

		task.defer(function()
			onStartedComplete:Destroy()
		end)
	end)
end

--[=[
	@return Promise
	Returns a promise that is resolved once Knit has started. This is useful
	for any code that needs to tie into Knit controllers but is not the script
	that called `Start`.
	```lua
	Knit.OnStart():andThen(function()
		local MyController = Knit.GetController("MyController")
		MyController:DoSomething()
	end):catch(warn)
	```
]=]
function KnitClient.OnStart()
	if startedComplete then
		return Promise.resolve()
	else
		return Promise.fromEvent(onStartedComplete.Event)
	end
end

return KnitClient
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="34">
              <Properties>
                <string name="Name">KnitServer</string>
                <string name="Source"><![CDATA[--[=[
	@interface Middleware
	.Inbound ServerMiddleware?
	.Outbound ServerMiddleware?
	@within KnitServer
]=]
type Middleware = {
	Inbound: ServerMiddleware?,
	Outbound: ServerMiddleware?,
}

--[=[
	@type ServerMiddlewareFn (player: Player, args: {any}) -> (shouldContinue: boolean, ...: any)
	@within KnitServer

	For more info, see [ServerComm](https://sleitnick.github.io/RbxUtil/api/ServerComm/) documentation.
]=]
type ServerMiddlewareFn = (player: Player, args: { any }) -> (boolean, ...any)

--[=[
	@type ServerMiddleware {ServerMiddlewareFn}
	@within KnitServer
	An array of server middleware functions.
]=]
type ServerMiddleware = { ServerMiddlewareFn }

--[=[
	@interface ServiceDef
	.Name string
	.Client table?
	.Middleware Middleware?
	.[any] any
	@within KnitServer
	Used to define a service when creating it in `CreateService`.

	The middleware tables provided will be used instead of the Knit-level
	middleware (if any). This allows fine-tuning each service's middleware.
	These can also be left out or `nil` to not include middleware.
]=]
type ServiceDef = {
	Name: string,
	Client: { [any]: any }?,
	Middleware: Middleware?,
	[any]: any,
}

--[=[
	@interface Service
	.Name string
	.Client ServiceClient
	.KnitComm Comm
	.[any] any
	@within KnitServer
]=]
type Service = {
	Name: string,
	Client: ServiceClient,
	KnitComm: any,
	[any]: any,
}

--[=[
	@interface ServiceClient
	.Server Service
	.[any] any
	@within KnitServer
]=]
type ServiceClient = {
	Server: Service,
	[any]: any,
}

--[=[
	@interface KnitOptions
	.Middleware Middleware?
	@within KnitServer

	- Middleware will apply to all services _except_ ones that define
	their own middleware.
]=]
type KnitOptions = {
	Middleware: Middleware?,
}

local defaultOptions: KnitOptions = {
	Middleware = nil,
}

local selectedOptions = nil

--[=[
	@class KnitServer
	@server
	Knit server-side lets developers create services and expose methods and signals
	to the clients.

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

	-- Load service modules within some folder:
	Knit.AddServices(somewhere.Services)

	-- Start Knit:
	Knit.Start():andThen(function()
		print("Knit started")
	end):catch(warn)
	```
]=]
local KnitServer = {}

--[=[
	@prop Util Folder
	@within KnitServer
	@readonly
	References the Util folder. Should only be accessed when using Knit as
	a standalone module. If using Knit from Wally, modules should just be
	pulled in via Wally instead of relying on Knit's Util folder, as this
	folder only contains what is necessary for Knit to run in Wally mode.
]=]
KnitServer.Util = (script.Parent :: Instance).Parent

local SIGNAL_MARKER = newproxy(true)
getmetatable(SIGNAL_MARKER).__tostring = function()
	return "SIGNAL_MARKER"
end

local UNRELIABLE_SIGNAL_MARKER = newproxy(true)
getmetatable(UNRELIABLE_SIGNAL_MARKER).__tostring = function()
	return "UNRELIABLE_SIGNAL_MARKER"
end

local PROPERTY_MARKER = newproxy(true)
getmetatable(PROPERTY_MARKER).__tostring = function()
	return "PROPERTY_MARKER"
end

local knitRepServiceFolder = Instance.new("Folder")
knitRepServiceFolder.Name = "Services"

local Promise = require(KnitServer.Util.Promise)
local Comm = require(KnitServer.Util.Comm)
local ServerComm = Comm.ServerComm

local services: { [string]: Service } = {}
local started = false
local startedComplete = false
local onStartedComplete = Instance.new("BindableEvent")

local function DoesServiceExist(serviceName: string): boolean
	local service: Service? = services[serviceName]

	return service ~= nil
end

--[=[
	Constructs a new service.

	:::caution
	Services must be created _before_ calling `Knit.Start()`.
	:::
	```lua
	-- Create a service
	local MyService = Knit.CreateService {
		Name = "MyService",
		Client = {},
	}

	-- Expose a ToAllCaps remote function to the clients
	function MyService.Client:ToAllCaps(player, msg)
		return msg:upper()
	end

	-- Knit will call KnitStart after all services have been initialized
	function MyService:KnitStart()
		print("MyService started")
	end

	-- Knit will call KnitInit when Knit is first started
	function MyService:KnitInit()
		print("MyService initialize")
	end
	```
]=]
function KnitServer.CreateService(serviceDef: ServiceDef): Service
	assert(type(serviceDef) == "table", `Service must be a table; got {type(serviceDef)}`)
	assert(type(serviceDef.Name) == "string", `Service.Name must be a string; got {type(serviceDef.Name)}`)
	assert(#serviceDef.Name > 0, "Service.Name must be a non-empty string")
	assert(not DoesServiceExist(serviceDef.Name), `Service "{serviceDef.Name}" already exists`)
	assert(not started, `Services cannot be created after calling "Knit.Start()"`)

	local service = serviceDef
	service.KnitComm = ServerComm.new(knitRepServiceFolder, serviceDef.Name)

	if type(service.Client) ~= "table" then
		service.Client = { Server = service }
	else
		if service.Client.Server ~= service then
			service.Client.Server = service
		end
	end

	services[service.Name] = service

	return service
end

--[=[
	Requires all the modules that are children of the given parent. This is an easy
	way to quickly load all services that might be in a folder.
	```lua
	Knit.AddServices(somewhere.Services)
	```
]=]
function KnitServer.AddServices(parent: Instance): { Service }
	assert(not started, `Services cannot be added after calling "Knit.Start()"`)

	local addedServices = {}
	for _, v in parent:GetChildren() do
		if not v:IsA("ModuleScript") then
			continue
		end

		table.insert(addedServices, require(v))
	end

	return addedServices
end

--[=[
	Requires all the modules that are descendants of the given parent.
]=]
function KnitServer.AddServicesDeep(parent: Instance): { Service }
	assert(not started, `Services cannot be added after calling "Knit.Start()"`)

	local addedServices = {}
	for _, v in parent:GetDescendants() do
		if not v:IsA("ModuleScript") then
			continue
		end

		table.insert(addedServices, require(v))
	end

	return addedServices
end

--[=[
	Gets the service by name. Throws an error if the service is not found.
]=]
function KnitServer.GetService(serviceName: string): Service
	assert(started, "Cannot call GetService until Knit has been started")
	assert(type(serviceName) == "string", `ServiceName must be a string; got {type(serviceName)}`)

	return assert(services[serviceName], `Could not find service "{serviceName}"`) :: Service
end

--[=[
	Gets a table of all services.
]=]
function KnitServer.GetServices(): { [string]: Service }
	assert(started, "Cannot call GetServices until Knit has been started")

	return services
end

--[=[
	@return SIGNAL_MARKER
	Returns a marker that will transform the current key into
	a RemoteSignal once the service is created. Should only
	be called within the Client table of a service.

	See [RemoteSignal](https://sleitnick.github.io/RbxUtil/api/RemoteSignal)
	documentation for more info.
	```lua
	local MyService = Knit.CreateService {
		Name = "MyService",
		Client = {
			-- Create the signal marker, which will turn into a
			-- RemoteSignal when Knit.Start() is called:
			MySignal = Knit.CreateSignal(),
		},
	}

	function MyService:KnitInit()
		-- Connect to the signal:
		self.Client.MySignal:Connect(function(player, ...) end)
	end
	```
]=]
function KnitServer.CreateSignal()
	return SIGNAL_MARKER
end

--[=[
	@return UNRELIABLE_SIGNAL_MARKER

	Returns a marker that will transform the current key into
	an unreliable RemoteSignal once the service is created. Should
	only be called within the Client table of a service.

	See [RemoteSignal](https://sleitnick.github.io/RbxUtil/api/RemoteSignal)
	documentation for more info.

	:::info Unreliable Events
	Internally, this uses UnreliableRemoteEvents, which allows for
	network communication that is unreliable and unordered. This is
	useful for events that are not crucial for gameplay, since the
	delivery of the events may occur out of order or not at all.

	See  the documentation for [UnreliableRemoteEvents](https://create.roblox.com/docs/reference/engine/classes/UnreliableRemoteEvent)
	for more info.
]=]
function KnitServer.CreateUnreliableSignal()
	return UNRELIABLE_SIGNAL_MARKER
end

--[=[
	@return PROPERTY_MARKER
	Returns a marker that will transform the current key into
	a RemoteProperty once the service is created. Should only
	be called within the Client table of a service. An initial
	value can be passed along as well.

	RemoteProperties are great for replicating data to all of
	the clients. Different data can also be set per client.

	See [RemoteProperty](https://sleitnick.github.io/RbxUtil/api/RemoteProperty)
	documentation for more info.

	```lua
	local MyService = Knit.CreateService {
		Name = "MyService",
		Client = {
			-- Create the property marker, which will turn into a
			-- RemoteProperty when Knit.Start() is called:
			MyProperty = Knit.CreateProperty("HelloWorld"),
		},
	}

	function MyService:KnitInit()
		-- Change the value of the property:
		self.Client.MyProperty:Set("HelloWorldAgain")
	end
	```
]=]
function KnitServer.CreateProperty(initialValue: any)
	return { PROPERTY_MARKER, initialValue }
end

--[=[
	@return Promise
	Starts Knit. Should only be called once.

	Optionally, `KnitOptions` can be passed in order to set
	Knit's custom configurations.

	:::caution
	Be sure that all services have been created _before_
	calling `Start`. Services cannot be added later.
	:::

	```lua
	Knit.Start():andThen(function()
		print("Knit started!")
	end):catch(warn)
	```
	
	Example of Knit started with options:
	```lua
	Knit.Start({
		Middleware = {
			Inbound = {
				function(player, args)
					print("Player is giving following args to server:", args)
					return true
				end
			},
		},
	}):andThen(function()
		print("Knit started!")
	end):catch(warn)
	```
]=]
function KnitServer.Start(options: KnitOptions?)
	if started then
		return Promise.reject("Knit already started")
	end

	started = true

	table.freeze(services)

	if options == nil then
		selectedOptions = defaultOptions
	else
		assert(typeof(options) == "table", `KnitOptions should be a table or nil; got {typeof(options)}`)
		selectedOptions = options
		for k, v in defaultOptions do
			if selectedOptions[k] == nil then
				selectedOptions[k] = v
			end
		end
	end

	return Promise.new(function(resolve)
		local knitMiddleware = if selectedOptions.Middleware ~= nil then selectedOptions.Middleware else {}

		-- Bind remotes:
		for _, service in services do
			local middleware = if service.Middleware ~= nil then service.Middleware else {}
			local inbound = if middleware.Inbound ~= nil then middleware.Inbound else knitMiddleware.Inbound
			local outbound = if middleware.Outbound ~= nil then middleware.Outbound else knitMiddleware.Outbound

			service.Middleware = nil

			for k, v in service.Client do
				if type(v) == "function" then
					service.KnitComm:WrapMethod(service.Client, k, inbound, outbound)
				elseif v == SIGNAL_MARKER then
					service.Client[k] = service.KnitComm:CreateSignal(k, false, inbound, outbound)
				elseif v == UNRELIABLE_SIGNAL_MARKER then
					service.Client[k] = service.KnitComm:CreateSignal(k, true, inbound, outbound)
				elseif type(v) == "table" and v[1] == PROPERTY_MARKER then
					service.Client[k] = service.KnitComm:CreateProperty(k, v[2], inbound, outbound)
				end
			end
		end

		-- Init:
		local promisesInitServices = {}
		for _, service in services do
			if type(service.KnitInit) == "function" then
				table.insert(
					promisesInitServices,
					Promise.new(function(r)
						debug.setmemorycategory(service.Name)
						service:KnitInit()
						r()
					end)
				)
			end
		end

		resolve(Promise.all(promisesInitServices))
	end):andThen(function()
		-- Start:
		for _, service in services do
			if type(service.KnitStart) == "function" then
				task.spawn(function()
					debug.setmemorycategory(service.Name)
					service:KnitStart()
				end)
			end
		end

		startedComplete = true
		onStartedComplete:Fire()

		task.defer(function()
			onStartedComplete:Destroy()
		end)

		-- Expose service remotes to everyone:
		knitRepServiceFolder.Parent = script.Parent
	end)
end

--[=[
	@return Promise
	Returns a promise that is resolved once Knit has started. This is useful
	for any code that needs to tie into Knit services but is not the script
	that called `Start`.
	```lua
	Knit.OnStart():andThen(function()
		local MyService = Knit.Services.MyService
		MyService:DoSomething()
	end):catch(warn)
	```
]=]
function KnitServer.OnStart()
	if startedComplete then
		return Promise.resolve()
	else
		return Promise.fromEvent(onStartedComplete.Event)
	end
end

return KnitServer
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="35">
              <Properties>
                <string name="Name">wally</string>
                <string name="Source">return {
	dependencies = {
		Comm = "sleitnick/comm@^1",
		Promise = "evaera/promise@^4",
	},
	package = {
		description = "Knit is a lightweight game framework",
		license = "MIT",
		name = "sleitnick/knit",
		realm = "shared",
		registry = "https://github.com/UpliftGames/wally-index",
		version = "1.7.0",
	},
}</string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="36">
              <Properties>
                <string name="Name">wally_bundle</string>
                <string name="Source">return {
	dependencies = {
		Comm = "sleitnick/comm@^1",
		Component = "sleitnick/component@^2",
		EnumList = "sleitnick/enum-list@^2",
		Input = "sleitnick/input@^2",
		Option = "sleitnick/option@^1",
		Promise = "evaera/promise@^4",
		Signal = "sleitnick/signal@^2",
		Streamable = "sleitnick/streamable@^1",
		TableUtil = "sleitnick/table-util@^1",
		Timer = "sleitnick/timer@^1",
		Trove = "sleitnick/trove@^1",
	},
	package = {
		name = "sleitnick/knit-bundle",
		realm = "shared",
		registry = "https://github.com/UpliftGames/wally-index",
		version = "0.1.0",
	},
}</string>
              </Properties>
            </Item>
          </Item>
        </Item>
        <Item class="Folder" referent="37">
          <Properties>
            <string name="Name">sleitnick_option@1.0.5</string>
          </Properties>
          <Item class="ModuleScript" referent="38">
            <Properties>
              <string name="Name">option</string>
              <string name="Source"><![CDATA[-- Option
-- Stephen Leitnick
-- August 28, 2020

--[[

	MatchTable {
		Some: (value: any) -> any
		None: () -> any
	}

	CONSTRUCTORS:

		Option.Some(anyNonNilValue): Option<any>
		Option.Wrap(anyValue): Option<any>


	STATIC FIELDS:

		Option.None: Option<None>


	STATIC METHODS:

		Option.Is(obj): boolean


	METHODS:

		opt:Match(): (matches: MatchTable) -> any
		opt:IsSome(): boolean
		opt:IsNone(): boolean
		opt:Unwrap(): any
		opt:Expect(errMsg: string): any
		opt:ExpectNone(errMsg: string): void
		opt:UnwrapOr(default: any): any
		opt:UnwrapOrElse(default: () -> any): any
		opt:And(opt2: Option<any>): Option<any>
		opt:AndThen(predicate: (unwrapped: any) -> Option<any>): Option<any>
		opt:Or(opt2: Option<any>): Option<any>
		opt:OrElse(orElseFunc: () -> Option<any>): Option<any>
		opt:XOr(opt2: Option<any>): Option<any>
		opt:Contains(value: any): boolean

	--------------------------------------------------------------------

	Options are useful for handling nil-value cases. Any time that an
	operation might return nil, it is useful to instead return an
	Option, which will indicate that the value might be nil, and should
	be explicitly checked before using the value. This will help
	prevent common bugs caused by nil values that can fail silently.


	Example:

	local result1 = Option.Some(32)
	local result2 = Option.Some(nil)
	local result3 = Option.Some("Hi")
	local result4 = Option.Some(nil)
	local result5 = Option.None

	-- Use 'Match' to match if the value is Some or None:
	result1:Match {
		Some = function(value) print(value) end;
		None = function() print("No value") end;
	}

	-- Raw check:
	if result2:IsSome() then
		local value = result2:Unwrap() -- Explicitly call Unwrap
		print("Value of result2:", value)
	end

	if result3:IsNone() then
		print("No result for result3")
	end

	-- Bad, will throw error bc result4 is none:
	local value = result4:Unwrap()

--]]

export type MatchTable<T> = {
	Some: (value: T) -> any,
	None: () -> any,
}

export type MatchFn<T> = (matches: MatchTable<T>) -> any

export type DefaultFn<T> = () -> T

export type AndThenFn<T> = (value: T) -> Option<T>

export type OrElseFn<T> = () -> Option<T>

export type Option<T> = typeof(setmetatable(
	{} :: {
		Match: (self: Option<T>) -> MatchFn<T>,
		IsSome: (self: Option<T>) -> boolean,
		IsNone: (self: Option<T>) -> boolean,
		Contains: (self: Option<T>, value: T) -> boolean,
		Unwrap: (self: Option<T>) -> T,
		Expect: (self: Option<T>, errMsg: string) -> T,
		ExpectNone: (self: Option<T>, errMsg: string) -> nil,
		UnwrapOr: (self: Option<T>, default: T) -> T,
		UnwrapOrElse: (self: Option<T>, defaultFn: DefaultFn<T>) -> T,
		And: (self: Option<T>, opt2: Option<T>) -> Option<T>,
		AndThen: (self: Option<T>, predicate: AndThenFn<T>) -> Option<T>,
		Or: (self: Option<T>, opt2: Option<T>) -> Option<T>,
		OrElse: (self: Option<T>, orElseFunc: OrElseFn<T>) -> Option<T>,
		XOr: (self: Option<T>, opt2: Option<T>) -> Option<T>,
	},
	{} :: {
		__index: Option<T>,
	}
))

local CLASSNAME = "Option"

--[=[
	@class Option

	Represents an optional value in Lua. This is useful to avoid `nil` bugs, which can
	go silently undetected within code and cause hidden or hard-to-find bugs.
]=]
local Option = {}
Option.__index = Option

function Option._new(value)
	local self = setmetatable({
		ClassName = CLASSNAME,
		_v = value,
		_s = (value ~= nil),
	}, Option)
	return self
end

--[=[
	@param value T
	@return Option<T>

	Creates an Option instance with the given value. Throws an error
	if the given value is `nil`.
]=]
function Option.Some(value)
	assert(value ~= nil, "Option.Some() value cannot be nil")
	return Option._new(value)
end

--[=[
	@param value T
	@return Option<T> | Option<None>

	Safely wraps the given value as an option. If the
	value is `nil`, returns `Option.None`, otherwise
	returns `Option.Some(value)`.
]=]
function Option.Wrap(value)
	if value == nil then
		return Option.None
	else
		return Option.Some(value)
	end
end

--[=[
	@param obj any
	@return boolean
	Returns `true` if `obj` is an Option.
]=]
function Option.Is(obj)
	return type(obj) == "table" and getmetatable(obj) == Option
end

--[=[
	@param obj any
	Throws an error if `obj` is not an Option.
]=]
function Option.Assert(obj)
	assert(Option.Is(obj), "Result was not of type Option")
end

--[=[
	@param data table
	@return Option
	Deserializes the data into an Option. This data should have come from
	the `option:Serialize()` method.
]=]
function Option.Deserialize(data) -- type data = {ClassName: string, Value: any}
	assert(type(data) == "table" and data.ClassName == CLASSNAME, "Invalid data for deserializing Option")
	return data.Value == nil and Option.None or Option.Some(data.Value)
end

--[=[
	@return table
	Returns a serialized version of the option.
]=]
function Option:Serialize()
	return {
		ClassName = self.ClassName,
		Value = self._v,
	}
end

--[=[
	@param matches {Some: (value: any) -> any, None: () -> any}
	@return any

	Matches against the option.

	```lua
	local opt = Option.Some(32)
	opt:Match {
		Some = function(num) print("Number", num) end,
		None = function() print("No value") end,
	}
	```
]=]
function Option:Match(matches)
	local onSome = matches.Some
	local onNone = matches.None
	assert(type(onSome) == "function", "Missing 'Some' match")
	assert(type(onNone) == "function", "Missing 'None' match")
	if self:IsSome() then
		return onSome(self:Unwrap())
	else
		return onNone()
	end
end

--[=[
	@return boolean
	Returns `true` if the option has a value.
]=]
function Option:IsSome()
	return self._s
end

--[=[
	@return boolean
	Returns `true` if the option is None.
]=]
function Option:IsNone()
	return not self._s
end

--[=[
	@param msg string
	@return value: any
	Unwraps the value in the option, otherwise throws an error with `msg` as the error message.
	```lua
	local opt = Option.Some(10)
	print(opt:Expect("No number")) -> 10
	print(Option.None:Expect("No number")) -- Throws an error "No number"
	```
]=]
function Option:Expect(msg)
	assert(self:IsSome(), msg)
	return self._v
end

--[=[
	@param msg string
	Throws an error with `msg` as the error message if the value is _not_ None.
]=]
function Option:ExpectNone(msg)
	assert(self:IsNone(), msg)
end

--[=[
	@return value: any
	Returns the value in the option, or throws an error if the option is None.
]=]
function Option:Unwrap()
	return self:Expect("Cannot unwrap option of None type")
end

--[=[
	@param default any
	@return value: any
	If the option holds a value, returns the value. Otherwise, returns `default`.
]=]
function Option:UnwrapOr(default)
	if self:IsSome() then
		return self:Unwrap()
	else
		return default
	end
end

--[=[
	@param defaultFn () -> any
	@return value: any
	If the option holds a value, returns the value. Otherwise, returns the
	result of the `defaultFn` function.
]=]
function Option:UnwrapOrElse(defaultFn)
	if self:IsSome() then
		return self:Unwrap()
	else
		return defaultFn()
	end
end

--[=[
	@param optionB Option
	@return Option
	Returns `optionB` if the calling option has a value,
	otherwise returns None.

	```lua
	local optionA = Option.Some(32)
	local optionB = Option.Some(64)
	local opt = optionA:And(optionB)
	-- opt == optionB

	local optionA = Option.None
	local optionB = Option.Some(64)
	local opt = optionA:And(optionB)
	-- opt == Option.None
	```
]=]
function Option:And(optionB)
	if self:IsSome() then
		return optionB
	else
		return Option.None
	end
end

--[=[
	@param andThenFn (value: any) -> Option
	@return value: Option
	If the option holds a value, then the `andThenFn`
	function is called with the held value of the option,
	and then the resultant Option returned by the `andThenFn`
	is returned. Otherwise, None is returned.

	```lua
	local optA = Option.Some(32)
	local optB = optA:AndThen(function(num)
		return Option.Some(num * 2)
	end)
	print(optB:Expect("Expected number")) --> 64
	```
]=]
function Option:AndThen(andThenFn)
	if self:IsSome() then
		local result = andThenFn(self:Unwrap())
		Option.Assert(result)
		return result
	else
		return Option.None
	end
end

--[=[
	@param optionB Option
	@return Option
	If caller has a value, returns itself. Otherwise, returns `optionB`.
]=]
function Option:Or(optionB)
	if self:IsSome() then
		return self
	else
		return optionB
	end
end

--[=[
	@param orElseFn () -> Option
	@return Option
	If caller has a value, returns itself. Otherwise, returns the
	option generated by the `orElseFn` function.
]=]
function Option:OrElse(orElseFn)
	if self:IsSome() then
		return self
	else
		local result = orElseFn()
		Option.Assert(result)
		return result
	end
end

--[=[
	@param optionB Option
	@return Option
	If both `self` and `optionB` have values _or_ both don't have a value,
	then this returns None. Otherwise, it returns the option that does have
	a value.
]=]
function Option:XOr(optionB)
	local someOptA = self:IsSome()
	local someOptB = optionB:IsSome()
	if someOptA == someOptB then
		return Option.None
	elseif someOptA then
		return self
	else
		return optionB
	end
end

--[=[
	@param predicate (value: any) -> boolean
	@return Option
	Returns `self` if this option has a value and the predicate returns `true.
	Otherwise, returns None.
]=]
function Option:Filter(predicate)
	if self:IsNone() or not predicate(self._v) then
		return Option.None
	else
		return self
	end
end

--[=[
	@param value any
	@return boolean
	Returns `true` if this option contains `value`.
]=]
function Option:Contains(value)
	return self:IsSome() and self._v == value
end

--[=[
	@return string
	Metamethod to transform the option into a string.
	```lua
	local optA = Option.Some(64)
	local optB = Option.None
	print(optA) --> Option<number>
	print(optB) --> Option<None>
	```
]=]
function Option:__tostring()
	if self:IsSome() then
		return ("Option<" .. typeof(self._v) .. ">")
	else
		return "Option<None>"
	end
end

--[=[
	@return boolean
	@param opt Option
	Metamethod to check equality between two options. Returns `true` if both
	options hold the same value _or_ both options are None.
	```lua
	local o1 = Option.Some(32)
	local o2 = Option.Some(32)
	local o3 = Option.Some(64)
	local o4 = Option.None
	local o5 = Option.None

	print(o1 == o2) --> true
	print(o1 == o3) --> false
	print(o1 == o4) --> false
	print(o4 == o5) --> true
	```
]=]
function Option:__eq(opt)
	if Option.Is(opt) then
		if self:IsSome() and opt:IsSome() then
			return (self:Unwrap() == opt:Unwrap())
		elseif self:IsNone() and opt:IsNone() then
			return true
		end
	end
	return false
end

--[=[
	@prop None Option<None>
	@within Option
	Represents no value.
]=]
Option.None = Option._new()

return (Option :: any) :: {
	Some: <T>(value: T) -> Option<T>,
	Wrap: <T>(value: T) -> Option<T>,

	Is: (obj: any) -> boolean,

	None: Option<any>,
}
]]></string>
            </Properties>
            <Item class="ModuleScript" referent="39">
              <Properties>
                <string name="Name">init.spec</string>
                <string name="Source"><![CDATA[return function()
	local Option = require(script.Parent)

	describe("Some", function()
		it("should create some option", function()
			local opt = Option.Some(true)
			expect(opt:IsSome()).to.equal(true)
		end)

		it("should fail to create some option with nil", function()
			expect(function()
				Option.Some(nil)
			end).to.throw()
		end)

		it("should not be none", function()
			local opt = Option.Some(10)
			expect(opt:IsNone()).to.equal(false)
		end)
	end)

	describe("None", function()
		it("should be able to reference none", function()
			expect(function()
				local _none = Option.None
			end).never.to.throw()
		end)

		it("should be able to check if none", function()
			local none = Option.None
			expect(none:IsNone()).to.equal(true)
		end)

		it("should be able to check if not some", function()
			local none = Option.None
			expect(none:IsSome()).to.equal(false)
		end)
	end)

	describe("Equality", function()
		it("should equal the same some from same options", function()
			local opt = Option.Some(32)
			expect(opt).to.equal(opt)
		end)

		it("should equal the same some from different options", function()
			local opt1 = Option.Some(32)
			local opt2 = Option.Some(32)
			expect(opt1).to.equal(opt2)
		end)
	end)

	describe("Assert", function()
		it("should assert that a some option is an option", function()
			expect(Option.Is(Option.Some(10))).to.equal(true)
		end)

		it("should assert that a none option is an option", function()
			expect(Option.Is(Option.None)).to.equal(true)
		end)

		it("should assert that a non-option is not an option", function()
			expect(Option.Is(10)).to.equal(false)
			expect(Option.Is(true)).to.equal(false)
			expect(Option.Is(false)).to.equal(false)
			expect(Option.Is("Test")).to.equal(false)
			expect(Option.Is({})).to.equal(false)
			expect(Option.Is(function() end)).to.equal(false)
			expect(Option.Is(coroutine.create(function() end))).to.equal(false)
			expect(Option.Is(Option)).to.equal(false)
		end)
	end)

	describe("Unwrap", function()
		it("should unwrap a some option", function()
			local opt = Option.Some(10)
			expect(function()
				opt:Unwrap()
			end).never.to.throw()
			expect(opt:Unwrap()).to.equal(10)
		end)

		it("should fail to unwrap a none option", function()
			local opt = Option.None
			expect(function()
				opt:Unwrap()
			end).to.throw()
		end)
	end)

	describe("Expect", function()
		it("should expect a some option", function()
			local opt = Option.Some(10)
			expect(function()
				opt:Expect("Expecting some value")
			end).never.to.throw()
			expect(opt:Unwrap()).to.equal(10)
		end)

		it("should fail when expecting on a none option", function()
			local opt = Option.None
			expect(function()
				opt:Expect("Expecting some value")
			end).to.throw()
		end)
	end)

	describe("ExpectNone", function()
		it("should fail to expect a none option", function()
			local opt = Option.Some(10)
			expect(function()
				opt:ExpectNone("Expecting some value")
			end).to.throw()
		end)

		it("should expect a none option", function()
			local opt = Option.None
			expect(function()
				opt:ExpectNone("Expecting some value")
			end).never.to.throw()
		end)
	end)

	describe("UnwrapOr", function()
		it("should unwrap a some option", function()
			local opt = Option.Some(10)
			expect(opt:UnwrapOr(20)).to.equal(10)
		end)

		it("should unwrap a none option", function()
			local opt = Option.None
			expect(opt:UnwrapOr(20)).to.equal(20)
		end)
	end)

	describe("UnwrapOrElse", function()
		it("should unwrap a some option", function()
			local opt = Option.Some(10)
			local result = opt:UnwrapOrElse(function()
				return 30
			end)
			expect(result).to.equal(10)
		end)

		it("should unwrap a none option", function()
			local opt = Option.None
			local result = opt:UnwrapOrElse(function()
				return 30
			end)
			expect(result).to.equal(30)
		end)
	end)

	describe("And", function()
		it("should return the second option with and when both are some", function()
			local opt1 = Option.Some(1)
			local opt2 = Option.Some(2)
			expect(opt1:And(opt2)).to.equal(opt2)
		end)

		it("should return none when first option is some and second option is none", function()
			local opt1 = Option.Some(1)
			local opt2 = Option.None
			expect(opt1:And(opt2):IsNone()).to.equal(true)
		end)

		it("should return none when first option is none and second option is some", function()
			local opt1 = Option.None
			local opt2 = Option.Some(2)
			expect(opt1:And(opt2):IsNone()).to.equal(true)
		end)

		it("should return none when both options are none", function()
			local opt1 = Option.None
			local opt2 = Option.None
			expect(opt1:And(opt2):IsNone()).to.equal(true)
		end)
	end)

	describe("AndThen", function()
		it("should pass the some value to the predicate", function()
			local opt = Option.Some(32)
			opt:AndThen(function(value)
				expect(value).to.equal(32)
				return Option.None
			end)
		end)

		it("should throw if an option is not returned from predicate", function()
			local opt = Option.Some(32)
			expect(function()
				opt:AndThen(function() end)
			end).to.throw()
		end)

		it("should return none if the option is none", function()
			local opt = Option.None
			expect(opt:AndThen(function()
				return Option.Some(10)
			end):IsNone()).to.equal(true)
		end)

		it("should return option of predicate if option is some", function()
			local opt = Option.Some(32)
			local result = opt:AndThen(function()
				return Option.Some(10)
			end)
			expect(result:IsSome()).to.equal(true)
			expect(result:Unwrap()).to.equal(10)
		end)
	end)

	describe("Or", function()
		it("should return the first option if it is some", function()
			local opt1 = Option.Some(10)
			local opt2 = Option.Some(20)
			expect(opt1:Or(opt2)).to.equal(opt1)
		end)

		it("should return the second option if the first one is none", function()
			local opt1 = Option.None
			local opt2 = Option.Some(20)
			expect(opt1:Or(opt2)).to.equal(opt2)
		end)
	end)

	describe("OrElse", function()
		it("should return the first option if it is some", function()
			local opt1 = Option.Some(10)
			local opt2 = Option.Some(20)
			expect(opt1:OrElse(function()
				return opt2
			end)).to.equal(opt1)
		end)

		it("should return the second option if the first one is none", function()
			local opt1 = Option.None
			local opt2 = Option.Some(20)
			expect(opt1:OrElse(function()
				return opt2
			end)).to.equal(opt2)
		end)

		it("should throw if the predicate does not return an option", function()
			local opt1 = Option.None
			expect(function()
				opt1:OrElse(function() end)
			end).to.throw()
		end)
	end)

	describe("XOr", function()
		it("should return first option if first option is some and second option is none", function()
			local opt1 = Option.Some(1)
			local opt2 = Option.None
			expect(opt1:XOr(opt2)).to.equal(opt1)
		end)

		it("should return second option if first option is none and second option is some", function()
			local opt1 = Option.None
			local opt2 = Option.Some(2)
			expect(opt1:XOr(opt2)).to.equal(opt2)
		end)

		it("should return none if first and second option are some", function()
			local opt1 = Option.Some(1)
			local opt2 = Option.Some(2)
			expect(opt1:XOr(opt2)).to.equal(Option.None)
		end)

		it("should return none if first and second option are none", function()
			local opt1 = Option.None
			local opt2 = Option.None
			expect(opt1:XOr(opt2)).to.equal(Option.None)
		end)
	end)

	describe("Filter", function()
		it("should return none if option is none", function()
			local opt = Option.None
			expect(opt:Filter(function() end)).to.equal(Option.None)
		end)

		it("should return none if option is some but fails predicate", function()
			local opt = Option.Some(10)
			expect(opt:Filter(function(_v)
				return false
			end)).to.equal(Option.None)
		end)

		it("should return self if option is some and passes predicate", function()
			local opt = Option.Some(10)
			expect(opt:Filter(function(_v)
				return true
			end)).to.equal(opt)
		end)
	end)

	describe("Contains", function()
		it("should return true if some option contains the given value", function()
			local opt = Option.Some(32)
			expect(opt:Contains(32)).to.equal(true)
		end)

		it("should return false if some option does not contain the given value", function()
			local opt = Option.Some(32)
			expect(opt:Contains(64)).to.equal(false)
		end)

		it("should return false if option is none", function()
			local opt = Option.None
			expect(opt:Contains(64)).to.equal(false)
		end)
	end)

	describe("ToString", function()
		it("should return string of none option", function()
			local opt = Option.None
			expect(tostring(opt)).to.equal("Option<None>")
		end)

		it("should return string of some option with type", function()
			local values = { 10, true, false, "test", {}, function() end, coroutine.create(function() end), workspace }
			for _, value in ipairs(values) do
				local expectedString = ("Option<%s>"):format(typeof(value))
				expect(tostring(Option.Some(value))).to.equal(expectedString)
			end
		end)
	end)
end
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="40">
              <Properties>
                <string name="Name">wally</string>
                <string name="Source">return {
	package = {
		authors = {"Stephen Leitnick"},
		description = "Represent optional values in Lua",
		license = "MIT",
		name = "sleitnick/option",
		realm = "shared",
		registry = "https://github.com/UpliftGames/wally-index",
		version = "1.0.5",
	},
}</string>
              </Properties>
            </Item>
          </Item>
        </Item>
        <Item class="Folder" referent="41">
          <Properties>
            <string name="Name">sleitnick_signal@2.0.3</string>
          </Properties>
          <Item class="ModuleScript" referent="42">
            <Properties>
              <string name="Name">signal</string>
              <string name="Source"><![CDATA[-- -----------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- License:                                                                   --
--   Licensed under the MIT license.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--   sleitnick - August 3rd, 2021 - Modified for Knit.                        --
-- -----------------------------------------------------------------------------

-- Signal types
export type Connection = {
	Disconnect: (self: Connection) -> (),
	Destroy: (self: Connection) -> (),
	Connected: boolean,
}

export type Signal<T...> = {
	Fire: (self: Signal<T...>, T...) -> (),
	FireDeferred: (self: Signal<T...>, T...) -> (),
	Connect: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	Once: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	DisconnectAll: (self: Signal<T...>) -> (),
	GetConnections: (self: Signal<T...>) -> { Connection },
	Destroy: (self: Signal<T...>) -> (),
	Wait: (self: Signal<T...>) -> T...,
}

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
	acquireRunnerThreadAndCallEventHandler(...)
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

--[=[
	@within Signal
	@interface SignalConnection
	.Connected boolean
	.Disconnect (SignalConnection) -> ()

	Represents a connection to a signal.
	```lua
	local connection = signal:Connect(function() end)
	print(connection.Connected) --> true
	connection:Disconnect()
	print(connection.Connected) --> false
	```
]=]

-- Connection class
local Connection = {}
Connection.__index = Connection

function Connection:Disconnect()
	if not self.Connected then
		return
	end
	self.Connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end

Connection.Destroy = Connection.Disconnect

-- Make Connection strict
setmetatable(Connection, {
	__index = function(_tb, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

--[=[
	@within Signal
	@type ConnectionFn (...any) -> ()

	A function connected to a signal.
]=]

--[=[
	@class Signal

	A Signal is a data structure that allows events to be dispatched
	and observed.

	This implementation is a direct copy of the de facto standard, [GoodSignal](https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063),
	with some added methods and typings.

	For example:
	```lua
	local signal = Signal.new()

	-- Subscribe to a signal:
	signal:Connect(function(msg)
		print("Got message:", msg)
	end)

	-- Dispatch an event:
	signal:Fire("Hello world!")
	```
]=]
local Signal = {}
Signal.__index = Signal

--[=[
	Constructs a new Signal

	@return Signal
]=]
function Signal.new<T...>(): Signal<T...>
	local self = setmetatable({
		_handlerListHead = false,
		_proxyHandler = nil,
		_yieldedThreads = nil,
	}, Signal)

	return self
end

--[=[
	Constructs a new Signal that wraps around an RBXScriptSignal.

	@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
	@return Signal

	For example:
	```lua
	local signal = Signal.Wrap(workspace.ChildAdded)
	signal:Connect(function(part) print(part.Name .. " added") end)
	Instance.new("Part").Parent = workspace
	```
]=]
function Signal.Wrap<T...>(rbxScriptSignal: RBXScriptSignal): Signal<T...>
	assert(
		typeof(rbxScriptSignal) == "RBXScriptSignal",
		"Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal)
	)

	local signal = Signal.new()
	signal._proxyHandler = rbxScriptSignal:Connect(function(...)
		signal:Fire(...)
	end)

	return signal
end

--[=[
	Checks if the given object is a Signal.

	@param obj any -- Object to check
	@return boolean -- `true` if the object is a Signal.
]=]
function Signal.Is(obj: any): boolean
	return type(obj) == "table" and getmetatable(obj) == Signal
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called anytime the signal is fired.
	```lua
	signal:Connect(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	```
]=]
function Signal:Connect(fn)
	local connection = setmetatable({
		Connected = true,
		_signal = self,
		_fn = fn,
		_next = false,
	}, Connection)

	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end

	return connection
end

--[=[
	@deprecated v1.3.0 -- Use `Signal:Once` instead.
	@param fn ConnectionFn
	@return SignalConnection
]=]
function Signal:ConnectOnce(fn)
	return self:Once(fn)
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called the next time the signal fires. Once
	the connection is triggered, it will disconnect itself.
	```lua
	signal:Once(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	signal:Fire("This message will not go through", 10)
	```
]=]
function Signal:Once(fn)
	local connection
	local done = false

	connection = self:Connect(function(...)
		if done then
			return
		end

		done = true
		connection:Disconnect()
		fn(...)
	end)

	return connection
end

function Signal:GetConnections()
	local items = {}

	local item = self._handlerListHead
	while item do
		table.insert(items, item)
		item = item._next
	end

	return items
end

-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
--[=[
	Disconnects all connections from the signal.
	```lua
	signal:DisconnectAll()
	```
]=]
function Signal:DisconnectAll()
	local item = self._handlerListHead
	while item do
		item.Connected = false
		item = item._next
	end
	self._handlerListHead = false

	local yieldedThreads = rawget(self, "_yieldedThreads")
	if yieldedThreads then
		for thread in yieldedThreads do
			if coroutine.status(thread) == "suspended" then
				warn(debug.traceback(thread, "signal disconnected; yielded thread cancelled", 2))
				task.cancel(thread)
			end
		end
		table.clear(self._yieldedThreads)
	end
end

-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
--[=[
	@param ... any

	Fire the signal, which will call all of the connected functions with the given arguments.
	```lua
	signal:Fire("Hello")

	-- Any number of arguments can be fired:
	signal:Fire("Hello", 32, {Test = "Test"}, true)
	```
]=]
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item.Connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

--[=[
	@param ... any

	Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
	```lua
	signal:FireDeferred("Hello")
	```
]=]
function Signal:FireDeferred(...)
	local item = self._handlerListHead
	while item do
		local conn = item
		task.defer(function(...)
			if conn.Connected then
				conn._fn(...)
			end
		end, ...)
		item = item._next
	end
end

--[=[
	@return ... any
	@yields

	Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
	Yielding the current thread is not always desirable. If the desire is to only capture the next event
	fired, using `Once` might be a better solution.
	```lua
	task.spawn(function()
		local msg, num = signal:Wait()
		print(msg, num) --> "Hello", 32
	end)
	signal:Fire("Hello", 32)
	```
]=]
function Signal:Wait()
	local yieldedThreads = rawget(self, "_yieldedThreads")
	if not yieldedThreads then
		yieldedThreads = {}
		rawset(self, "_yieldedThreads", yieldedThreads)
	end

	local thread = coroutine.running()
	yieldedThreads[thread] = true

	self:Once(function(...)
		yieldedThreads[thread] = nil

		if coroutine.status(thread) == "suspended" then
			task.spawn(thread, ...)
		end
	end)

	return coroutine.yield()
end

--[=[
	Cleans up the signal.

	Technically, this is only necessary if the signal is created using
	`Signal.Wrap`. Connections should be properly GC'd once the signal
	is no longer referenced anywhere. However, it is still good practice
	to include ways to strictly clean up resources. Calling `Destroy`
	on a signal will also disconnect all connections immediately.
	```lua
	signal:Destroy()
	```
]=]
function Signal:Destroy()
	self:DisconnectAll()

	local proxyHandler = rawget(self, "_proxyHandler")
	if proxyHandler then
		proxyHandler:Disconnect()
	end
end

-- Make signal strict
setmetatable(Signal, {
	__index = function(_tb, key)
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

return table.freeze({
	new = Signal.new,
	Wrap = Signal.Wrap,
	Is = Signal.Is,
})
]]></string>
            </Properties>
            <Item class="ModuleScript" referent="43">
              <Properties>
                <string name="Name">init.test</string>
                <string name="Source"><![CDATA[local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

local function AwaitCondition(predicate: () -> boolean, timeout: number?)
	local start = os.clock()
	timeout = (timeout or 10)
	while true do
		if predicate() then
			return true
		end
		if (os.clock() - start) > timeout then
			return false
		end
		task.wait()
	end
end

return function(ctx: Test.TestContext)
	local Signal = require(script.Parent)

	local signal

	local function NumConns(sig)
		sig = sig or signal
		return #sig:GetConnections()
	end

	ctx:BeforeEach(function()
		signal = Signal.new()
	end)

	ctx:AfterEach(function()
		signal:Destroy()
	end)

	ctx:Describe("Constructor", function()
		ctx:Test("should create a new signal and fire it", function()
			ctx:Expect(Signal.Is(signal)):ToBe(true)
			task.defer(function()
				signal:Fire(10, 20)
			end)
			local n1, n2 = signal:Wait()
			ctx:Expect(n1):ToBe(10)
			ctx:Expect(n2):ToBe(20)
		end)

		ctx:Test("should create a proxy signal and connect to it", function()
			local signalWrap = Signal.Wrap(game:GetService("RunService").Heartbeat)
			ctx:Expect(Signal.Is(signalWrap)):ToBe(true)
			local fired = false
			signalWrap:Connect(function()
				fired = true
			end)
			ctx:Expect(AwaitCondition(function()
				return fired
			end, 2)):ToBe(true)
			signalWrap:Destroy()
		end)
	end)

	ctx:Describe("FireDeferred", function()
		ctx:Test("should be able to fire primitive argument", function()
			local send = 10
			local value
			signal:Connect(function(v)
				value = v
			end)
			signal:FireDeferred(send)
			ctx:Expect(AwaitCondition(function()
				return (send == value)
			end, 1)):ToBe(true)
		end)

		ctx:Test("should be able to fire a reference based argument", function()
			local send = { 10, 20 }
			local value
			signal:Connect(function(v)
				value = v
			end)
			signal:FireDeferred(send)
			ctx:Expect(AwaitCondition(function()
				return (send == value)
			end, 1)):ToBe(true)
		end)
	end)

	ctx:Describe("Fire", function()
		ctx:Test("should be able to fire primitive argument", function()
			local send = 10
			local value
			signal:Connect(function(v)
				value = v
			end)
			signal:Fire(send)
			ctx:Expect(value):ToBe(send)
		end)

		ctx:Test("should be able to fire a reference based argument", function()
			local send = { 10, 20 }
			local value
			signal:Connect(function(v)
				value = v
			end)
			signal:Fire(send)
			ctx:Expect(value):ToBe(send)
		end)
	end)

	ctx:Describe("ConnectOnce", function()
		ctx:Test("should only capture first fire", function()
			local value
			local c = signal:ConnectOnce(function(v)
				value = v
			end)
			ctx:Expect(c.Connected):ToBe(true)
			signal:Fire(10)
			ctx:Expect(c.Connected):ToBe(false)
			signal:Fire(20)
			ctx:Expect(value):ToBe(10)
		end)
	end)

	ctx:Describe("Wait", function()
		ctx:Test("should be able to wait for a signal to fire", function()
			task.defer(function()
				signal:Fire(10, 20, 30)
			end)
			local n1, n2, n3 = signal:Wait()
			ctx:Expect(n1):ToBe(10)
			ctx:Expect(n2):ToBe(20)
			ctx:Expect(n3):ToBe(30)
		end)
	end)

	ctx:Describe("DisconnectAll", function()
		ctx:Test("should disconnect all connections", function()
			signal:Connect(function() end)
			signal:Connect(function() end)
			ctx:Expect(NumConns()):ToBe(2)
			signal:DisconnectAll()
			ctx:Expect(NumConns()):ToBe(0)
		end)
	end)

	ctx:Describe("Disconnect", function()
		ctx:Test("should disconnect connection", function()
			local con = signal:Connect(function() end)
			ctx:Expect(NumConns()):ToBe(1)
			con:Disconnect()
			ctx:Expect(NumConns()):ToBe(0)
		end)

		ctx:Test("should still work if connections disconnected while firing", function()
			local a = 0
			local c
			signal:Connect(function()
				a += 1
			end)
			c = signal:Connect(function()
				c:Disconnect()
				a += 1
			end)
			signal:Connect(function()
				a += 1
			end)
			signal:Fire()
			ctx:Expect(a):ToBe(3)
		end)

		ctx:Test("should still work if connections disconnected while firing deferred", function()
			local a = 0
			local c
			signal:Connect(function()
				a += 1
			end)
			c = signal:Connect(function()
				c:Disconnect()
				a += 1
			end)
			signal:Connect(function()
				a += 1
			end)
			signal:FireDeferred()
			ctx:Expect(AwaitCondition(function()
				return a == 3
			end)):ToBe(true)
		end)
	end)
end
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="44">
              <Properties>
                <string name="Name">wally</string>
                <string name="Source">return {
	package = {
		authors = {"Stephen Leitnick"},
		description = "Signal class",
		exclude = {"node_modules", "package.json", "**/*.ts"},
		license = "MIT",
		name = "sleitnick/signal",
		realm = "shared",
		registry = "https://github.com/UpliftGames/wally-index",
		version = "2.0.3",
	},
}</string>
              </Properties>
            </Item>
          </Item>
        </Item>
        <Item class="Folder" referent="45">
          <Properties>
            <string name="Name">sleitnick_table-util@1.2.1</string>
          </Properties>
          <Item class="ModuleScript" referent="46">
            <Properties>
              <string name="Name">table-util</string>
              <string name="Source"><![CDATA[--!strict

-- TableUtil
-- Stephen Leitnick
-- September 13, 2017

--[=[
	@class TableUtil

	A collection of helpful table utility functions. Many of these functions are carried over from JavaScript or
	Python that are not present in Lua.

	Tables that only work specifically with arrays or dictionaries are marked as such in the documentation.

	:::info Immutability
	All functions (_except_ `SwapRemove`, `SwapRemoveFirstValue`, and `Lock`) treat tables as immutable and will return
	copies of the given table(s) with the operations performed on the copies.
]=]
local TableUtil = {}

local HttpService = game:GetService("HttpService")
local rng = Random.new()

--[=[
	@within TableUtil
	@function Copy
	@param tbl table -- Table to copy
	@param deep boolean? -- Whether or not to perform a deep copy
	@return table

	Creates a copy of the given table. By default, a shallow copy is
	performed. For deep copies, a second boolean argument must be
	passed to the function.

	:::caution No cyclical references
	Deep copies are _not_ protected against cyclical references. Passing
	a table with cyclical references _and_ the `deep` parameter set to
	`true` will result in a stack-overflow.
]=]
local function Copy<T>(t: T, deep: boolean?): T
	if not deep then
		return (table.clone(t :: any) :: any) :: T
	end
	local function DeepCopy(tbl: { any })
		local tCopy = table.clone(tbl)
		for k, v in tCopy do
			if type(v) == "table" then
				tCopy[k] = DeepCopy(v)
			end
		end
		return tCopy
	end
	return DeepCopy(t :: any) :: T
end

--[=[
	@within TableUtil
	@function Sync
	@param srcTbl table -- Source table
	@param templateTbl table -- Template table
	@return table

	Synchronizes the `srcTbl` based on the `templateTbl`. This will make
	sure that `srcTbl` has all of the same keys as `templateTbl`, including
	removing keys in `srcTbl` that are not present in `templateTbl`. This
	is a _deep_ operation, so any nested tables will be synchronized as
	well.

	```lua
	local template = {kills = 0, deaths = 0, xp = 0}
	local data = {kills = 10, experience = 12}
	data = TableUtil.Sync(data, template)
	print(data) --> {kills = 10, deaths = 0, xp = 0}
	```

	:::caution Data Loss Warning
	This is a two-way sync, so the source table will have data
	_removed_ that isn't found in the template table. This can
	be problematic if used for player data, where there might
	be dynamic data added that isn't in the template.

	For player data, use `TableUtil.Reconcile` instead.
]=]
local function Sync<S, T>(srcTbl: S, templateTbl: T): T
	assert(type(srcTbl) == "table", "First argument must be a table")
	assert(type(templateTbl) == "table", "Second argument must be a table")

	local tbl = table.clone(srcTbl)

	-- If 'tbl' has something 'templateTbl' doesn't, then remove it from 'tbl'
	-- If 'tbl' has something of a different type than 'templateTbl', copy from 'templateTbl'
	-- If 'templateTbl' has something 'tbl' doesn't, then add it to 'tbl'
	for k, v in pairs(tbl) do
		local vTemplate = templateTbl[k]

		-- Remove keys not within template:
		if vTemplate == nil then
			tbl[k] = nil

			-- Synchronize data types:
		elseif type(v) ~= type(vTemplate) then
			if type(vTemplate) == "table" then
				tbl[k] = Copy(vTemplate, true)
			else
				tbl[k] = vTemplate
			end

			-- Synchronize sub-tables:
		elseif type(v) == "table" then
			tbl[k] = Sync(v, vTemplate)
		end
	end

	-- Add any missing keys:
	for k, vTemplate in pairs(templateTbl) do
		local v = tbl[k]

		if v == nil then
			if type(vTemplate) == "table" then
				tbl[k] = Copy(vTemplate, true)
			else
				tbl[k] = vTemplate
			end
		end
	end

	return (tbl :: any) :: T
end

--[=[
	@within TableUtil
	@function Reconcile
	@param source table
	@param template table
	@return table

	Performs a one-way sync on the `source` table against the
	`template` table. Any keys found in `template` that are
	not found in `source` will be added to `source`. This is
	useful for syncing player data against data template tables
	to ensure players have all the necessary keys, while
	maintaining existing keys that may no longer be in the
	template.

	This is a deep operation, so nested tables will also be
	properly reconciled.

	```lua
	local template = {kills = 0, deaths = 0, xp = 0}
	local data = {kills = 10, abc = 20}
	local correctedData = TableUtil.Reconcile(data, template)
	
	print(correctedData) --> {kills = 10, deaths = 0, xp = 0, abc = 20}
	```
]=]
local function Reconcile<S, T>(src: S, template: T): S & T
	assert(type(src) == "table", "First argument must be a table")
	assert(type(template) == "table", "Second argument must be a table")

	local tbl = table.clone(src)

	for k, v in template do
		local sv = src[k]
		if sv == nil then
			if type(v) == "table" then
				tbl[k] = Copy(v, true)
			else
				tbl[k] = v
			end
		elseif type(sv) == "table" then
			if type(v) == "table" then
				tbl[k] = Reconcile(sv, v)
			else
				tbl[k] = Copy(sv, true)
			end
		end
	end

	return (tbl :: any) :: S & T
end

--[=[
	@within TableUtil
	@function SwapRemove
	@param tbl table -- Array
	@param i number -- Index

	Removes index `i` in the table by swapping the value at `i` with
	the last value in the array, and then trimming off the last
	value from the array.

	This allows removal of the value at `i` in `O(1)` time, but does
	not preserve array ordering. If a value needs to be removed from
	an array, but ordering of the array does not matter, using
	`SwapRemove` is always preferred over `table.remove`.

	In the following example, we remove "B" at index 2. SwapRemove does
	this by moving the last value "E" over top of "B", and then trimming
	off "E" at the end of the array:
	```lua
	local t = {"A", "B", "C", "D", "E"}
	TableUtil.SwapRemove(t, 2) -- Remove "B"
	print(t) --> {"A", "E", "C", "D"}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
]=]
local function SwapRemove<T>(t: { T }, i: number)
	local n = #t
	t[i] = t[n]
	t[n] = nil
end

--[=[
	@within TableUtil
	@function SwapRemoveFirstValue
	@param tbl table -- Array
	@param v any -- Value to find
	@return number?

	Performs `table.find(tbl, v)` to find the index of the given
	value, and then performs `TableUtil.SwapRemove` on that index.

	```lua
	local t = {"A", "B", "C", "D", "E"}
	TableUtil.SwapRemoveFirstValue(t, "C")
	print(t) --> {"A", "B", "E", "D"}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
]=]
local function SwapRemoveFirstValue<T>(t: { T }, v: T): number?
	local index: number? = table.find(t, v)
	if index then
		SwapRemove(t, index)
	end
	return index
end

--[=[
	@within TableUtil
	@function Map
	@param tbl table
	@param predicate (value: any, key: any, tbl: table) -> newValue: any
	@return table

	Performs a map operation against the given table, which can be used to
	map new values based on the old values at given keys/indices.

	For example:

	```lua
	local t = {A = 10, B = 20, C = 30}
	local t2 = TableUtil.Map(t, function(value)
		return value * 2
	end)
	print(t2) --> {A = 20, B = 40, C = 60}
	```
]=]
local function Map<T, M>(t: { T }, f: (T, number, { T }) -> M): { M }
	assert(type(t) == "table", "First argument must be a table")
	assert(type(f) == "function", "Second argument must be a function")
	local newT = table.create(#t)
	for k, v in t do
		newT[k] = f(v, k, t)
	end
	return newT
end

--[=[
	@within TableUtil
	@function Filter
	@param tbl table
	@param predicate (value: any, key: any, tbl: table) -> keep: boolean
	@return table

	Performs a filter operation against the given table, which can be used to
	filter out unwanted values from the table.

	For example:

	```lua
	local t = {A = 10, B = 20, C = 30}
	local t2 = TableUtil.Filter(t, function(value, key)
		return value > 15
	end)
	print(t2) --> {B = 40, C = 60}
	```
]=]
local function Filter<T>(t: { T }, predicate: (T, any, { T }) -> boolean): { T }
	assert(type(t) == "table", "First argument must be a table")
	assert(type(predicate) == "function", "Second argument must be a function")
	local newT = table.create(#t)
	if #t > 0 then
		local n = 0
		for i, v in t do
			if predicate(v, i, t) then
				n += 1
				newT[n] = v
			end
		end
	else
		for k, v in t do
			if predicate(v, k, t) then
				newT[k] = v
			end
		end
	end
	return newT
end

--[=[
	@within TableUtil
	@function Reduce
	@param tbl table
	@param predicate (accumulator: any, value: any, index: any, tbl: table) -> result: any
	@return table

	Performs a reduce operation against the given table, which can be used to
	reduce the table into a single value. This could be used to sum up a table
	or transform all the values into a compound value of any kind.

	For example:

	```lua
	local t = {10, 20, 30, 40}
	local result = TableUtil.Reduce(t, function(accum, value)
		return accum + value
	end)
	print(result) --> 100
	```
]=]
local function Reduce<T, R>(t: { T }, predicate: (R, T, any, { T }) -> R, init: R): R
	assert(type(t) == "table", "First argument must be a table")
	assert(type(predicate) == "function", "Second argument must be a function")
	local result = init :: R
	if #t > 0 then
		local start = 1
		if init == nil then
			result = (t[1] :: any) :: R
			start = 2
		end
		for i = start, #t do
			result = predicate(result, t[i], i, t)
		end
	else
		local start = nil
		if init == nil then
			result = (next(t) :: any) :: R
			start = result
		end
		for k, v in next, t, start :: any? do
			result = predicate(result, v, k, t)
		end
	end
	return result
end

--[=[
	@within TableUtil
	@function Assign
	@param target table
	@param ... table
	@return table

	Copies all values of the given tables into the `target` table.

	```lua
	local t = {A = 10}
	local t2 = {B = 20}
	local t3 = {C = 30, D = 40}
	local newT = TableUtil.Assign(t, t2, t3)
	print(newT) --> {A = 10, B = 20, C = 30, D = 40}
	```
]=]
local function Assign<T>(target: { T }, ...: { any }): { T } & { any }
	local tbl = table.clone(target)
	for _, src in { ... } do
		for k, v in src do
			tbl[k] = v
		end
	end
	return tbl
end

--[=[
	@within TableUtil
	@function Extend
	@param target table
	@param extension table
	@return table

	Extends the target array with the extension array.

	```lua
	local t = {10, 20, 30}
	local t2 = {30, 40, 50}
	local tNew = TableUtil.Extend(t, t2)
	print(tNew) --> {10, 20, 30, 30, 40, 50}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
]=]
local function Extend<T, E>(target: { T }, extension: { E }): { T } & { E }
	local tbl = table.clone(target) :: { any }
	for _, v in extension do
		table.insert(tbl, v)
	end
	return tbl
end

--[=[
	@within TableUtil
	@function Reverse
	@param tbl table
	@return table

	Reverses the array.

	```lua
	local t = {1, 5, 10}
	local tReverse = TableUtil.Reverse(t)
	print(tReverse) --> {10, 5, 1}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
]=]
local function Reverse<T>(tbl: { T }): { T }
	local n = #tbl
	local tblRev = table.create(n)
	for i = 1, n do
		tblRev[i] = tbl[n - i + 1]
	end
	return tblRev
end

--[=[
	@within TableUtil
	@function Shuffle
	@param tbl table
	@param rngOverride Random?
	@return table

	Shuffles the table.

	```lua
	local t = {1, 2, 3, 4, 5, 6, 7, 8, 9}
	local shuffled = TableUtil.Shuffle(t)
	print(shuffled) --> e.g. {9, 4, 6, 7, 3, 1, 5, 8, 2}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
]=]
local function Shuffle<T>(tbl: { T }, rngOverride: Random?): { T }
	assert(type(tbl) == "table", "First argument must be a table")
	local shuffled = table.clone(tbl)
	local random = if typeof(rngOverride) == "Random" then rngOverride else rng
	for i = #tbl, 2, -1 do
		local j = random:NextInteger(1, i)
		shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
	end
	return shuffled
end

--[=[
	@within TableUtil
	@function Sample
	@param tbl table
	@param sampleSize number
	@param rngOverride Random?
	@return table

	Returns a random sample of the table.

	```lua
	local t = {1, 2, 3, 4, 5, 6, 7, 8, 9}
	local sample = TableUtil.Sample(t, 3)
	print(sample) --> e.g. {6, 2, 5}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
]=]
local function Sample<T>(tbl: { T }, size: number, rngOverride: Random?): { T }
	assert(type(tbl) == "table", "First argument must be a table")
	assert(type(size) == "number", "Second argument must be a number")

	-- If given table is empty, just return a new empty table:
	local len = #tbl
	if len == 0 then
		return {}
	end

	local shuffled = table.clone(tbl)
	local sample = table.create(size)
	local random = if typeof(rngOverride) == "Random" then rngOverride else rng

	-- Clamp sample size to be no larger than the given table size:
	size = math.clamp(size, 1, len)

	for i = 1, size do
		local j = random:NextInteger(i, len)
		shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
	end

	table.move(shuffled, 1, size, 1, sample)

	return sample
end

--[=[
	@within TableUtil
	@function Flat
	@param tbl table
	@param depth number?
	@return table

	Returns a new table where all sub-arrays have been
	bubbled up to the top. The depth at which the scan
	is performed is dictated by the `depth` parameter,
	which is set to `1` by default.

	```lua
	local t = {{10, 20}, {90, 100}, {30, 15}}
	local flat = TableUtil.Flat(t)
	print(flat) --> {10, 20, 90, 100, 30, 15}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
]=]
local function Flat<T>(tbl: { T }, depth: number?): { T }
	local maxDepth: number = if type(depth) == "number" then depth else 1
	local flatTbl = table.create(#tbl)
	local function Scan(t: { any }, d: number)
		for _, v in t do
			if type(v) == "table" and d < maxDepth then
				Scan(v, d + 1)
			else
				table.insert(flatTbl, v)
			end
		end
	end
	Scan(tbl, 0)
	return flatTbl
end

--[=[
	@within TableUtil
	@function FlatMap
	@param tbl table
	@param predicate (key: any, value: any, tbl: table) -> newValue: any
	@return table

	Calls `TableUtil.Map` on the given table and predicate, and then
	calls `TableUtil.Flat` on the result from the map operation.

	```lua
	local t = {10, 20, 30}
	local result = TableUtil.FlatMap(t, function(value)
		return {value, value * 2}
	end)
	print(result) --> {10, 20, 20, 40, 30, 60}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
]=]
local function FlatMap<T, M>(tbl: { T }, callback: (T, number, { T }) -> M): { M }
	return Flat(Map(tbl, callback))
end

--[=[
	@within TableUtil
	@function Keys
	@param tbl table
	@return table

	Returns an array with all the keys in the table.

	```lua
	local t = {A = 10, B = 20, C = 30}
	local keys = TableUtil.Keys(t)
	print(keys) --> {"A", "B", "C"}
	```

	:::caution Ordering
	The ordering of the keys is never guaranteed. If order is imperative, call
	`table.sort` on the resulting `keys` array.
	```lua
	local keys = TableUtil.Keys(t)
	table.sort(keys)
	```
]=]
local function Keys<K, V>(tbl: { [K]: V }): { K }
	local keys = table.create(#tbl)
	for k in tbl do
		table.insert(keys, k)
	end
	return keys
end

--[=[
	@within TableUtil
	@function Values
	@param tbl table
	@return table

	Returns an array with all the values in the table.

	```lua
	local t = {A = 10, B = 20, C = 30}
	local values = TableUtil.Values(t)
	print(values) --> {10, 20, 30}
	```

	:::caution Ordering
	The ordering of the values is never guaranteed. If order is imperative, call
	`table.sort` on the resulting `values` array.
	```lua
	local values = TableUtil.Values(t)
	table.sort(values)
	```
]=]
local function Values<K, V>(tbl: { [K]: V }): { V }
	local values = table.create(#tbl)
	for _, v in tbl do
		table.insert(values, v)
	end
	return values
end

--[=[
	@within TableUtil
	@function Find
	@param tbl table
	@param callback (value: any, index: any, tbl: table) -> boolean
	@return (value: any?, key: any?)

	Performs a linear scan across the table and calls `callback` on
	each item in the array. Returns the value and key of the first
	pair in which the callback returns `true`.

	```lua
	local t = {
		{Name = "Bob", Age = 20};
		{Name = "Jill", Age = 30};
		{Name = "Ann", Age = 25};
	}

	-- Find first person who has a name starting with J:
	local firstPersonWithJ = TableUtil.Find(t, function(person)
		return person.Name:sub(1, 1):lower() == "j"
	end)

	print(firstPersonWithJ) --> {Name = "Jill", Age = 30}
	```

	:::caution Dictionary Ordering
	While `Find` can also be used with dictionaries, dictionary ordering is never
	guaranteed, and thus the result could be different if there are more
	than one possible matches given the data and callback function.
]=]
local function Find<K, V>(tbl: { [K]: V }, callback: (V, K, { [K]: V }) -> boolean): (V?, K?)
	for k, v in tbl do
		if callback(v, k, tbl) then
			return v, k
		end
	end
	return nil, nil
end

--[=[
	@within TableUtil
	@function Every
	@param tbl table
	@param callback (value: any, index: any, tbl: table) -> boolean
	@return boolean

	Returns `true` if the `callback` also returns `true` for _every_
	item in the table.

	```lua
	local t = {10, 20, 40, 50, 60}

	local allAboveZero = TableUtil.Every(t, function(value)
		return value > 0
	end)

	print("All above zero:", allAboveZero) --> All above zero: true
	```
]=]
local function Every<K, V>(tbl: { [K]: V }, callback: (V, K, { [K]: V }) -> boolean): boolean
	for k, v in tbl do
		if not callback(v, k, tbl) then
			return false
		end
	end
	return true
end

--[=[
	@within TableUtil
	@function Some
	@param tbl table
	@param callback (value: any, index: any, tbl: table) -> boolean
	@return boolean

	Returns `true` if the `callback` also returns `true` for _at least
	one_ of the items in the table.

	```lua
	local t = {10, 20, 40, 50, 60}

	local someBelowTwenty = TableUtil.Some(t, function(value)
		return value < 20
	end)

	print("Some below twenty:", someBelowTwenty) --> Some below twenty: true
	```
]=]
local function Some<K, V>(tbl: { [K]: V }, callback: (V, K, { [K]: V }) -> boolean): boolean
	for k, v in tbl do
		if callback(v, k, tbl) then
			return true
		end
	end
	return false
end

--[=[
	@within TableUtil
	@function Truncate
	@param tbl table
	@param length number
	@return table

	Returns a new table truncated to the length of `length`. Any length
	equal or greater than the current length will simply return a
	shallow copy of the table.

	```lua
	local t = {10, 20, 30, 40, 50, 60, 70, 80}
	local tTruncated = TableUtil.Truncate(t, 3)
	print(tTruncated) --> {10, 20, 30}
	```
]=]
local function Truncate<T>(tbl: { T }, len: number): { T }
	local n = #tbl
	len = math.clamp(len, 1, n)
	return if len == n then table.clone(tbl) else table.move(tbl, 1, len, 1, table.create(len))
end

--[=[
	@within TableUtil
	@function Zip
	@param ... table
	@return (iter: (t: table, k: any) -> (key: any?, values: table?), tbl: table, startIndex: any?)

	Returns an iterator that can scan through multiple tables at the same time side-by-side, matching
	against shared keys/indices.

	```lua
	local t1 = {10, 20, 30, 40, 50}
	local t2 = {60, 70, 80, 90, 100}

	for key,values in TableUtil.Zip(t1, t2) do
		print(key, values)
	end

	--[[
		Outputs:
		1 {10, 60}
		2 {20, 70}
		3 {30, 80}
		4 {40, 90}
		5 {50, 100}
	--]]
	```
]=]
local function Zip(...: { [any]: any }): ((t: { any }, k: any) -> (any, any), { any }, any)
	assert(select("#", ...) > 0, "Must supply at least 1 table")
	local function ZipIteratorArray(all: { any }, k: number): (number?, { any }?)
		k += 1
		local values = {}
		for i, t in all do
			local v = t[k]
			if v ~= nil then
				values[i] = v
			else
				return nil, nil
			end
		end
		return k, values
	end
	local function ZipIteratorMap(all: { [any]: any }, k: any): (number?, { any }?)
		local values = {}
		for i, t in all do
			local v = next(t, k)
			if v ~= nil then
				values[i] = v
			else
				return nil, nil
			end
		end
		return k, values
	end
	local all = { ... }
	if #all[1] > 0 then
		return ZipIteratorArray, all, 0
	else
		return ZipIteratorMap, all, nil
	end
end

--[=[
	@within TableUtil
	@function Lock
	@param tbl table
	@return table

	Locks the table using `table.freeze`, as well as any
	nested tables within the given table. This will lock
	the whole deep structure of the table, disallowing any
	further modifications.

	```lua
	local tbl = {xyz = {abc = 32}}
	tbl.xyz.abc = 28 -- Works fine
	TableUtil.Lock(tbl)
	tbl.xyz.abc = 64 -- Will throw an error (cannot modify readonly table)
	```
]=]
local function Lock<T>(tbl: T): T
	local function Freeze(t: { [any]: any })
		for k, v in pairs(t) do
			if type(v) == "table" then
				t[k] = Freeze(v)
			end
		end
		return table.freeze(t)
	end
	return Freeze(tbl :: any)
end

--[=[
	@within TableUtil
	@function IsEmpty
	@param tbl table
	@return boolean

	Returns `true` if the given table is empty. This is
	simply performed by checking if `next(tbl)` is `nil`
	and works for both arrays and dictionaries. This is
	useful when needing to check if a table is empty but
	not knowing if it is an array or dictionary.

	```lua
	TableUtil.IsEmpty({}) -- true
	TableUtil.IsEmpty({"abc"}) -- false
	TableUtil.IsEmpty({abc = 32}) -- false
	```
]=]
local function IsEmpty(tbl: { any }): boolean
	return next(tbl) == nil
end

--[=[
	@within TableUtil
	@function EncodeJSON
	@param value any
	@return string

	Proxy for [`HttpService:JSONEncode`](https://developer.roblox.com/en-us/api-reference/function/HttpService/JSONEncode).
]=]
local function EncodeJSON(value: any): string
	return HttpService:JSONEncode(value)
end

--[=[
	@within TableUtil
	@function DecodeJSON
	@param value any
	@return string

	Proxy for [`HttpService:JSONDecode`](https://developer.roblox.com/en-us/api-reference/function/HttpService/JSONDecode).
]=]
local function DecodeJSON(str: string): any
	return HttpService:JSONDecode(str)
end

TableUtil.Copy = Copy
TableUtil.Sync = Sync
TableUtil.Reconcile = Reconcile
TableUtil.SwapRemove = SwapRemove
TableUtil.SwapRemoveFirstValue = SwapRemoveFirstValue
TableUtil.Map = Map
TableUtil.Filter = Filter
TableUtil.Reduce = Reduce
TableUtil.Assign = Assign
TableUtil.Extend = Extend
TableUtil.Reverse = Reverse
TableUtil.Shuffle = Shuffle
TableUtil.Sample = Sample
TableUtil.Flat = Flat
TableUtil.FlatMap = FlatMap
TableUtil.Keys = Keys
TableUtil.Values = Values
TableUtil.Find = Find
TableUtil.Every = Every
TableUtil.Some = Some
TableUtil.Truncate = Truncate
TableUtil.Zip = Zip
TableUtil.Lock = Lock
TableUtil.IsEmpty = IsEmpty
TableUtil.EncodeJSON = EncodeJSON
TableUtil.DecodeJSON = DecodeJSON

return TableUtil
]]></string>
            </Properties>
            <Item class="ModuleScript" referent="47">
              <Properties>
                <string name="Name">init.spec</string>
                <string name="Source"><![CDATA[return function()
	local TableUtil = require(script.Parent)

	describe("Copy (Deep)", function()
		it("should create a deep table copy", function()
			local tbl = { a = { b = { c = { d = 32 } } } }
			local tblCopy = TableUtil.Copy(tbl, true)
			expect(tbl).never.to.equal(tblCopy)
			expect(tbl.a).never.to.equal(tblCopy.a)
			expect(tblCopy.a.b.c.d).to.equal(tbl.a.b.c.d)
		end)
	end)

	describe("Copy (Shallow)", function()
		it("should create a shallow dictionary copy", function()
			local tbl = { a = { b = { c = { d = 32 } } } }
			local tblCopy = TableUtil.Copy(tbl)
			expect(tblCopy).never.to.equal(tbl)
			expect(tblCopy.a).to.equal(tbl.a)
			expect(tblCopy.a.b.c.d).to.equal(tbl.a.b.c.d)
		end)

		it("should create a shallow array copy", function()
			local tbl = { 10, 20, 30, 40 }
			local tblCopy = TableUtil.Copy(tbl)
			expect(tblCopy).never.to.equal(tbl)
			for i, v in ipairs(tbl) do
				expect(tblCopy[i]).to.equal(v)
			end
		end)
	end)

	describe("Sync", function()
		it("should sync tables", function()
			local template = { a = 32, b = 64, c = 128, e = { h = 1 } }
			local tblSrc = { a = 32, b = 10, d = 1, e = { h = 2, n = 2 }, f = { x = 10 } }
			local tbl = TableUtil.Sync(tblSrc, template)
			expect(tbl.a).to.equal(template.a)
			expect(tbl.b).to.equal(10)
			expect(tbl.c).to.equal(template.c)
			expect(tbl.d).never.to.be.ok()
			expect(tbl.e.h).to.equal(2)
			expect(tbl.e.n).never.to.be.ok()
			expect(tbl.f).never.to.be.ok()
		end)
	end)

	describe("Reconcile", function()
		it("should reconcile table", function()
			local template = { kills = 0, deaths = 0, xp = 10, stuff = {}, stuff2 = "abc", stuff3 = { "data" } }
			local data =
				{ kills = 10, deaths = 4, stuff = { "abc", "xyz" }, extra = 5, stuff2 = { abc = 10 }, stuff3 = true }
			local reconciled = TableUtil.Reconcile(data, template)
			expect(reconciled).never.to.equal(data)
			expect(reconciled).never.to.equal(template)
			expect(reconciled.kills).to.equal(10)
			expect(reconciled.deaths).to.equal(4)
			expect(reconciled.xp).to.equal(10)
			expect(reconciled.stuff[1]).to.equal("abc")
			expect(reconciled.stuff[2]).to.equal("xyz")
			expect(reconciled.extra).to.equal(5)
			expect(type(reconciled.stuff2)).to.equal("table")
			expect(reconciled.stuff2).never.to.equal(data.stuff2)
			expect(reconciled.stuff2.abc).to.equal(10)
			expect(type(reconciled.stuff3)).to.equal("boolean")
			expect(reconciled.stuff3).to.equal(true)
		end)
	end)

	describe("SwapRemove", function()
		it("should swap remove index", function()
			local tbl = { 1, 2, 3, 4, 5 }
			TableUtil.SwapRemove(tbl, 3)
			expect(#tbl).to.equal(4)
			expect(tbl[3]).to.equal(5)
		end)
	end)

	describe("SwapRemoveFirstValue", function()
		it("should swap remove first value given", function()
			local tbl = { "hello", "world", "goodbye", "planet" }
			TableUtil.SwapRemoveFirstValue(tbl, "world")
			expect(#tbl).to.equal(3)
			expect(tbl[2]).to.equal("planet")
		end)
	end)

	describe("Map", function()
		it("should map table", function()
			local tbl = {
				{ FirstName = "John", LastName = "Doe" },
				{ FirstName = "Jane", LastName = "Smith" },
			}
			local tblMapped = TableUtil.Map(tbl, function(person)
				return person.FirstName .. " " .. person.LastName
			end)
			expect(tblMapped[1]).to.equal("John Doe")
			expect(tblMapped[2]).to.equal("Jane Smith")
		end)
	end)

	describe("Filter", function()
		it("should filter table", function()
			local tbl = { 10, 20, 30, 40, 50, 60, 70, 80, 90 }
			local tblFiltered = TableUtil.Filter(tbl, function(n)
				return (n >= 30 and n <= 60)
			end)
			expect(#tblFiltered).to.equal(4)
			expect(tblFiltered[1]).to.equal(30)
			expect(tblFiltered[#tblFiltered]).to.equal(60)
		end)
	end)

	describe("Reduce", function()
		it("should reduce table with numbers", function()
			local tbl = { 1, 2, 3, 4, 5 }
			local reduced = TableUtil.Reduce(tbl, function(accum, value)
				return accum + value
			end)
			expect(reduced).to.equal(15)
		end)

		it("should reduce table", function()
			local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } }
			local reduced = TableUtil.Reduce(tbl, function(accum, value)
				return accum + value.Score
			end, 0)
			expect(reduced).to.equal(60)
		end)

		it("should reduce table with initial value", function()
			local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } }
			local reduced = TableUtil.Reduce(tbl, function(accum, value)
				return accum + value.Score
			end, 40)
			expect(reduced).to.equal(100)
		end)

		it("should reduce functions", function()
			local function Square(x)
				return x * x
			end
			local function Double(x)
				return x * 2
			end
			local Func = TableUtil.Reduce({ Square, Double }, function(a, b)
				return function(x)
					return a(b(x))
				end
			end)
			local result = Func(10)
			expect(result).to.equal(400)
		end)
	end)

	describe("Assign", function()
		it("should assign tables", function()
			local target = { a = 32, x = 100 }
			local t1 = { b = 64, c = 128 }
			local t2 = { a = 10, c = 100, d = 200 }
			local tbl = TableUtil.Assign(target, t1, t2)
			expect(tbl.a).to.equal(10)
			expect(tbl.b).to.equal(64)
			expect(tbl.c).to.equal(100)
			expect(tbl.d).to.equal(200)
			expect(tbl.x).to.equal(100)
		end)
	end)

	describe("Extend", function()
		it("should extend tables", function()
			local tbl = { "a", "b", "c" }
			local extension = { "d", "e", "f" }
			local extended = TableUtil.Extend(tbl, extension)
			expect(table.concat(extended)).to.equal("abcdef")
		end)
	end)

	describe("Reverse", function()
		it("should create a table in reverse", function()
			local tbl = { 1, 2, 3 }
			local tblRev = TableUtil.Reverse(tbl)
			expect(table.concat(tblRev)).to.equal("321")
		end)
	end)

	describe("Shuffle", function()
		it("should shuffle a table", function()
			local tbl = { 1, 2, 3, 4, 5 }
			expect(function()
				TableUtil.Shuffle(tbl)
			end).never.to.throw()
		end)
	end)

	describe("Sample", function()
		it("should sample a table", function()
			local tbl = { 1, 2, 3, 4, 5 }
			local sample = TableUtil.Sample(tbl, 3)
			expect(#sample).to.equal(3)
		end)
	end)

	describe("Flat", function()
		it("should flatten table", function()
			local tbl = { 1, 2, 3, { 4, 5, { 6, 7 } } }
			local tblFlat = TableUtil.Flat(tbl, 3)
			expect(table.concat(tblFlat)).to.equal("1234567")
		end)
	end)

	describe("FlatMap", function()
		it("should map and flatten table", function()
			local tbl = { 1, 2, 3, 4, 5, 6, 7 }
			local tblFlat = TableUtil.FlatMap(tbl, function(n)
				return { n, n * 2 }
			end)
			expect(table.concat(tblFlat)).to.equal("12243648510612714")
		end)
	end)

	describe("Keys", function()
		it("should give all keys of table", function()
			local tbl = { a = 1, b = 2, c = 3 }
			local keys = TableUtil.Keys(tbl)
			expect(#keys).to.equal(3)
			expect(table.find(keys, "a")).to.be.ok()
			expect(table.find(keys, "b")).to.be.ok()
			expect(table.find(keys, "c")).to.be.ok()
		end)
	end)

	describe("Values", function()
		it("should give all values of table", function()
			local tbl = { a = 1, b = 2, c = 3 }
			local values = TableUtil.Values(tbl)
			expect(#values).to.equal(3)
			expect(table.find(values, 1)).to.be.ok()
			expect(table.find(values, 2)).to.be.ok()
			expect(table.find(values, 3)).to.be.ok()
		end)
	end)

	describe("Find", function()
		it("should find item in array", function()
			local tbl = { 10, 20, 30 }
			local item, index = TableUtil.Find(tbl, function(value)
				return (value == 20)
			end)
			expect(item).to.be.ok()
			expect(index).to.equal(2)
			expect(item).to.equal(20)
		end)

		it("should find item in dictionary", function()
			local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } }
			local item, index = TableUtil.Find(tbl, function(value)
				return (value.Score == 20)
			end)
			expect(item).to.be.ok()
			expect(index).to.equal(2)
			expect(item.Score).to.equal(20)
		end)
	end)

	describe("Every", function()
		it("should see every value is above 20", function()
			local tbl = { 21, 40, 200 }
			local every = TableUtil.Every(tbl, function(n)
				return (n > 20)
			end)
			expect(every).to.equal(true)
		end)

		it("should see every value is not above 20", function()
			local tbl = { 20, 40, 200 }
			local every = TableUtil.Every(tbl, function(n)
				return (n > 20)
			end)
			expect(every).never.to.equal(true)
		end)
	end)

	describe("Some", function()
		it("should see some value is above 20", function()
			local tbl = { 5, 40, 1 }
			local every = TableUtil.Some(tbl, function(n)
				return (n > 20)
			end)
			expect(every).to.equal(true)
		end)

		it("should see some value is not above 20", function()
			local tbl = { 5, 15, 1 }
			local every = TableUtil.Some(tbl, function(n)
				return (n > 20)
			end)
			expect(every).never.to.equal(true)
		end)
	end)

	describe("Truncate", function()
		it("should truncate an array", function()
			local t1 = { 1, 2, 3, 4, 5 }
			local t2 = TableUtil.Truncate(t1, 3)
			expect(#t2).to.equal(3)
			expect(t2[1]).to.equal(t1[1])
			expect(t2[2]).to.equal(t1[2])
			expect(t2[3]).to.equal(t1[3])
		end)

		it("should truncate an array with out of bounds sizes", function()
			local t1 = { 1, 2, 3, 4, 5 }
			expect(function()
				TableUtil.Truncate(t1, -1)
			end).to.never.throw()
			expect(function()
				TableUtil.Truncate(t1, #t1 + 1)
			end).to.never.throw()
			local t2 = TableUtil.Truncate(t1, #t1 + 10)
			expect(#t2).to.equal(#t1)
			expect(t2).to.never.equal(t1)
		end)
	end)

	describe("Lock", function()
		it("should lock a table", function()
			local t = { abc = { xyz = { num = 32 } } }
			expect(function()
				t.abc.xyz.num = 64
			end).never.to.throw()
			local t2 = TableUtil.Lock(t)
			expect(t.abc.xyz.num).to.equal(64)
			expect(t).to.equal(t2)
			expect(function()
				t.abc.xyz.num = 10
			end).to.throw()
		end)
	end)

	describe("Zip", function()
		it("should zip arrays together", function()
			local t1 = { 1, 2, 3, 4, 5 }
			local t2 = { 9, 8, 7, 6, 5 }
			local t3 = { 1, 1, 1, 1, 1 }
			local lastIndex = 0
			for i, v in TableUtil.Zip(t1, t2, t3) do
				lastIndex = i
				expect(v[1]).to.equal(t1[i])
				expect(v[2]).to.equal(t2[i])
				expect(v[3]).to.equal(t3[i])
			end
			expect(lastIndex).to.equal(math.min(#t1, #t2, #t3))
		end)

		it("should zip arrays of different lengths together", function()
			local t1 = { 1, 2, 3, 4, 5 }
			local t2 = { 9, 8, 7, 6 }
			local t3 = { 1, 1, 1 }
			local lastIndex = 0
			for i, v in TableUtil.Zip(t1, t2, t3) do
				lastIndex = i
				expect(v[1]).to.equal(t1[i])
				expect(v[2]).to.equal(t2[i])
				expect(v[3]).to.equal(t3[i])
			end
			expect(lastIndex).to.equal(math.min(#t1, #t2, #t3))
		end)

		it("should zip maps together", function()
			local t1 = { a = 10, b = 20, c = 30 }
			local t2 = { a = 100, b = 200, c = 300 }
			local t3 = { a = 3000, b = 2000, c = 3000 }
			for k, v in TableUtil.Zip(t1, t2, t3) do
				expect(v[1]).to.equal(t1[k])
				expect(v[2]).to.equal(t2[k])
				expect(v[3]).to.equal(t3[k])
			end
		end)

		it("should zip maps of different keys together", function()
			local t1 = { a = 10, b = 20, c = 30, d = 40 }
			local t2 = { a = 100, b = 200, c = 300, z = 10 }
			local t3 = { a = 3000, b = 2000, c = 3000, x = 0 }
			for k, v in TableUtil.Zip(t1, t2, t3) do
				expect(v[1]).to.equal(t1[k])
				expect(v[2]).to.equal(t2[k])
				expect(v[3]).to.equal(t3[k])
			end
		end)
	end)

	describe("IsEmpty", function()
		it("should detect that table is empty", function()
			local tbl = {}
			local isEmpty = TableUtil.IsEmpty(tbl)
			expect(isEmpty).to.equal(true)
		end)

		it("should detect that array is not empty", function()
			local tbl = { 10, 20, 30 }
			local isEmpty = TableUtil.IsEmpty(tbl)
			expect(isEmpty).to.equal(false)
		end)

		it("should detect that dictionary is not empty", function()
			local tbl = { a = 10, b = 20, c = 30 }
			local isEmpty = TableUtil.IsEmpty(tbl)
			expect(isEmpty).to.equal(false)
		end)
	end)

	describe("JSON", function()
		it("should encode json", function()
			local tbl = { hello = "world" }
			local json = TableUtil.EncodeJSON(tbl)
			expect(json).to.equal('{"hello":"world"}')
		end)

		it("should decode json", function()
			local json = '{"hello":"world"}'
			local tbl = TableUtil.DecodeJSON(json)
			expect(tbl).to.be.a("table")
			expect(tbl.hello).to.equal("world")
		end)
	end)
end
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="48">
              <Properties>
                <string name="Name">wally</string>
                <string name="Source">return {
	package = {
		authors = {"Stephen Leitnick"},
		description = "Table utility functions",
		license = "MIT",
		name = "sleitnick/table-util",
		realm = "shared",
		registry = "https://github.com/UpliftGames/wally-index",
		version = "1.2.1",
	},
}</string>
              </Properties>
            </Item>
          </Item>
        </Item>
      </Item>
    </Item>
    <Item class="Folder" referent="49">
      <Properties>
        <string name="Name">Shared</string>
      </Properties>
      <Item class="Folder" referent="50">
        <Properties>
          <string name="Name">Constants</string>
        </Properties>
        <Item class="ModuleScript" referent="51">
          <Properties>
            <string name="Name">Moves</string>
            <string name="Source"><![CDATA[--[[
	Moves.lua
	All combat move definitions with frame data.
]]

local Moves = {}

-- Move categories
Moves.Category = {
	MELEE = "Melee",
	KI_BLAST = "KiBlast",
	BEAM = "Beam",
	SIGNATURE = "Signature",
	ULTIMATE = "Ultimate",
	EVASIVE = "Evasive",
}

-- Loadout slot layout: 4 Signature + 1 Ultimate + 1 Evasive
Moves.LOADOUT_SLOTS = 6

Moves.List = {
	-- =====================
	-- BASIC ATTACKS (everyone has these)
	-- =====================
	LightCombo = {
		id = "LightCombo",
		displayName = "Light Combo",
		category = "Melee",
		description = "A quick 4-hit combo ending with a knockback.",
		unlockDefault = true,
		kiCost = 0,
		staminaCost = 0,
		cooldown = 0,
		hits = {
			{ frame = 0, damageMultiplier = 0.5, knockback = Vector3.new(0, 0, 3), hitstun = 0.2 },
			{ frame = 6, damageMultiplier = 0.5, knockback = Vector3.new(0, 0, 3), hitstun = 0.2 },
			{ frame = 12, damageMultiplier = 0.7, knockback = Vector3.new(0, 0, 5), hitstun = 0.3 },
			{ frame = 20, damageMultiplier = 1.0, knockback = Vector3.new(0, 5, 10), hitstun = 0.5, launcher = true },
		},
		range = 6,
	},

	HeavyAttack = {
		id = "HeavyAttack",
		displayName = "Heavy Attack",
		category = "Melee",
		description = "A slow but powerful strike that breaks guards.",
		unlockDefault = true,
		kiCost = 0,
		staminaCost = 5,
		cooldown = 1.0,
		hits = {
			{ frame = 12, damageMultiplier = 2.0, knockback = Vector3.new(0, 8, 15), hitstun = 0.8, guardBreak = true },
		},
		range = 7,
	},

	KiBlast = {
		id = "KiBlast",
		displayName = "Ki Blast",
		category = "KiBlast",
		description = "A basic energy projectile.",
		unlockDefault = true,
		kiCost = 5,
		staminaCost = 0,
		cooldown = 0.5,
		projectileSpeed = 100,
		damageMultiplier = 0.8,
		range = 200,
	},

	ChargedKiBlast = {
		id = "ChargedKiBlast",
		displayName = "Charged Ki Blast",
		category = "KiBlast",
		description = "A powerful energy blast. Damage scales with charge time.",
		unlockDefault = true,
		kiCostBase = 10,
		kiCostPerSecond = 5,
		staminaCost = 0,
		cooldown = 1.0,
		maxChargeTime = 3.0,
		projectileSpeed = 80,
		damageMultiplierBase = 1.0,
		damageMultiplierMax = 4.0,
		range = 200,
	},

	-- =====================
	-- SIGNATURE MOVES
	-- =====================
	Kamehameha = {
		id = "Kamehameha",
		displayName = "Kamehameha",
		category = "Beam",
		description = "The legendary turtle school beam attack.",
		unlockDefault = false,
		races = { "Saiyan", "Human" },
		zeniCost = 5000,
		levelRequirement = 20,
		kiCost = 30,
		staminaCost = 0,
		cooldown = 5.0,
		chargeTime = 1.0,
		damageMultiplier = 3.0,
		damagePerTick = 0.5,
		duration = 3.0,
		beamWidth = 4,
		range = 300,
		canBeamStruggle = true,
	},

	FinalFlash = {
		id = "FinalFlash",
		displayName = "Final Flash",
		category = "Beam",
		description = "A devastating beam charged with both hands.",
		unlockDefault = false,
		races = { "Saiyan" },
		zeniCost = 8000,
		levelRequirement = 50,
		kiCost = 40,
		staminaCost = 0,
		cooldown = 7.0,
		chargeTime = 1.5,
		damageMultiplier = 4.0,
		damagePerTick = 0.6,
		duration = 2.5,
		beamWidth = 5,
		range = 300,
		canBeamStruggle = true,
	},

	SpecialBeamCannon = {
		id = "SpecialBeamCannon",
		displayName = "Special Beam Cannon",
		category = "Beam",
		description = "A piercing beam that drills through defenses.",
		unlockDefault = false,
		races = { "Namekian" },
		zeniCost = 6000,
		levelRequirement = 30,
		kiCost = 35,
		staminaCost = 0,
		cooldown = 6.0,
		chargeTime = 2.0,
		damageMultiplier = 4.5,
		damagePerTick = 0,
		duration = 0.5,
		beamWidth = 2,
		range = 300,
		piercing = true,
		canBeamStruggle = false,
	},

	DeathBeam = {
		id = "DeathBeam",
		displayName = "Death Beam",
		category = "Signature",
		description = "A precise, fast beam from the fingertip.",
		unlockDefault = false,
		races = { "FriezaRace" },
		zeniCost = 4000,
		levelRequirement = 25,
		kiCost = 15,
		staminaCost = 0,
		cooldown = 2.0,
		damageMultiplier = 2.0,
		projectileSpeed = 200,
		range = 250,
	},

	GalickGun = {
		id = "GalickGun",
		displayName = "Galick Gun",
		category = "Beam",
		description = "A powerful beam attack rivaling the Kamehameha.",
		unlockDefault = false,
		races = { "Saiyan" },
		zeniCost = 5000,
		levelRequirement = 25,
		kiCost = 30,
		staminaCost = 0,
		cooldown = 5.0,
		chargeTime = 1.0,
		damageMultiplier = 3.0,
		damagePerTick = 0.5,
		duration = 3.0,
		beamWidth = 4,
		range = 300,
		canBeamStruggle = true,
	},

	Masenko = {
		id = "Masenko",
		displayName = "Masenko",
		category = "Beam",
		description = "A quick overhead beam attack.",
		unlockDefault = false,
		races = { "Saiyan", "Namekian", "Human" },
		zeniCost = 3000,
		levelRequirement = 15,
		kiCost = 20,
		staminaCost = 0,
		cooldown = 4.0,
		chargeTime = 0.5,
		damageMultiplier = 2.0,
		damagePerTick = 0.3,
		duration = 2.0,
		beamWidth = 3,
		range = 250,
		canBeamStruggle = true,
	},

	VanishingBall = {
		id = "VanishingBall",
		displayName = "Vanishing Ball",
		category = "Signature",
		description = "A condensed energy sphere that explodes on impact.",
		unlockDefault = false,
		races = { "Majin" },
		zeniCost = 5000,
		levelRequirement = 30,
		kiCost = 25,
		staminaCost = 0,
		cooldown = 5.0,
		damageMultiplier = 3.5,
		projectileSpeed = 90,
		explosionRadius = 15,
		range = 200,
	},

	-- =====================
	-- ULTIMATE MOVES
	-- =====================
	SpiritBomb = {
		id = "SpiritBomb",
		displayName = "Spirit Bomb",
		category = "Ultimate",
		description = "Gather energy from all living things into a massive sphere.",
		unlockDefault = false,
		races = { "Saiyan", "Human" },
		zeniCost = 50000,
		levelRequirement = 100,
		kiCost = 80,
		staminaCost = 20,
		cooldown = 30.0,
		chargeTime = 5.0,
		damageMultiplier = 10.0,
		explosionRadius = 30,
		range = 100,
	},

	DeathBall = {
		id = "DeathBall",
		displayName = "Death Ball",
		category = "Ultimate",
		description = "A massive sphere of destructive energy.",
		unlockDefault = false,
		races = { "FriezaRace" },
		zeniCost = 40000,
		levelRequirement = 80,
		kiCost = 70,
		staminaCost = 15,
		cooldown = 25.0,
		chargeTime = 3.0,
		damageMultiplier = 8.0,
		explosionRadius = 25,
		range = 150,
	},

	HumanExtinctionAttack = {
		id = "HumanExtinctionAttack",
		displayName = "Human Extinction Attack",
		category = "Ultimate",
		description = "Unleash countless ki blasts that rain down on all enemies.",
		unlockDefault = false,
		races = { "Majin" },
		zeniCost = 45000,
		levelRequirement = 90,
		kiCost = 75,
		staminaCost = 20,
		cooldown = 28.0,
		damageMultiplier = 7.0,
		aoeRadius = 50,
		range = 300,
	},

	-- =====================
	-- EVASIVE MOVES
	-- =====================
	Vanish = {
		id = "Vanish",
		displayName = "Vanish",
		category = "Evasive",
		description = "Teleport behind the attacker for a counter opportunity.",
		unlockDefault = true,
		kiCost = 0,
		staminaCost = 25,
		cooldown = 2.0,
		iFrames = 0.3,
		teleportBehind = true,
	},

	AfterImage = {
		id = "AfterImage",
		displayName = "After Image",
		category = "Evasive",
		description = "Leave an after-image and reappear nearby, confusing the opponent.",
		unlockDefault = false,
		races = { "Saiyan", "Human", "FriezaRace" },
		zeniCost = 10000,
		levelRequirement = 50,
		kiCost = 10,
		staminaCost = 15,
		cooldown = 3.0,
		iFrames = 0.5,
		teleportDistance = 20,
	},

	InstantTransmission = {
		id = "InstantTransmission",
		displayName = "Instant Transmission",
		category = "Evasive",
		description = "Teleport to any targeted location instantly.",
		unlockDefault = false,
		races = { "Saiyan" },
		zeniCost = 30000,
		levelRequirement = 100,
		kiCost = 20,
		staminaCost = 10,
		cooldown = 5.0,
		iFrames = 0.2,
		teleportToTarget = true,
		maxRange = 200,
	},
}

function Moves:Get(moveId: string)
	return self.List[moveId]
end

function Moves:GetDefaultMoves(): { string }
	local defaults = {}
	for id, move in pairs(self.List) do
		if move.unlockDefault then
			table.insert(defaults, id)
		end
	end
	return defaults
end

function Moves:GetMovesForRace(raceName: string): { string }
	local moves = {}
	for id, move in pairs(self.List) do
		if move.unlockDefault then
			table.insert(moves, id)
		elseif move.races then
			for _, race in ipairs(move.races) do
				if race == raceName then
					table.insert(moves, id)
					break
				end
			end
		end
	end
	return moves
end

return Moves
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="52">
          <Properties>
            <string name="Name">Races</string>
            <string name="Source"><![CDATA[--[[
	Races.lua
	Defines all playable races, their base stats, passives, and transformation trees.
]]

local Races = {}

Races.List = {
	Saiyan = {
		displayName = "Saiyan",
		description = "A warrior race with unrivaled combat potential. Grows stronger after each defeat.",
		passive = {
			id = "ZenkaiBoost",
			name = "Zenkai Boost",
			description = "+15% stat recovery after being KO'd. Stacks up to 3 times per life.",
			maxStacks = 3,
			boostPercent = 0.15,
		},
		baseStats = {
			Melee = 10,
			KiPower = 10,
			Defense = 8,
			Speed = 9,
			Health = 100,
			Stamina = 100,
		},
		transformationTree = {
			"Kaioken",
			"SSJ",
			"SSJ2",
			"SSJ3",
			"SSG",
			"SSB",
			"SSBE",
			"UltraInstinct",
		},
	},

	Namekian = {
		displayName = "Namekian",
		description = "A wise race from Planet Namek. Possesses regenerative abilities.",
		passive = {
			id = "Regeneration",
			name = "Regeneration",
			description = "Passively regenerate 1% HP per second when not in combat for 5 seconds.",
			regenPercent = 0.01,
			outOfCombatDelay = 5,
		},
		baseStats = {
			Melee = 8,
			KiPower = 9,
			Defense = 12,
			Speed = 7,
			Health = 120,
			Stamina = 100,
		},
		transformationTree = {
			"GiantForm",
			"OrangeNamekian",
		},
	},

	FriezaRace = {
		displayName = "Frieza Race",
		description = "A tyrannical race that suppresses its true power in lower forms.",
		passive = {
			id = "Suppression",
			name = "Power Suppression",
			description = "Lower forms cost no ki to maintain. Final+ forms have reduced ki drain.",
			kiDrainReduction = 0.25,
		},
		baseStats = {
			Melee = 9,
			KiPower = 11,
			Defense = 9,
			Speed = 10,
			Health = 95,
			Stamina = 90,
		},
		transformationTree = {
			"SecondForm",
			"ThirdForm",
			"FinalForm",
			"GoldenForm",
			"BlackFrieza",
		},
	},

	Majin = {
		displayName = "Majin",
		description = "An ancient magical race with elastic bodies and absorption powers.",
		passive = {
			id = "Absorption",
			name = "Absorption",
			description = "Defeating an enemy restores 10% HP and 5% Ki.",
			hpRestore = 0.10,
			kiRestore = 0.05,
		},
		baseStats = {
			Melee = 11,
			KiPower = 8,
			Defense = 10,
			Speed = 7,
			Health = 130,
			Stamina = 90,
		},
		transformationTree = {
			"EvilBuu",
			"SuperBuu",
			"KidBuu",
			"PureForm",
		},
	},

	Android = {
		displayName = "Android",
		description = "Artificial warriors with infinite energy reactors.",
		passive = {
			id = "InfiniteEnergy",
			name = "Infinite Energy",
			description = "Ki regenerates passively at 2% per second. Transformations cost 50% less ki.",
			kiRegenPercent = 0.02,
			transformKiReduction = 0.50,
		},
		baseStats = {
			Melee = 10,
			KiPower = 10,
			Defense = 10,
			Speed = 8,
			Health = 100,
			Stamina = 110,
		},
		transformationTree = {
			"SemiPerfect",
			"Perfect",
			"SuperPerfect",
		},
	},

	Human = {
		displayName = "Human",
		description = "Versatile fighters from Earth with limitless potential through training.",
		passive = {
			id = "Adaptability",
			name = "Adaptability",
			description = "+20% XP gain from all sources. +10% stat point effectiveness.",
			xpBonus = 0.20,
			statPointBonus = 0.10,
		},
		baseStats = {
			Melee = 9,
			KiPower = 9,
			Defense = 9,
			Speed = 9,
			Health = 100,
			Stamina = 100,
		},
		transformationTree = {
			"Kaioken",
			"PotentialUnleashed",
			"MaxPower",
		},
	},
}

-- Quick lookup
Races.Names = {}
for name in pairs(Races.List) do
	table.insert(Races.Names, name)
end
table.sort(Races.Names)

function Races:Get(raceName: string)
	return self.List[raceName]
end

function Races:IsValid(raceName: string): boolean
	return self.List[raceName] ~= nil
end

return Races
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="53">
          <Properties>
            <string name="Name">Sagas</string>
            <string name="Source"><![CDATA[--[[
	Sagas.lua
	Story saga and quest chain definitions.
]]

local Sagas = {}

Sagas.List = {
	-- =====================
	-- DRAGON BALL Z SAGAS
	-- =====================
	{
		id = "SaiyanSaga",
		displayName = "Saiyan Saga",
		description = "The Saiyans are coming to Earth! Prepare for the ultimate battle.",
		levelRequirement = 1,
		zone = "WestCity",
		quests = {
			{
				id = "saiyan_1",
				title = "Training Begins",
				description = "Defeat 5 training dummies in West City.",
				objectives = { { type = "defeat_npc", npcId = "TrainingDummy", count = 5 } },
				rewards = { xp = 200, zeni = 100 },
			},
			{
				id = "saiyan_2",
				title = "Saibaman Attack",
				description = "Defeat the Saibamen threatening the city.",
				objectives = { { type = "defeat_npc", npcId = "Saibaman", count = 6 } },
				rewards = { xp = 500, zeni = 300 },
			},
			{
				id = "saiyan_3",
				title = "Nappa's Assault",
				description = "Defeat Nappa before he destroys everything.",
				objectives = { { type = "defeat_npc", npcId = "Nappa", count = 1 } },
				rewards = { xp = 1000, zeni = 500, moves = { "Masenko" } },
			},
			{
				id = "saiyan_4",
				title = "Vegeta: The Prince of Saiyans",
				description = "Face Vegeta in an epic showdown.",
				objectives = { { type = "defeat_npc", npcId = "Vegeta_Saiyan", count = 1 } },
				rewards = { xp = 2000, zeni = 1000 },
				isBossFight = true,
			},
		},
	},

	{
		id = "FriezaSaga",
		displayName = "Frieza Saga",
		description = "Travel to Planet Namek to face the galactic tyrant Frieza.",
		levelRequirement = 30,
		zone = "Namek",
		quests = {
			{
				id = "frieza_1",
				title = "Arrival on Namek",
				description = "Defeat Frieza's soldiers on Planet Namek.",
				objectives = { { type = "defeat_npc", npcId = "FriezaSoldier", count = 10 } },
				rewards = { xp = 1500, zeni = 800 },
			},
			{
				id = "frieza_2",
				title = "The Ginyu Force",
				description = "Defeat all members of the Ginyu Force.",
				objectives = {
					{ type = "defeat_npc", npcId = "Guldo", count = 1 },
					{ type = "defeat_npc", npcId = "Recoome", count = 1 },
					{ type = "defeat_npc", npcId = "Burter", count = 1 },
					{ type = "defeat_npc", npcId = "Jeice", count = 1 },
					{ type = "defeat_npc", npcId = "CaptainGinyu", count = 1 },
				},
				rewards = { xp = 5000, zeni = 3000 },
			},
			{
				id = "frieza_3",
				title = "Frieza: Final Form",
				description = "Defeat Frieza in his final form. Unlock the legendary transformation!",
				objectives = { { type = "defeat_npc", npcId = "Frieza_Final", count = 1 } },
				rewards = { xp = 10000, zeni = 5000, transformations = { "SSJ" } },
				isBossFight = true,
			},
		},
	},

	{
		id = "CellSaga",
		displayName = "Cell Saga",
		description = "The bio-android Cell seeks perfection. Stop him before it's too late.",
		levelRequirement = 60,
		zone = "WastelandArena",
		quests = {
			{
				id = "cell_1",
				title = "Cell Juniors",
				description = "Eliminate the Cell Juniors terrorizing the arena.",
				objectives = { { type = "defeat_npc", npcId = "CellJunior", count = 7 } },
				rewards = { xp = 5000, zeni = 2000 },
			},
			{
				id = "cell_2",
				title = "Perfect Cell",
				description = "Challenge Perfect Cell at the Cell Games arena.",
				objectives = { { type = "defeat_npc", npcId = "PerfectCell", count = 1 } },
				rewards = { xp = 15000, zeni = 8000, transformations = { "SSJ2" }, moves = { "Kamehameha" } },
				isBossFight = true,
			},
		},
	},

	{
		id = "BuuSaga",
		displayName = "Buu Saga",
		description = "The ancient terror Majin Buu has been unleashed upon the world.",
		levelRequirement = 100,
		zone = "KamisLookout",
		quests = {
			{
				id = "buu_1",
				title = "Babidi's Minions",
				description = "Defeat Babidi's magical warriors.",
				objectives = {
					{ type = "defeat_npc", npcId = "Dabura", count = 1 },
					{ type = "defeat_npc", npcId = "Pui_Pui", count = 1 },
					{ type = "defeat_npc", npcId = "Yakon", count = 1 },
				},
				rewards = { xp = 8000, zeni = 4000 },
			},
			{
				id = "buu_2",
				title = "Majin Buu Awakens",
				description = "Defeat Majin Buu in all his forms.",
				objectives = { { type = "defeat_npc", npcId = "KidBuu_Boss", count = 1 } },
				rewards = { xp = 25000, zeni = 15000, transformations = { "SSJ3" }, moves = { "SpiritBomb" } },
				isBossFight = true,
			},
		},
	},

	-- =====================
	-- DRAGON BALL SUPER SAGAS
	-- =====================
	{
		id = "BattleOfGods",
		displayName = "Battle of Gods",
		description = "The God of Destruction Beerus has awakened. Can you match a god?",
		levelRequirement = 150,
		zone = "BeerusPlanet",
		quests = {
			{
				id = "bog_1",
				title = "Beerus's Test",
				description = "Survive against Whis in a sparring match for 60 seconds.",
				objectives = { { type = "survive_timer", duration = 60, npcId = "Whis" } },
				rewards = { xp = 15000, zeni = 10000 },
			},
			{
				id = "bog_2",
				title = "Battle of Gods",
				description = "Challenge Beerus, the God of Destruction.",
				objectives = { { type = "defeat_npc", npcId = "Beerus", count = 1 } },
				rewards = { xp = 30000, zeni = 20000, transformations = { "SSG" } },
				isBossFight = true,
			},
		},
	},

	{
		id = "ResurrectionF",
		displayName = "Resurrection F",
		description = "Frieza has returned, stronger than ever with a new golden form.",
		levelRequirement = 200,
		zone = "WestCity",
		quests = {
			{
				id = "rf_1",
				title = "Frieza's Army Returns",
				description = "Defeat the revived Frieza Force.",
				objectives = { { type = "defeat_npc", npcId = "FriezaSoldierElite", count = 20 } },
				rewards = { xp = 20000, zeni = 12000 },
			},
			{
				id = "rf_2",
				title = "Golden Frieza",
				description = "Defeat Golden Frieza and prove the power of Super Saiyan Blue.",
				objectives = { { type = "defeat_npc", npcId = "GoldenFrieza_Boss", count = 1 } },
				rewards = { xp = 40000, zeni = 25000, transformations = { "SSB" } },
				isBossFight = true,
			},
		},
	},

	{
		id = "Universe6Tournament",
		displayName = "Universe 6 Tournament",
		description = "Compete in the tournament between Universe 6 and Universe 7.",
		levelRequirement = 250,
		zone = "Space",
		quests = {
			{
				id = "u6_1",
				title = "Universe 6 Fighters",
				description = "Defeat the Universe 6 team.",
				objectives = {
					{ type = "defeat_npc", npcId = "Botamo", count = 1 },
					{ type = "defeat_npc", npcId = "Frost", count = 1 },
					{ type = "defeat_npc", npcId = "Magetta", count = 1 },
					{ type = "defeat_npc", npcId = "Cabba", count = 1 },
					{ type = "defeat_npc", npcId = "Hit", count = 1 },
				},
				rewards = { xp = 50000, zeni = 30000, moves = { "FinalFlash" } },
				isBossFight = true,
			},
		},
	},

	{
		id = "TournamentOfPower",
		displayName = "Tournament of Power",
		description = "Fight for the survival of Universe 7 in the ultimate battle royale.",
		levelRequirement = 350,
		zone = "TournamentOfPower",
		quests = {
			{
				id = "top_1",
				title = "Survival Match",
				description = "Defeat 30 fighters from other universes.",
				objectives = { { type = "defeat_npc", npcId = "UniverseFighter", count = 30 } },
				rewards = { xp = 40000, zeni = 20000 },
			},
			{
				id = "top_2",
				title = "Jiren the Gray",
				description = "Face the strongest mortal in all universes.",
				objectives = { { type = "defeat_npc", npcId = "Jiren", count = 1 } },
				rewards = { xp = 100000, zeni = 50000, transformations = { "UltraInstinct" } },
				isBossFight = true,
			},
		},
	},

	{
		id = "BrolySaga",
		displayName = "Broly Saga",
		description = "A legendary Saiyan warrior with uncontrollable power appears.",
		levelRequirement = 400,
		zone = "Space",
		quests = {
			{
				id = "broly_1",
				title = "The Legendary Super Saiyan",
				description = "Defeat Broly in his full-power form.",
				objectives = { { type = "defeat_npc", npcId = "Broly_FullPower", count = 1 } },
				rewards = { xp = 120000, zeni = 60000, transformations = { "SSBE" } },
				isBossFight = true,
			},
		},
	},
}

function Sagas:GetById(sagaId: string)
	for _, saga in ipairs(self.List) do
		if saga.id == sagaId then
			return saga
		end
	end
	return nil
end

function Sagas:GetQuestById(questId: string)
	for _, saga in ipairs(self.List) do
		for _, quest in ipairs(saga.quests) do
			if quest.id == questId then
				return quest, saga
			end
		end
	end
	return nil, nil
end

function Sagas:GetAvailableSagas(playerLevel: number, completedSagas: { string }): { any }
	local available = {}
	local completedSet = {}
	for _, id in ipairs(completedSagas) do
		completedSet[id] = true
	end

	for _, saga in ipairs(self.List) do
		if playerLevel >= saga.levelRequirement and not completedSet[saga.id] then
			table.insert(available, saga)
		end
	end
	return available
end

return Sagas
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="54">
          <Properties>
            <string name="Name">Settings</string>
            <string name="Source"><![CDATA[--[[
	Settings.lua
	Game-wide settings and tuning constants.
]]

local Settings = {}

-- Combat
Settings.Combat = {
	BASE_MELEE_RANGE = 6,
	BLOCK_DAMAGE_REDUCTION = 0.6,
	GUARD_BREAK_STUN = 1.5,
	VANISH_STAMINA_COST = 25,
	VANISH_COOLDOWN = 2.0,
	VANISH_I_FRAMES = 0.3,
	KI_CHARGE_RATE = 10,            -- Ki per second while charging
	KI_CHARGE_VULNERABILITY = true,
	AERIAL_RECOVERY_TIME = 0.5,
	GROUND_SLAM_BONUS = 1.5,        -- Multiplier for wall/ground slam
	BEAM_STRUGGLE_DURATION = 5.0,
	BEAM_STRUGGLE_TAP_WEIGHT = 0.4, -- How much taps matter vs ki power
	MAX_COMBO_HITS = 10,
	COMBAT_TIMEOUT = 5.0,           -- Seconds until "out of combat"
}

-- Progression
Settings.Progression = {
	MAX_LEVEL = 500,
	STAT_POINTS_PER_LEVEL = 3,
	XP_EXPONENT = 1.5,
	XP_BASE = 100,
	REBIRTH_STAT_BONUS = 0.05,
	MAX_REBIRTHS = 20,
	NPC_KILL_XP_BASE = 50,
	PVP_WIN_XP_BASE = 200,
	WORLD_BOSS_PARTICIPATION_XP = 1000,
}

-- Economy
Settings.Economy = {
	NPC_KILL_ZENI_BASE = 20,
	QUEST_ZENI_MULTIPLIER = 1.0,
	PVP_WIN_ZENI = 100,
	WORLD_BOSS_ZENI = 500,
}

-- PvP
Settings.PvP = {
	ELO_K_FACTOR = 32,
	ELO_FLOOR = 100,
	ELO_DEFAULT = 1000,
	MATCH_ROUNDS = 3,                -- Best of 3
	ROUND_TIME_LIMIT = 120,          -- Seconds per round
	QUEUE_TIMEOUT = 60,              -- Max queue wait time
	TOURNAMENT_MIN_PLAYERS = 8,
	TOURNAMENT_MAX_PLAYERS = 16,
}

-- World Boss
Settings.WorldBoss = {
	SPAWN_INTERVAL = 1800,           -- 30 minutes
	MIN_PLAYERS_TO_SPAWN = 3,
	DAMAGE_CONTRIBUTION_THRESHOLD = 0.05, -- 5% damage to get rewards
}

-- Misc
Settings.Misc = {
	AUTO_SAVE_INTERVAL = 60,         -- Seconds
	MAX_LOADOUT_SLOTS = 6,
}

return Settings
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="55">
          <Properties>
            <string name="Name">Stats</string>
            <string name="Source"><![CDATA[--[[
	Stats.lua
	Stat system constants and formulas.
]]

local Stats = {}

Stats.Names = { "Melee", "KiPower", "Defense", "Speed", "Health", "Stamina" }

Stats.Descriptions = {
	Melee = "Increases melee attack damage and combo power.",
	KiPower = "Increases ki blast damage and beam attack strength.",
	Defense = "Reduces incoming damage and increases block efficiency.",
	Speed = "Increases movement speed and attack speed.",
	Health = "Increases maximum health pool.",
	Stamina = "Increases maximum stamina for dodges and blocks.",
}

-- Points given per level up
Stats.POINTS_PER_LEVEL = 3

-- Max level before rebirth
Stats.MAX_LEVEL = 500

-- XP required for a given level
function Stats:GetRequiredXP(level: number): number
	return math.floor(100 * level ^ 1.5)
end

-- Effective stat value from base + allocated points
-- Each point = 1 unit for combat stats, 10 units for Health/Stamina
Stats.POINT_MULTIPLIERS = {
	Melee = 1,
	KiPower = 1,
	Defense = 1,
	Speed = 1,
	Health = 10,
	Stamina = 10,
}

-- Per-level passive stat growth (added automatically)
Stats.LEVEL_GROWTH = {
	Melee = 0.5,
	KiPower = 0.5,
	Defense = 0.5,
	Speed = 0.3,
	Health = 5,
	Stamina = 3,
}

-- Rebirth bonus per prestige
Stats.REBIRTH_BONUS_PERCENT = 0.05

return Stats
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="56">
          <Properties>
            <string name="Name">Transformations</string>
            <string name="Source"><![CDATA[--[[
	Transformations.lua
	All transformation definitions for every race.
]]

local Transformations = {}

Transformations.List = {
	-- =====================
	-- SAIYAN FORMS
	-- =====================
	Kaioken = {
		id = "Kaioken",
		race = { "Saiyan", "Human" },
		displayName = "Kaioken",
		prerequisite = nil,
		levelRequirement = 10,
		statMultipliers = { Melee = 1.2, KiPower = 1.2, Speed = 1.3, Defense = 0.9 },
		kiDrainPerSecond = 1,
		auraColor = Color3.fromRGB(255, 50, 50),
		auraId = "Kaioken_Aura",
		hairChange = nil,
		bodyChange = "Kaioken_Glow",
		unlockMethod = "level",
	},
	SSJ = {
		id = "SSJ",
		race = { "Saiyan" },
		displayName = "Super Saiyan",
		prerequisite = "Kaioken",
		levelRequirement = 30,
		statMultipliers = { Melee = 1.5, KiPower = 1.5, Speed = 1.3, Defense = 1.1 },
		kiDrainPerSecond = 2,
		auraColor = Color3.fromRGB(255, 255, 50),
		auraId = "SSJ_Aura",
		hairChange = "SSJ_Hair",
		bodyChange = nil,
		unlockMethod = "quest",
	},
	SSJ2 = {
		id = "SSJ2",
		race = { "Saiyan" },
		displayName = "Super Saiyan 2",
		prerequisite = "SSJ",
		levelRequirement = 60,
		statMultipliers = { Melee = 2.0, KiPower = 2.0, Speed = 1.5, Defense = 1.2 },
		kiDrainPerSecond = 3,
		auraColor = Color3.fromRGB(255, 255, 100),
		auraId = "SSJ2_Aura",
		hairChange = "SSJ2_Hair",
		bodyChange = nil,
		unlockMethod = "quest",
	},
	SSJ3 = {
		id = "SSJ3",
		race = { "Saiyan" },
		displayName = "Super Saiyan 3",
		prerequisite = "SSJ2",
		levelRequirement = 100,
		statMultipliers = { Melee = 3.0, KiPower = 3.0, Speed = 1.8, Defense = 1.3 },
		kiDrainPerSecond = 5,
		auraColor = Color3.fromRGB(255, 255, 130),
		auraId = "SSJ3_Aura",
		hairChange = "SSJ3_Hair",
		bodyChange = nil,
		unlockMethod = "quest",
	},
	SSG = {
		id = "SSG",
		race = { "Saiyan" },
		displayName = "Super Saiyan God",
		prerequisite = "SSJ3",
		levelRequirement = 150,
		statMultipliers = { Melee = 4.0, KiPower = 4.5, Speed = 2.5, Defense = 2.0 },
		kiDrainPerSecond = 4,
		auraColor = Color3.fromRGB(255, 80, 80),
		auraId = "SSG_Aura",
		hairChange = "SSG_Hair",
		bodyChange = nil,
		unlockMethod = "quest",
	},
	SSB = {
		id = "SSB",
		race = { "Saiyan" },
		displayName = "Super Saiyan Blue",
		prerequisite = "SSG",
		levelRequirement = 200,
		statMultipliers = { Melee = 5.0, KiPower = 5.5, Speed = 3.0, Defense = 2.5 },
		kiDrainPerSecond = 5,
		auraColor = Color3.fromRGB(50, 150, 255),
		auraId = "SSB_Aura",
		hairChange = "SSB_Hair",
		bodyChange = nil,
		unlockMethod = "quest",
	},
	SSBE = {
		id = "SSBE",
		race = { "Saiyan" },
		displayName = "Super Saiyan Blue Evolution",
		prerequisite = "SSB",
		levelRequirement = 300,
		statMultipliers = { Melee = 6.5, KiPower = 7.0, Speed = 3.5, Defense = 3.0 },
		kiDrainPerSecond = 7,
		auraColor = Color3.fromRGB(30, 100, 255),
		auraId = "SSBE_Aura",
		hairChange = "SSBE_Hair",
		bodyChange = nil,
		unlockMethod = "quest",
	},
	UltraInstinct = {
		id = "UltraInstinct",
		race = { "Saiyan" },
		displayName = "Ultra Instinct",
		prerequisite = "SSBE",
		levelRequirement = 400,
		statMultipliers = { Melee = 8.0, KiPower = 8.0, Speed = 5.0, Defense = 4.0 },
		kiDrainPerSecond = 10,
		auraColor = Color3.fromRGB(200, 200, 255),
		auraId = "UI_Aura",
		hairChange = "UI_Hair",
		bodyChange = "UI_Glow",
		unlockMethod = "quest",
	},

	-- =====================
	-- NAMEKIAN FORMS
	-- =====================
	GiantForm = {
		id = "GiantForm",
		race = { "Namekian" },
		displayName = "Giant Form",
		prerequisite = nil,
		levelRequirement = 30,
		statMultipliers = { Melee = 2.0, KiPower = 1.0, Speed = 0.6, Defense = 2.5, Health = 2.0 },
		kiDrainPerSecond = 3,
		auraColor = Color3.fromRGB(100, 255, 100),
		auraId = "Giant_Aura",
		hairChange = nil,
		bodyChange = "Giant_Scale",
		unlockMethod = "quest",
	},
	OrangeNamekian = {
		id = "OrangeNamekian",
		race = { "Namekian" },
		displayName = "Orange Piccolo",
		prerequisite = "GiantForm",
		levelRequirement = 200,
		statMultipliers = { Melee = 6.0, KiPower = 5.0, Speed = 3.0, Defense = 4.0 },
		kiDrainPerSecond = 6,
		auraColor = Color3.fromRGB(255, 165, 0),
		auraId = "Orange_Aura",
		hairChange = nil,
		bodyChange = "Orange_Body",
		unlockMethod = "quest",
	},

	-- =====================
	-- FRIEZA RACE FORMS
	-- =====================
	SecondForm = {
		id = "SecondForm",
		race = { "FriezaRace" },
		displayName = "Second Form",
		prerequisite = nil,
		levelRequirement = 20,
		statMultipliers = { Melee = 1.5, KiPower = 1.5, Speed = 1.0, Defense = 1.5 },
		kiDrainPerSecond = 0,
		auraColor = Color3.fromRGB(200, 150, 255),
		auraId = "Frieza2_Aura",
		hairChange = nil,
		bodyChange = "Frieza_Second",
		unlockMethod = "level",
	},
	ThirdForm = {
		id = "ThirdForm",
		race = { "FriezaRace" },
		displayName = "Third Form",
		prerequisite = "SecondForm",
		levelRequirement = 50,
		statMultipliers = { Melee = 2.0, KiPower = 2.0, Speed = 1.2, Defense = 1.8 },
		kiDrainPerSecond = 0,
		auraColor = Color3.fromRGB(200, 150, 255),
		auraId = "Frieza3_Aura",
		hairChange = nil,
		bodyChange = "Frieza_Third",
		unlockMethod = "level",
	},
	FinalForm = {
		id = "FinalForm",
		race = { "FriezaRace" },
		displayName = "Final Form",
		prerequisite = "ThirdForm",
		levelRequirement = 80,
		statMultipliers = { Melee = 3.5, KiPower = 3.5, Speed = 2.5, Defense = 2.0 },
		kiDrainPerSecond = 1,
		auraColor = Color3.fromRGB(220, 180, 255),
		auraId = "FriezaFinal_Aura",
		hairChange = nil,
		bodyChange = "Frieza_Final",
		unlockMethod = "quest",
	},
	GoldenForm = {
		id = "GoldenForm",
		race = { "FriezaRace" },
		displayName = "Golden Form",
		prerequisite = "FinalForm",
		levelRequirement = 200,
		statMultipliers = { Melee = 5.5, KiPower = 6.0, Speed = 3.5, Defense = 3.0 },
		kiDrainPerSecond = 5,
		auraColor = Color3.fromRGB(255, 215, 0),
		auraId = "Golden_Aura",
		hairChange = nil,
		bodyChange = "Frieza_Golden",
		unlockMethod = "quest",
	},
	BlackFrieza = {
		id = "BlackFrieza",
		race = { "FriezaRace" },
		displayName = "Black Frieza",
		prerequisite = "GoldenForm",
		levelRequirement = 400,
		statMultipliers = { Melee = 8.5, KiPower = 8.5, Speed = 4.5, Defense = 4.5 },
		kiDrainPerSecond = 8,
		auraColor = Color3.fromRGB(30, 30, 30),
		auraId = "BlackFrieza_Aura",
		hairChange = nil,
		bodyChange = "Frieza_Black",
		unlockMethod = "quest",
	},

	-- =====================
	-- MAJIN FORMS
	-- =====================
	EvilBuu = {
		id = "EvilBuu",
		race = { "Majin" },
		displayName = "Evil Buu",
		prerequisite = nil,
		levelRequirement = 30,
		statMultipliers = { Melee = 1.8, KiPower = 1.5, Speed = 1.3, Defense = 1.2 },
		kiDrainPerSecond = 2,
		auraColor = Color3.fromRGB(200, 100, 200),
		auraId = "EvilBuu_Aura",
		hairChange = nil,
		bodyChange = "EvilBuu_Body",
		unlockMethod = "quest",
	},
	SuperBuu = {
		id = "SuperBuu",
		race = { "Majin" },
		displayName = "Super Buu",
		prerequisite = "EvilBuu",
		levelRequirement = 80,
		statMultipliers = { Melee = 3.0, KiPower = 3.0, Speed = 2.0, Defense = 2.0 },
		kiDrainPerSecond = 4,
		auraColor = Color3.fromRGB(255, 100, 255),
		auraId = "SuperBuu_Aura",
		hairChange = nil,
		bodyChange = "SuperBuu_Body",
		unlockMethod = "quest",
	},
	KidBuu = {
		id = "KidBuu",
		race = { "Majin" },
		displayName = "Kid Buu",
		prerequisite = "SuperBuu",
		levelRequirement = 150,
		statMultipliers = { Melee = 5.0, KiPower = 4.0, Speed = 4.0, Defense = 2.0 },
		kiDrainPerSecond = 6,
		auraColor = Color3.fromRGB(255, 50, 200),
		auraId = "KidBuu_Aura",
		hairChange = nil,
		bodyChange = "KidBuu_Body",
		unlockMethod = "quest",
	},
	PureForm = {
		id = "PureForm",
		race = { "Majin" },
		displayName = "Pure Form",
		prerequisite = "KidBuu",
		levelRequirement = 300,
		statMultipliers = { Melee = 7.0, KiPower = 7.0, Speed = 3.5, Defense = 3.5 },
		kiDrainPerSecond = 8,
		auraColor = Color3.fromRGB(255, 150, 255),
		auraId = "PureForm_Aura",
		hairChange = nil,
		bodyChange = "PureForm_Body",
		unlockMethod = "quest",
	},

	-- =====================
	-- ANDROID FORMS
	-- =====================
	SemiPerfect = {
		id = "SemiPerfect",
		race = { "Android" },
		displayName = "Semi-Perfect Form",
		prerequisite = nil,
		levelRequirement = 40,
		statMultipliers = { Melee = 1.8, KiPower = 1.8, Speed = 1.2, Defense = 1.5 },
		kiDrainPerSecond = 1,
		auraColor = Color3.fromRGB(100, 255, 100),
		auraId = "SemiPerfect_Aura",
		hairChange = nil,
		bodyChange = "SemiPerfect_Body",
		unlockMethod = "quest",
	},
	Perfect = {
		id = "Perfect",
		race = { "Android" },
		displayName = "Perfect Form",
		prerequisite = "SemiPerfect",
		levelRequirement = 120,
		statMultipliers = { Melee = 4.0, KiPower = 4.0, Speed = 2.5, Defense = 3.0 },
		kiDrainPerSecond = 2,
		auraColor = Color3.fromRGB(150, 255, 150),
		auraId = "Perfect_Aura",
		hairChange = nil,
		bodyChange = "Perfect_Body",
		unlockMethod = "quest",
	},
	SuperPerfect = {
		id = "SuperPerfect",
		race = { "Android" },
		displayName = "Super Perfect Form",
		prerequisite = "Perfect",
		levelRequirement = 250,
		statMultipliers = { Melee = 6.0, KiPower = 6.5, Speed = 3.5, Defense = 3.5 },
		kiDrainPerSecond = 4,
		auraColor = Color3.fromRGB(200, 255, 50),
		auraId = "SuperPerfect_Aura",
		hairChange = nil,
		bodyChange = "SuperPerfect_Body",
		unlockMethod = "quest",
	},

	-- =====================
	-- HUMAN FORMS
	-- =====================
	PotentialUnleashed = {
		id = "PotentialUnleashed",
		race = { "Human" },
		displayName = "Potential Unleashed",
		prerequisite = "Kaioken",
		levelRequirement = 80,
		statMultipliers = { Melee = 3.0, KiPower = 3.0, Speed = 2.0, Defense = 2.0 },
		kiDrainPerSecond = 2,
		auraColor = Color3.fromRGB(255, 255, 255),
		auraId = "PU_Aura",
		hairChange = nil,
		bodyChange = "PU_Glow",
		unlockMethod = "quest",
	},
	MaxPower = {
		id = "MaxPower",
		race = { "Human" },
		displayName = "Max Power",
		prerequisite = "PotentialUnleashed",
		levelRequirement = 200,
		statMultipliers = { Melee = 5.0, KiPower = 5.0, Speed = 3.0, Defense = 3.0 },
		kiDrainPerSecond = 5,
		auraColor = Color3.fromRGB(255, 255, 200),
		auraId = "MaxPower_Aura",
		hairChange = nil,
		bodyChange = "MaxPower_Glow",
		unlockMethod = "quest",
	},
}

function Transformations:Get(formId: string)
	return self.List[formId]
end

function Transformations:GetTreeForRace(raceName: string): { string }
	-- Returns ordered list from Races constant
	-- This is a helper; actual tree order comes from Races.lua
	local tree = {}
	for id, form in pairs(self.List) do
		for _, race in ipairs(form.race) do
			if race == raceName then
				table.insert(tree, id)
				break
			end
		end
	end
	return tree
end

return Transformations
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="57">
          <Properties>
            <string name="Name">Zones</string>
            <string name="Source"><![CDATA[--[[
	Zones.lua
	World zone definitions with level requirements and properties.
]]

local Zones = {}

Zones.List = {
	{
		id = "WestCity",
		displayName = "West City",
		map = "Earth",
		levelRequirement = 1,
		xpMultiplier = 1.0,
		description = "The starting area. A bustling metropolis on Earth.",
		npcLevelRange = { 1, 15 },
		spawnPoint = Vector3.new(0, 5, 0),
	},
	{
		id = "WastelandArena",
		displayName = "Wasteland Arena",
		map = "Earth",
		levelRequirement = 10,
		xpMultiplier = 1.0,
		description = "A barren rocky battlefield. Perfect for training.",
		npcLevelRange = { 10, 30 },
		spawnPoint = Vector3.new(500, 5, 0),
	},
	{
		id = "KamisLookout",
		displayName = "Kami's Lookout",
		map = "Earth",
		levelRequirement = 20,
		xpMultiplier = 1.5,
		description = "The guardian's palace above the clouds. Enhanced training grounds.",
		npcLevelRange = { 20, 50 },
		spawnPoint = Vector3.new(0, 500, 500),
	},
	{
		id = "HyperbolicTimeChamber",
		displayName = "Hyperbolic Time Chamber",
		map = "Earth",
		levelRequirement = 30,
		xpMultiplier = 3.0,
		description = "A dimension where time moves differently. Extreme training environment.",
		npcLevelRange = { 30, 80 },
		spawnPoint = Vector3.new(0, 5, 1000),
	},
	{
		id = "Namek",
		displayName = "Planet Namek",
		map = "Namek",
		levelRequirement = 40,
		xpMultiplier = 1.5,
		description = "The home world of the Namekians. Green skies and blue grass.",
		npcLevelRange = { 40, 80 },
		spawnPoint = Vector3.new(2000, 5, 0),
	},
	{
		id = "Space",
		displayName = "Deep Space",
		map = "Space",
		levelRequirement = 60,
		xpMultiplier = 2.0,
		description = "The cold void of space. Only the strongest survive here.",
		npcLevelRange = { 60, 120 },
		spawnPoint = Vector3.new(4000, 5, 0),
	},
	{
		id = "BeerusPlanet",
		displayName = "Beerus's Planet",
		map = "BeerusPlanet",
		levelRequirement = 80,
		xpMultiplier = 2.5,
		description = "The God of Destruction's home. Divine energy flows here.",
		npcLevelRange = { 80, 200 },
		spawnPoint = Vector3.new(6000, 5, 0),
	},
	{
		id = "TournamentOfPower",
		displayName = "Tournament of Power Arena",
		map = "ToP",
		levelRequirement = 100,
		xpMultiplier = 3.0,
		description = "The ultimate battle arena. Fight for the survival of your universe.",
		npcLevelRange = { 100, 500 },
		spawnPoint = Vector3.new(8000, 5, 0),
	},
}

function Zones:GetById(zoneId: string)
	for _, zone in ipairs(self.List) do
		if zone.id == zoneId then
			return zone
		end
	end
	return nil
end

function Zones:GetAccessibleZones(playerLevel: number): { any }
	local accessible = {}
	for _, zone in ipairs(self.List) do
		if playerLevel >= zone.levelRequirement then
			table.insert(accessible, zone)
		end
	end
	return accessible
end

return Zones
]]></string>
          </Properties>
        </Item>
      </Item>
      <Item class="Folder" referent="58">
        <Properties>
          <string name="Name">Network</string>
        </Properties>
        <Item class="ModuleScript" referent="59">
          <Properties>
            <string name="Name">Remotes</string>
            <string name="Source"><![CDATA[--[[
	Remotes.lua
	Central registry for all RemoteEvents and RemoteFunctions.
	Accessed by both server and client via ReplicatedStorage.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Remotes = {}

-- Remote definitions: { Name = "RemoteEvent" | "RemoteFunction" }
local REMOTE_DEFINITIONS = {
	-- Combat
	CombatIntent = "RemoteEvent",        -- Client -> Server: attack type, target
	CombatResult = "RemoteEvent",        -- Server -> Client: damage, knockback, state
	BeamStruggleInput = "RemoteEvent",   -- Client -> Server: tap events during beam struggle
	BeamStruggleSync = "RemoteEvent",    -- Server -> Client: beam struggle state

	-- Transformation
	RequestTransform = "RemoteEvent",    -- Client -> Server: requested form ID
	RequestRevert = "RemoteEvent",       -- Client -> Server: revert to base
	TransformSync = "RemoteEvent",       -- Server -> All Clients: player transformed

	-- Data / Stats
	StatAllocation = "RemoteEvent",      -- Client -> Server: stat point spending
	RequestPlayerData = "RemoteFunction", -- Client -> Server: get own profile data
	PlayerDataUpdate = "RemoteEvent",    -- Server -> Client: profile data changed

	-- Character Creation
	SubmitCharacterCreation = "RemoteFunction", -- Client -> Server: race + appearance

	-- Quest
	QuestAccept = "RemoteEvent",         -- Client -> Server: accept quest ID
	QuestAbandon = "RemoteEvent",        -- Client -> Server: abandon quest ID
	QuestUpdate = "RemoteEvent",         -- Server -> Client: quest progress update
	QuestComplete = "RemoteEvent",       -- Server -> Client: quest completed

	-- NPC / Dialogue
	NPCInteract = "RemoteEvent",        -- Client -> Server: interact with NPC
	DialogueResponse = "RemoteEvent",    -- Client -> Server: dialogue choice

	-- PvP
	PvpQueueJoin = "RemoteEvent",        -- Client -> Server: join queue (mode)
	PvpQueueLeave = "RemoteEvent",       -- Client -> Server: leave queue
	PvpMatchStart = "RemoteEvent",       -- Server -> Client: match starting
	PvpMatchEnd = "RemoteEvent",         -- Server -> Client: match results

	-- Shop
	RequestShop = "RemoteFunction",      -- Client -> Server: get shop inventory
	PurchaseItem = "RemoteFunction",     -- Client -> Server: buy item

	-- Move Loadout
	UpdateLoadout = "RemoteEvent",       -- Client -> Server: new loadout config

	-- Rebirth
	RequestRebirth = "RemoteFunction",   -- Client -> Server: attempt rebirth
}

-- Folder to hold all remotes
local remotesFolder = ReplicatedStorage:FindFirstChild("Remotes")
if not remotesFolder then
	remotesFolder = Instance.new("Folder")
	remotesFolder.Name = "Remotes"
	remotesFolder.Parent = ReplicatedStorage
end

-- Create or find remotes
for name, remoteType in pairs(REMOTE_DEFINITIONS) do
	local existing = remotesFolder:FindFirstChild(name)
	if not existing then
		local remote = Instance.new(remoteType)
		remote.Name = name
		remote.Parent = remotesFolder
	end
end

-- Access helper
function Remotes:Get(name: string): RemoteEvent | RemoteFunction
	local remote = remotesFolder:FindFirstChild(name)
	if not remote then
		-- Wait briefly in case of race condition on startup
		remote = remotesFolder:WaitForChild(name, 5)
	end
	assert(remote, "Remote not found: " .. name)
	return remote
end

return Remotes
]]></string>
          </Properties>
        </Item>
      </Item>
      <Item class="Folder" referent="60">
        <Properties>
          <string name="Name">Types</string>
        </Properties>
        <Item class="ModuleScript" referent="61">
          <Properties>
            <string name="Name">GameTypes</string>
            <string name="Source"><![CDATA[--[[
	GameTypes.lua
	Type definitions for the game (documentation/reference).
	Luau doesn't have full structural types, so these serve as documented shapes.
]]

export type StatBlock = {
	Melee: number,
	KiPower: number,
	Defense: number,
	Speed: number,
	Health: number,
	Stamina: number,
}

export type PlayerProfile = {
	Race: string,
	Level: number,
	XP: number,
	Zeni: number,
	Rebirths: number,
	Stats: StatBlock,
	StatPoints: number,
	UnlockedTransformations: { string },
	CurrentTransformation: string,
	UnlockedMoves: { string },
	MoveLoadout: { string? },
	Appearance: Appearance,
	QuestProgress: { [string]: QuestState },
	CompletedSagas: { string },
	PvpElo: number,
	PvpWins: number,
	PvpLosses: number,
}

export type Appearance = {
	SkinColor: Color3,
	HairStyle: string,
	HairColor: Color3,
	Outfit: string,
	OutfitColor: Color3,
}

export type QuestState = {
	questId: string,
	objectives: { ObjectiveProgress },
	completed: boolean,
}

export type ObjectiveProgress = {
	type: string,
	current: number,
	target: number,
	completed: boolean,
}

export type CombatState = "Idle" | "Attacking" | "Blocking" | "Stunned" | "Dodging" | "Charging" | "InBeamStruggle" | "Knocked" | "Aerial" | "Dead"

export type CombatIntent = {
	action: string,
	target: Player?,
	moveId: string?,
	timestamp: number,
}

export type DamageResult = {
	damage: number,
	knockback: Vector3,
	hitstun: number,
	guardBreak: boolean?,
	launcher: boolean?,
	blocked: boolean?,
}

return nil
]]></string>
          </Properties>
        </Item>
      </Item>
      <Item class="Folder" referent="62">
        <Properties>
          <string name="Name">Utils</string>
        </Properties>
        <Item class="ModuleScript" referent="63">
          <Properties>
            <string name="Name">AnimationUtil</string>
            <string name="Source"><![CDATA[--[[
	AnimationUtil.lua
	Helpers for loading and playing animations.
]]

local AnimationUtil = {}

-- Cache for loaded animation tracks per humanoid
local trackCache: { [Humanoid]: { [string]: AnimationTrack } } = {}

function AnimationUtil.LoadAnimation(humanoid: Humanoid, animationId: string): AnimationTrack
	if not trackCache[humanoid] then
		trackCache[humanoid] = {}
	end

	if trackCache[humanoid][animationId] then
		return trackCache[humanoid][animationId]
	end

	local animation = Instance.new("Animation")
	animation.AnimationId = animationId

	local animator = humanoid:FindFirstChildOfClass("Animator")
	if not animator then
		animator = Instance.new("Animator")
		animator.Parent = humanoid
	end

	local track = animator:LoadAnimation(animation)
	trackCache[humanoid][animationId] = track
	animation:Destroy()

	return track
end

function AnimationUtil.PlayAnimation(humanoid: Humanoid, animationId: string, fadeTime: number?, weight: number?, speed: number?): AnimationTrack
	local track = AnimationUtil.LoadAnimation(humanoid, animationId)
	track:Play(fadeTime or 0.1, weight or 1, speed or 1)
	return track
end

function AnimationUtil.StopAllAnimations(humanoid: Humanoid, fadeTime: number?)
	local animator = humanoid:FindFirstChildOfClass("Animator")
	if animator then
		for _, track in ipairs(animator:GetPlayingAnimationTracks()) do
			track:Stop(fadeTime or 0.1)
		end
	end
end

function AnimationUtil.CleanupCache(humanoid: Humanoid)
	trackCache[humanoid] = nil
end

return AnimationUtil
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="64">
          <Properties>
            <string name="Name">CombatUtil</string>
            <string name="Source"><![CDATA[--[[
	CombatUtil.lua
	Shared combat calculation utilities used by both server and client (for prediction).
]]

local CombatUtil = {}

-- Calculate raw damage from attacker stats, move multiplier, and transformation
function CombatUtil.CalculateDamage(
	attackerStats: { Melee: number, KiPower: number },
	damageMultiplier: number,
	isKiAttack: boolean,
	transformMultiplier: number?
): number
	local baseStat = if isKiAttack then attackerStats.KiPower else attackerStats.Melee
	local transformMult = transformMultiplier or 1.0
	return math.floor(baseStat * damageMultiplier * transformMult)
end

-- Apply defense reduction
function CombatUtil.ApplyDefense(rawDamage: number, defenderDefense: number, isBlocking: boolean, blockReduction: number): number
	local defenseReduction = defenderDefense / (defenderDefense + 100) -- Diminishing returns
	local damage = rawDamage * (1 - defenseReduction)

	if isBlocking then
		damage = damage * (1 - blockReduction)
	end

	return math.max(1, math.floor(damage))
end

-- Check if attacker is in range of target
function CombatUtil.IsInRange(attackerPos: Vector3, targetPos: Vector3, range: number): boolean
	return (attackerPos - targetPos).Magnitude <= range
end

-- Calculate knockback direction
function CombatUtil.GetKnockbackDirection(attackerPos: Vector3, targetPos: Vector3): Vector3
	local direction = (targetPos - attackerPos).Unit
	if direction ~= direction then -- NaN check
		direction = Vector3.new(0, 0, 1)
	end
	return direction
end

-- Determine beam struggle winner from inputs and ki power
function CombatUtil.ResolveBeamStruggle(
	player1Ki: number,
	player1Taps: number,
	player2Ki: number,
	player2Taps: number,
	tapWeight: number
): number -- Returns 1 or 2 for winner
	local kiWeight = 1 - tapWeight
	local score1 = (player1Ki * kiWeight) + (player1Taps * tapWeight)
	local score2 = (player2Ki * kiWeight) + (player2Taps * tapWeight)

	if score1 >= score2 then
		return 1
	else
		return 2
	end
end

return CombatUtil
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="65">
          <Properties>
            <string name="Name">MathUtil</string>
            <string name="Source"><![CDATA[--[[
	MathUtil.lua
	Common math utilities.
]]

local MathUtil = {}

function MathUtil.Lerp(a: number, b: number, t: number): number
	return a + (b - a) * t
end

function MathUtil.Clamp(value: number, min: number, max: number): number
	return math.max(min, math.min(max, value))
end

function MathUtil.Map(value: number, inMin: number, inMax: number, outMin: number, outMax: number): number
	return outMin + (value - inMin) * (outMax - outMin) / (inMax - inMin)
end

function MathUtil.Round(value: number, decimalPlaces: number?): number
	local mult = 10 ^ (decimalPlaces or 0)
	return math.floor(value * mult + 0.5) / mult
end

function MathUtil.CalculateElo(winnerElo: number, loserElo: number, kFactor: number): (number, number)
	local expectedWinner = 1 / (1 + 10 ^ ((loserElo - winnerElo) / 400))
	local expectedLoser = 1 - expectedWinner

	local newWinner = math.floor(winnerElo + kFactor * (1 - expectedWinner))
	local newLoser = math.max(100, math.floor(loserElo + kFactor * (0 - expectedLoser)))

	return newWinner, newLoser
end

return MathUtil
]]></string>
          </Properties>
        </Item>
      </Item>
    </Item>
  </Item>
  <Item class="ServerScriptService" referent="66">
    <Properties>
      <string name="Name">ServerScriptService</string>
    </Properties>
    <Item class="Script" referent="67">
      <Properties>
        <string name="Name">Server</string>
        <token name="RunContext">0</token>
        <string name="Source"><![CDATA[--[[
	Server Entry Point
	Bootstraps Knit framework and starts all services.
]]

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

-- Load all services (Knit auto-discovers them via AddServices)
local Services = script.Services
for _, module in ipairs(Services:GetChildren()) do
	if module:IsA("ModuleScript") then
		require(module)
	end
end

-- Start Knit
Knit.Start():andThen(function()
	print("[Server] All services started successfully!")
end):catch(function(err)
	warn("[Server] Failed to start services:", err)
end)
]]></string>
      </Properties>
      <Item class="Folder" referent="68">
        <Properties>
          <string name="Name">Modules</string>
        </Properties>
        <Item class="ModuleScript" referent="69">
          <Properties>
            <string name="Name">CharacterBuilder</string>
            <string name="Source"><![CDATA[--[[
	CharacterBuilder.lua
	Builds custom character models based on race, appearance, and transformation state.
	Applies visual changes (hair, aura, body color) to the player's character.
]]

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

local Races = require(ReplicatedStorage.Shared.Constants.Races)
local Transformations = require(ReplicatedStorage.Shared.Constants.Transformations)

local CharacterBuilder = {}

-- Hair style definitions (mesh part sizes/positions)
local HAIR_STYLES = {
	Default = {
		parts = {
			{ size = Vector3.new(1.2, 0.6, 1.2), offset = Vector3.new(0, 0.8, 0), color = nil },
		},
	},
	Spiky = {
		parts = {
			{ size = Vector3.new(1.2, 0.5, 1.2), offset = Vector3.new(0, 0.8, 0), color = nil },
			{ size = Vector3.new(0.4, 0.8, 0.3), offset = Vector3.new(0.3, 1.1, 0), color = nil },
			{ size = Vector3.new(0.4, 0.9, 0.3), offset = Vector3.new(-0.3, 1.15, 0), color = nil },
			{ size = Vector3.new(0.4, 0.7, 0.3), offset = Vector3.new(0, 1.2, 0.2), color = nil },
			{ size = Vector3.new(0.3, 0.6, 0.4), offset = Vector3.new(0, 0.9, -0.4), color = nil },
		},
	},
	Long = {
		parts = {
			{ size = Vector3.new(1.3, 0.5, 1.3), offset = Vector3.new(0, 0.8, 0), color = nil },
			{ size = Vector3.new(1.0, 1.5, 0.4), offset = Vector3.new(0, 0, -0.6), color = nil },
		},
	},
	Mohawk = {
		parts = {
			{ size = Vector3.new(0.4, 1.0, 1.2), offset = Vector3.new(0, 1.0, 0), color = nil },
		},
	},
	Bald = {
		parts = {},
	},
	-- Transformation hair overrides
	SSJ_Hair = {
		parts = {
			{ size = Vector3.new(1.3, 0.5, 1.3), offset = Vector3.new(0, 0.8, 0), color = Color3.fromRGB(255, 255, 50) },
			{ size = Vector3.new(0.5, 1.2, 0.3), offset = Vector3.new(0.35, 1.2, 0), color = Color3.fromRGB(255, 255, 50) },
			{ size = Vector3.new(0.5, 1.3, 0.3), offset = Vector3.new(-0.35, 1.3, 0), color = Color3.fromRGB(255, 255, 50) },
			{ size = Vector3.new(0.5, 1.1, 0.3), offset = Vector3.new(0, 1.35, 0.2), color = Color3.fromRGB(255, 255, 50) },
			{ size = Vector3.new(0.4, 1.0, 0.4), offset = Vector3.new(0, 1.1, -0.4), color = Color3.fromRGB(255, 255, 50) },
		},
	},
	SSJ2_Hair = {
		parts = {
			{ size = Vector3.new(1.3, 0.5, 1.3), offset = Vector3.new(0, 0.8, 0), color = Color3.fromRGB(255, 255, 80) },
			{ size = Vector3.new(0.5, 1.5, 0.3), offset = Vector3.new(0.4, 1.4, 0), color = Color3.fromRGB(255, 255, 80) },
			{ size = Vector3.new(0.5, 1.6, 0.3), offset = Vector3.new(-0.4, 1.5, 0), color = Color3.fromRGB(255, 255, 80) },
			{ size = Vector3.new(0.5, 1.4, 0.3), offset = Vector3.new(0, 1.5, 0.25), color = Color3.fromRGB(255, 255, 80) },
			{ size = Vector3.new(0.4, 1.3, 0.5), offset = Vector3.new(0, 1.3, -0.4), color = Color3.fromRGB(255, 255, 80) },
			{ size = Vector3.new(0.3, 1.0, 0.3), offset = Vector3.new(0.2, 1.6, 0.1), color = Color3.fromRGB(255, 255, 80) },
		},
	},
	SSJ3_Hair = {
		parts = {
			{ size = Vector3.new(1.3, 0.5, 1.3), offset = Vector3.new(0, 0.8, 0), color = Color3.fromRGB(255, 255, 50) },
			-- Long flowing hair down the back
			{ size = Vector3.new(1.0, 0.4, 0.6), offset = Vector3.new(0, 0.6, -0.5), color = Color3.fromRGB(255, 255, 50) },
			{ size = Vector3.new(0.9, 0.4, 0.6), offset = Vector3.new(0, 0.0, -0.7), color = Color3.fromRGB(255, 255, 50) },
			{ size = Vector3.new(0.8, 0.4, 0.6), offset = Vector3.new(0, -0.6, -0.8), color = Color3.fromRGB(255, 255, 50) },
			{ size = Vector3.new(0.7, 0.4, 0.6), offset = Vector3.new(0, -1.2, -0.9), color = Color3.fromRGB(255, 255, 50) },
			{ size = Vector3.new(0.6, 0.4, 0.6), offset = Vector3.new(0, -1.8, -0.9), color = Color3.fromRGB(255, 255, 50) },
			{ size = Vector3.new(0.5, 0.4, 0.5), offset = Vector3.new(0, -2.4, -0.8), color = Color3.fromRGB(255, 255, 50) },
		},
	},
	SSG_Hair = {
		parts = {
			{ size = Vector3.new(1.2, 0.5, 1.2), offset = Vector3.new(0, 0.8, 0), color = Color3.fromRGB(255, 50, 50) },
			{ size = Vector3.new(0.4, 0.6, 0.3), offset = Vector3.new(0.3, 1.0, 0), color = Color3.fromRGB(255, 50, 50) },
			{ size = Vector3.new(0.4, 0.7, 0.3), offset = Vector3.new(-0.3, 1.05, 0), color = Color3.fromRGB(255, 50, 50) },
		},
	},
	SSB_Hair = {
		parts = {
			{ size = Vector3.new(1.3, 0.5, 1.3), offset = Vector3.new(0, 0.8, 0), color = Color3.fromRGB(50, 150, 255) },
			{ size = Vector3.new(0.5, 1.2, 0.3), offset = Vector3.new(0.35, 1.2, 0), color = Color3.fromRGB(50, 150, 255) },
			{ size = Vector3.new(0.5, 1.3, 0.3), offset = Vector3.new(-0.35, 1.3, 0), color = Color3.fromRGB(50, 150, 255) },
			{ size = Vector3.new(0.5, 1.1, 0.3), offset = Vector3.new(0, 1.35, 0.2), color = Color3.fromRGB(50, 150, 255) },
		},
	},
	SSBE_Hair = {
		parts = {
			{ size = Vector3.new(1.3, 0.5, 1.3), offset = Vector3.new(0, 0.8, 0), color = Color3.fromRGB(30, 100, 255) },
			{ size = Vector3.new(0.5, 1.4, 0.3), offset = Vector3.new(0.35, 1.3, 0), color = Color3.fromRGB(30, 100, 255) },
			{ size = Vector3.new(0.5, 1.5, 0.3), offset = Vector3.new(-0.35, 1.4, 0), color = Color3.fromRGB(30, 100, 255) },
			{ size = Vector3.new(0.5, 1.3, 0.3), offset = Vector3.new(0, 1.5, 0.2), color = Color3.fromRGB(30, 100, 255) },
		},
	},
	UI_Hair = {
		parts = {
			{ size = Vector3.new(1.2, 0.5, 1.2), offset = Vector3.new(0, 0.8, 0), color = Color3.fromRGB(200, 200, 220) },
			{ size = Vector3.new(0.4, 0.8, 0.3), offset = Vector3.new(0.3, 1.1, 0), color = Color3.fromRGB(200, 200, 220) },
			{ size = Vector3.new(0.4, 0.9, 0.3), offset = Vector3.new(-0.3, 1.15, 0), color = Color3.fromRGB(200, 200, 220) },
		},
	},
}

-- Race-specific body customization
local RACE_BODY_CONFIGS = {
	Saiyan = {
		bodyScale = Vector3.new(1, 1, 1),
		defaultSkinColor = Color3.fromRGB(255, 200, 150),
		hasTail = true,
		tailColor = Color3.fromRGB(150, 100, 50),
	},
	Namekian = {
		bodyScale = Vector3.new(1.05, 1.1, 1),
		defaultSkinColor = Color3.fromRGB(80, 180, 80),
		hasTail = false,
		hasAntennae = true,
		antennaeColor = Color3.fromRGB(80, 180, 80),
	},
	FriezaRace = {
		bodyScale = Vector3.new(0.9, 0.95, 0.9),
		defaultSkinColor = Color3.fromRGB(220, 200, 230),
		hasTail = true,
		tailColor = Color3.fromRGB(220, 200, 230),
		hasHorns = true,
	},
	Majin = {
		bodyScale = Vector3.new(1.1, 1, 1.1),
		defaultSkinColor = Color3.fromRGB(255, 150, 180),
		hasTail = false,
		hasHeadTentacle = true,
	},
	Android = {
		bodyScale = Vector3.new(1, 1, 1),
		defaultSkinColor = Color3.fromRGB(240, 220, 200),
		hasTail = false,
	},
	Human = {
		bodyScale = Vector3.new(1, 1, 1),
		defaultSkinColor = Color3.fromRGB(255, 200, 150),
		hasTail = false,
	},
}

-- Apply full character appearance
function CharacterBuilder.ApplyAppearance(player: Player, data: any)
	local character = player.Character
	if not character then return end

	local humanoid = character:FindFirstChildOfClass("Humanoid")
	if not humanoid then return end

	local head = character:FindFirstChild("Head")
	local torso = character:FindFirstChild("UpperTorso") or character:FindFirstChild("Torso")
	if not head then return end

	local raceName = data.Race
	local raceConfig = RACE_BODY_CONFIGS[raceName]
	if not raceConfig then return end

	-- Apply skin color
	local skinColor
	if data.Appearance and data.Appearance.SkinColor then
		local sc = data.Appearance.SkinColor
		skinColor = Color3.fromRGB(sc.R or 255, sc.G or 200, sc.B or 150)
	else
		skinColor = raceConfig.defaultSkinColor
	end

	-- Color all body parts
	for _, part in ipairs(character:GetDescendants()) do
		if part:IsA("BasePart") and part.Name ~= "HumanoidRootPart" then
			-- Only color skin parts, not accessories
			if part.Name == "Head" or part.Name == "Torso" or part.Name == "UpperTorso"
				or part.Name == "LowerTorso" or part.Name:find("Arm") or part.Name:find("Leg")
				or part.Name == "LeftHand" or part.Name == "RightHand"
				or part.Name == "LeftFoot" or part.Name == "RightFoot" then
				part.Color = skinColor
			end
		end
	end

	-- Apply hair
	CharacterBuilder.ApplyHair(character, data)

	-- Race-specific features
	CharacterBuilder.ApplyRaceFeatures(character, raceName, raceConfig, skinColor)

	-- Apply outfit color
	CharacterBuilder.ApplyOutfit(character, data)
end

function CharacterBuilder.ApplyHair(character: Model, data: any)
	-- Remove existing hair
	local head = character:FindFirstChild("Head")
	if not head then return end

	for _, child in ipairs(head:GetChildren()) do
		if child.Name == "Hair" or child.Name == "TransformHair" then
			child:Destroy()
		end
	end

	-- Determine hair style
	local hairStyleName = "Spiky" -- default
	if data.Appearance and data.Appearance.HairStyle then
		hairStyleName = data.Appearance.HairStyle
	end

	-- Check if transformation overrides hair
	if data.CurrentTransformation and data.CurrentTransformation ~= "Base" then
		local form = Transformations:Get(data.CurrentTransformation)
		if form and form.hairChange and HAIR_STYLES[form.hairChange] then
			hairStyleName = form.hairChange
		end
	end

	local hairStyle = HAIR_STYLES[hairStyleName]
	if not hairStyle then
		hairStyle = HAIR_STYLES["Spiky"]
	end

	-- Hair color
	local hairColor = Color3.fromRGB(30, 30, 30)
	if data.Appearance and data.Appearance.HairColor then
		local hc = data.Appearance.HairColor
		hairColor = Color3.fromRGB(hc.R or 30, hc.G or 30, hc.B or 30)
	end

	-- Build hair parts
	local hairModel = Instance.new("Model")
	hairModel.Name = "Hair"
	hairModel.Parent = head

	for i, partDef in ipairs(hairStyle.parts) do
		local part = Instance.new("Part")
		part.Name = "HairPart_" .. i
		part.Size = partDef.size
		part.Color = partDef.color or hairColor
		part.Material = Enum.Material.SmoothPlastic
		part.CanCollide = false
		part.Anchored = false
		part.Parent = hairModel

		-- Weld to head
		local weld = Instance.new("WeldConstraint")
		weld.Part0 = head
		weld.Part1 = part
		weld.Parent = part

		part.CFrame = head.CFrame * CFrame.new(partDef.offset)
	end
end

function CharacterBuilder.ApplyRaceFeatures(character: Model, raceName: string, config: any, skinColor: Color3)
	-- Remove existing race features
	for _, child in ipairs(character:GetDescendants()) do
		if child.Name == "RaceFeature" then
			child:Destroy()
		end
	end

	local torso = character:FindFirstChild("UpperTorso") or character:FindFirstChild("Torso")
	local head = character:FindFirstChild("Head")
	if not torso or not head then return end

	-- Tail (Saiyan, Frieza Race)
	if config.hasTail then
		local tail = Instance.new("Part")
		tail.Name = "RaceFeature"
		tail.Size = Vector3.new(0.3, 0.3, 1.5)
		tail.Color = config.tailColor or skinColor
		tail.Material = Enum.Material.SmoothPlastic
		tail.CanCollide = false
		tail.Anchored = false
		tail.Parent = character

		local weld = Instance.new("WeldConstraint")
		weld.Part0 = torso
		weld.Part1 = tail
		weld.Parent = tail

		tail.CFrame = torso.CFrame * CFrame.new(0, -0.8, -0.8)
	end

	-- Antennae (Namekian)
	if config.hasAntennae then
		for _, xOff in ipairs({ -0.2, 0.2 }) do
			local antenna = Instance.new("Part")
			antenna.Name = "RaceFeature"
			antenna.Size = Vector3.new(0.1, 0.8, 0.1)
			antenna.Color = config.antennaeColor or skinColor
			antenna.Material = Enum.Material.SmoothPlastic
			antenna.CanCollide = false
			antenna.Anchored = false
			antenna.Parent = character

			local weld = Instance.new("WeldConstraint")
			weld.Part0 = head
			weld.Part1 = antenna
			weld.Parent = antenna

			antenna.CFrame = head.CFrame * CFrame.new(xOff, 0.8, 0.2) * CFrame.Angles(math.rad(-30), 0, 0)
		end
	end

	-- Horns (Frieza Race)
	if config.hasHorns then
		for _, xOff in ipairs({ -0.4, 0.4 }) do
			local horn = Instance.new("Part")
			horn.Name = "RaceFeature"
			horn.Size = Vector3.new(0.15, 0.6, 0.15)
			horn.Color = Color3.fromRGB(220, 220, 230)
			horn.Material = Enum.Material.SmoothPlastic
			horn.CanCollide = false
			horn.Anchored = false
			horn.Parent = character

			local weld = Instance.new("WeldConstraint")
			weld.Part0 = head
			weld.Part1 = horn
			weld.Parent = horn

			horn.CFrame = head.CFrame * CFrame.new(xOff, 0.7, 0) * CFrame.Angles(0, 0, xOff > 0 and math.rad(20) or math.rad(-20))
		end
	end

	-- Head tentacle (Majin)
	if config.hasHeadTentacle then
		local tentacle = Instance.new("Part")
		tentacle.Name = "RaceFeature"
		tentacle.Size = Vector3.new(0.3, 0.3, 1.5)
		tentacle.Color = skinColor
		tentacle.Material = Enum.Material.SmoothPlastic
		tentacle.CanCollide = false
		tentacle.Anchored = false
		tentacle.Parent = character

		local weld = Instance.new("WeldConstraint")
		weld.Part0 = head
		weld.Part1 = tentacle
		weld.Parent = tentacle

		tentacle.CFrame = head.CFrame * CFrame.new(0, 0.5, -0.5) * CFrame.Angles(math.rad(-60), 0, 0)
	end
end

function CharacterBuilder.ApplyOutfit(character: Model, data: any)
	-- Remove existing outfit parts
	for _, child in ipairs(character:GetDescendants()) do
		if child.Name == "OutfitPart" then
			child:Destroy()
		end
	end

	local torso = character:FindFirstChild("UpperTorso") or character:FindFirstChild("Torso")
	if not torso then return end

	local outfitColor = Color3.fromRGB(100, 100, 200) -- default blue gi
	if data.Appearance and data.Appearance.OutfitColor then
		local oc = data.Appearance.OutfitColor
		outfitColor = Color3.fromRGB(oc.R or 100, oc.G or 100, oc.B or 200)
	end

	-- Gi top (slightly larger than torso)
	local gi = Instance.new("Part")
	gi.Name = "OutfitPart"
	gi.Size = torso.Size + Vector3.new(0.1, 0.1, 0.1)
	gi.Color = outfitColor
	gi.Material = Enum.Material.Fabric
	gi.CanCollide = false
	gi.Anchored = false
	gi.Transparency = 0
	gi.Parent = character

	local weld = Instance.new("WeldConstraint")
	weld.Part0 = torso
	weld.Part1 = gi
	weld.Parent = gi

	gi.CFrame = torso.CFrame

	-- Belt
	local belt = Instance.new("Part")
	belt.Name = "OutfitPart"
	belt.Size = Vector3.new(torso.Size.X + 0.15, 0.3, torso.Size.Z + 0.15)
	belt.Color = Color3.fromRGB(40, 40, 40)
	belt.Material = Enum.Material.Fabric
	belt.CanCollide = false
	belt.Anchored = false
	belt.Parent = character

	local beltWeld = Instance.new("WeldConstraint")
	beltWeld.Part0 = torso
	beltWeld.Part1 = belt
	beltWeld.Parent = belt

	belt.CFrame = torso.CFrame * CFrame.new(0, -torso.Size.Y / 2 + 0.15, 0)

	-- Boots (on feet)
	for _, legName in ipairs({ "LeftFoot", "RightFoot", "Left Leg", "Right Leg" }) do
		local leg = character:FindFirstChild(legName)
		if leg then
			local boot = Instance.new("Part")
			boot.Name = "OutfitPart"
			boot.Size = leg.Size + Vector3.new(0.1, 0.1, 0.1)
			boot.Color = Color3.fromRGB(40, 40, 80)
			boot.Material = Enum.Material.Fabric
			boot.CanCollide = false
			boot.Anchored = false
			boot.Parent = character

			local bootWeld = Instance.new("WeldConstraint")
			bootWeld.Part0 = leg
			bootWeld.Part1 = boot
			bootWeld.Parent = boot

			boot.CFrame = leg.CFrame
		end
	end
end

-- Apply transformation visual changes
function CharacterBuilder.ApplyTransformation(player: Player, formId: string, data: any)
	local character = player.Character
	if not character then return end

	-- Re-apply hair with transformation override
	data.CurrentTransformation = formId
	CharacterBuilder.ApplyHair(character, data)

	-- Body color changes for certain forms
	local form = Transformations:Get(formId)
	if not form then return end

	if form.bodyChange then
		local head = character:FindFirstChild("Head")
		if form.bodyChange == "Kaioken_Glow" or form.bodyChange == "UI_Glow"
			or form.bodyChange == "PU_Glow" or form.bodyChange == "MaxPower_Glow" then
			-- Red/white tint on skin
			for _, part in ipairs(character:GetDescendants()) do
				if part:IsA("BasePart") and part.Name ~= "HumanoidRootPart" and part.Name ~= "OutfitPart" then
					if part.Name == "Head" or part.Name:find("Arm") or part.Name:find("Hand") then
						-- Don't change skin for glow, add a surface light instead
					end
				end
			end
		end

		if form.bodyChange == "Giant_Scale" then
			-- Scale up the character
			local humanoid = character:FindFirstChildOfClass("Humanoid")
			if humanoid then
				local bodyScale = humanoid:FindFirstChild("BodyHeightScale")
				if bodyScale then
					bodyScale.Value = 3
				end
				local bodyWidth = humanoid:FindFirstChild("BodyWidthScale")
				if bodyWidth then
					bodyWidth.Value = 2
				end
				local bodyDepth = humanoid:FindFirstChild("BodyDepthScale")
				if bodyDepth then
					bodyDepth.Value = 2
				end
			end
		end

		if form.bodyChange == "Frieza_Golden" then
			-- Turn body gold
			for _, part in ipairs(character:GetDescendants()) do
				if part:IsA("BasePart") and part.Name ~= "HumanoidRootPart" and part.Name ~= "OutfitPart" and part.Name ~= "Hair" then
					if part.Name == "Head" or part.Name == "UpperTorso" or part.Name == "LowerTorso"
						or part.Name:find("Arm") or part.Name:find("Leg")
						or part.Name:find("Hand") or part.Name:find("Foot") then
						part.Color = Color3.fromRGB(255, 215, 0)
						part.Material = Enum.Material.Neon
					end
				end
			end
		end

		if form.bodyChange == "Frieza_Black" then
			for _, part in ipairs(character:GetDescendants()) do
				if part:IsA("BasePart") and part.Name ~= "HumanoidRootPart" and part.Name ~= "OutfitPart" then
					if part.Name == "Head" or part.Name == "UpperTorso" or part.Name == "LowerTorso"
						or part.Name:find("Arm") or part.Name:find("Leg")
						or part.Name:find("Hand") or part.Name:find("Foot") then
						part.Color = Color3.fromRGB(30, 30, 30)
						part.Material = Enum.Material.Metal
					end
				end
			end
		end

		if form.bodyChange == "Orange_Body" then
			for _, part in ipairs(character:GetDescendants()) do
				if part:IsA("BasePart") and part.Name ~= "HumanoidRootPart" and part.Name ~= "OutfitPart" then
					if part.Name == "Head" or part.Name == "UpperTorso" or part.Name == "LowerTorso"
						or part.Name:find("Arm") or part.Name:find("Leg") then
						part.Color = Color3.fromRGB(255, 165, 50)
					end
				end
			end
		end
	end
end

return CharacterBuilder
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="70">
          <Properties>
            <string name="Name">DamageResolver</string>
            <string name="Source"><![CDATA[--[[
	DamageResolver.lua
	Resolves damage calculations for all combat interactions.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CombatUtil = require(ReplicatedStorage.Shared.Utils.CombatUtil)
local Settings = require(ReplicatedStorage.Shared.Constants.Settings)

local DamageResolver = {}

-- Resolve a melee hit
function DamageResolver.ResolveMelee(
	attackerStats: { [string]: number },
	defenderStats: { [string]: number },
	hitData: { damageMultiplier: number, knockback: Vector3, hitstun: number, guardBreak: boolean?, launcher: boolean? },
	isBlocking: boolean
): { damage: number, knockback: Vector3, hitstun: number, guardBreak: boolean, launcher: boolean, blocked: boolean }
	local rawDamage = CombatUtil.CalculateDamage(attackerStats, hitData.damageMultiplier, false)
	local blocked = isBlocking and not hitData.guardBreak

	local finalDamage = CombatUtil.ApplyDefense(
		rawDamage,
		defenderStats.Defense,
		blocked,
		Settings.Combat.BLOCK_DAMAGE_REDUCTION
	)

	return {
		damage = finalDamage,
		knockback = if blocked then Vector3.zero else hitData.knockback,
		hitstun = if blocked then 0.1 else hitData.hitstun,
		guardBreak = hitData.guardBreak and isBlocking,
		launcher = if blocked then false else (hitData.launcher or false),
		blocked = blocked,
	}
end

-- Resolve a ki blast hit
function DamageResolver.ResolveKiBlast(
	attackerStats: { [string]: number },
	defenderStats: { [string]: number },
	damageMultiplier: number,
	isBlocking: boolean
): { damage: number, blocked: boolean }
	local rawDamage = CombatUtil.CalculateDamage(attackerStats, damageMultiplier, true)

	local finalDamage = CombatUtil.ApplyDefense(
		rawDamage,
		defenderStats.Defense,
		isBlocking,
		Settings.Combat.BLOCK_DAMAGE_REDUCTION
	)

	return {
		damage = finalDamage,
		blocked = isBlocking,
	}
end

-- Resolve beam damage per tick
function DamageResolver.ResolveBeamTick(
	attackerStats: { [string]: number },
	defenderStats: { [string]: number },
	damagePerTick: number,
	isBlocking: boolean
): number
	local rawDamage = CombatUtil.CalculateDamage(attackerStats, damagePerTick, true)
	return CombatUtil.ApplyDefense(rawDamage, defenderStats.Defense, isBlocking, Settings.Combat.BLOCK_DAMAGE_REDUCTION)
end

-- Resolve ground/wall slam bonus
function DamageResolver.ResolveSlam(baseDamage: number): number
	return math.floor(baseDamage * Settings.Combat.GROUND_SLAM_BONUS)
end

return DamageResolver
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="71">
          <Properties>
            <string name="Name">EloCalculator</string>
            <string name="Source"><![CDATA[--[[
	EloCalculator.lua
	ELO rating calculations for PvP matchmaking.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Settings = require(ReplicatedStorage.Shared.Constants.Settings)

local EloCalculator = {}

function EloCalculator.Calculate(winnerElo: number, loserElo: number): (number, number)
	local K = Settings.PvP.ELO_K_FACTOR
	local floor = Settings.PvP.ELO_FLOOR

	local expectedWinner = 1 / (1 + 10 ^ ((loserElo - winnerElo) / 400))
	local expectedLoser = 1 - expectedWinner

	local newWinnerElo = math.floor(winnerElo + K * (1 - expectedWinner))
	local newLoserElo = math.max(floor, math.floor(loserElo + K * (0 - expectedLoser)))

	return newWinnerElo, newLoserElo
end

function EloCalculator.GetMatchQuality(elo1: number, elo2: number): number
	-- Returns 0-1 quality score (higher = better match)
	local diff = math.abs(elo1 - elo2)
	return math.max(0, 1 - (diff / 400))
end

return EloCalculator
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="72">
          <Properties>
            <string name="Name">HitValidator</string>
            <string name="Source"><![CDATA[--[[
	HitValidator.lua
	Server-side validation for all combat actions.
	Prevents exploits by checking range, cooldowns, state legality, etc.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CombatUtil = require(ReplicatedStorage.Shared.Utils.CombatUtil)
local Moves = require(ReplicatedStorage.Shared.Constants.Moves)
local Settings = require(ReplicatedStorage.Shared.Constants.Settings)

local HitValidator = {}

-- Cooldown tracking per player per move
local cooldowns: { [Player]: { [string]: number } } = {}

function HitValidator.InitPlayer(player: Player)
	cooldowns[player] = {}
end

function HitValidator.CleanupPlayer(player: Player)
	cooldowns[player] = nil
end

-- Check if a move is off cooldown
function HitValidator.IsOffCooldown(player: Player, moveId: string): boolean
	local playerCooldowns = cooldowns[player]
	if not playerCooldowns then
		return false
	end

	local lastUsed = playerCooldowns[moveId]
	if not lastUsed then
		return true
	end

	local move = Moves:Get(moveId)
	if not move then
		return false
	end

	return (tick() - lastUsed) >= move.cooldown
end

-- Record that a move was used
function HitValidator.RecordUse(player: Player, moveId: string)
	if cooldowns[player] then
		cooldowns[player][moveId] = tick()
	end
end

-- Validate a melee attack
function HitValidator.ValidateMelee(
	attackerCharacter: Model,
	targetCharacter: Model,
	moveId: string,
	attackerState: string
): (boolean, string?)
	-- State check
	local validStates = { Idle = true, Attacking = true }
	if not validStates[attackerState] then
		return false, "Invalid state for melee: " .. attackerState
	end

	-- Range check
	local attackerPos = attackerCharacter:GetPivot().Position
	local targetPos = targetCharacter:GetPivot().Position
	local move = Moves:Get(moveId)
	local range = if move then move.range else Settings.Combat.BASE_MELEE_RANGE

	if not CombatUtil.IsInRange(attackerPos, targetPos, range + 2) then -- +2 tolerance
		return false, "Out of range"
	end

	-- Cooldown check
	if not HitValidator.IsOffCooldown(attackerCharacter:FindFirstChild("Player") and game.Players:GetPlayerFromCharacter(attackerCharacter), moveId) then
		return false, "Move on cooldown"
	end

	return true, nil
end

-- Validate a ki blast
function HitValidator.ValidateKiBlast(
	player: Player,
	moveId: string,
	attackerState: string,
	currentKi: number
): (boolean, string?)
	local validStates = { Idle = true }
	if not validStates[attackerState] then
		return false, "Invalid state for ki blast: " .. attackerState
	end

	local move = Moves:Get(moveId)
	if not move then
		return false, "Unknown move"
	end

	local kiCost = move.kiCost or move.kiCostBase or 0
	if currentKi < kiCost then
		return false, "Not enough ki"
	end

	if not HitValidator.IsOffCooldown(player, moveId) then
		return false, "Move on cooldown"
	end

	return true, nil
end

-- Validate stamina usage (block, dodge)
function HitValidator.ValidateStaminaAction(
	attackerState: string,
	actionType: string,
	currentStamina: number
): (boolean, string?)
	if actionType == "Vanish" then
		if currentStamina < Settings.Combat.VANISH_STAMINA_COST then
			return false, "Not enough stamina"
		end
		local validStates = { Idle = true, Blocking = true, Stunned = true }
		if not validStates[attackerState] then
			return false, "Cannot vanish from state: " .. attackerState
		end
	elseif actionType == "Block" then
		local validStates = { Idle = true }
		if not validStates[attackerState] then
			return false, "Cannot block from state: " .. attackerState
		end
	end

	return true, nil
end

-- Speed/position sanity check
function HitValidator.ValidatePosition(
	character: Model,
	lastKnownPosition: Vector3,
	deltaTime: number,
	maxSpeed: number
): boolean
	local currentPos = character:GetPivot().Position
	local distance = (currentPos - lastKnownPosition).Magnitude
	local maxDistance = maxSpeed * deltaTime * 1.5 -- 1.5x tolerance

	return distance <= maxDistance
end

return HitValidator
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="73">
          <Properties>
            <string name="Name">LootTable</string>
            <string name="Source"><![CDATA[--[[
	LootTable.lua
	Weighted random loot drop system.
]]

local LootTable = {}

-- Roll a random item from a weighted loot table
-- entries: { { item = any, weight = number }, ... }
function LootTable.Roll(entries: { { item: any, weight: number } }): any
	local totalWeight = 0
	for _, entry in ipairs(entries) do
		totalWeight = totalWeight + entry.weight
	end

	local roll = math.random() * totalWeight
	local cumulative = 0

	for _, entry in ipairs(entries) do
		cumulative = cumulative + entry.weight
		if roll <= cumulative then
			return entry.item
		end
	end

	-- Fallback
	return entries[#entries].item
end

-- Roll multiple items (no duplicates)
function LootTable.RollMultiple(entries: { { item: any, weight: number } }, count: number): { any }
	local results = {}
	local remaining = table.clone(entries)

	for _ = 1, math.min(count, #remaining) do
		local item = LootTable.Roll(remaining)
		table.insert(results, item)

		-- Remove picked item
		for i, entry in ipairs(remaining) do
			if entry.item == item then
				table.remove(remaining, i)
				break
			end
		end
	end

	return results
end

return LootTable
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="74">
          <Properties>
            <string name="Name">NPCModelBuilder</string>
            <string name="Source"><![CDATA[--[[
	NPCModelBuilder.lua
	Creates NPC character models with visual appearance, health bars, and interaction prompts.
]]

local NPCModelBuilder = {}

-- NPC visual configs
local NPC_VISUALS = {
	TrainingDummy = {
		skinColor = Color3.fromRGB(180, 150, 100),
		outfitColor = Color3.fromRGB(200, 200, 200),
		bodyScale = 1,
		material = Enum.Material.Wood,
	},
	Saibaman = {
		skinColor = Color3.fromRGB(50, 180, 50),
		outfitColor = nil,
		bodyScale = 0.7,
		material = Enum.Material.SmoothPlastic,
	},
	Nappa = {
		skinColor = Color3.fromRGB(200, 170, 130),
		outfitColor = Color3.fromRGB(50, 50, 80),
		bodyScale = 1.3,
		bald = true,
		hasMoustache = true,
	},
	Vegeta_Saiyan = {
		skinColor = Color3.fromRGB(240, 210, 170),
		outfitColor = Color3.fromRGB(30, 30, 120),
		bodyScale = 1.0,
		hairColor = Color3.fromRGB(20, 20, 20),
		hairStyle = "Spiky",
		hasTail = true,
	},
	FriezaSoldier = {
		skinColor = Color3.fromRGB(200, 180, 200),
		outfitColor = Color3.fromRGB(60, 30, 80),
		bodyScale = 0.95,
	},
	Guldo = {
		skinColor = Color3.fromRGB(80, 200, 80),
		outfitColor = Color3.fromRGB(40, 40, 40),
		bodyScale = 0.6,
	},
	Recoome = {
		skinColor = Color3.fromRGB(240, 200, 170),
		outfitColor = Color3.fromRGB(40, 40, 40),
		bodyScale = 1.4,
		hairColor = Color3.fromRGB(255, 150, 50),
	},
	Burter = {
		skinColor = Color3.fromRGB(50, 80, 200),
		outfitColor = Color3.fromRGB(40, 40, 40),
		bodyScale = 1.2,
	},
	Jeice = {
		skinColor = Color3.fromRGB(255, 100, 80),
		outfitColor = Color3.fromRGB(40, 40, 40),
		bodyScale = 1.0,
		hairColor = Color3.fromRGB(255, 255, 255),
	},
	CaptainGinyu = {
		skinColor = Color3.fromRGB(120, 50, 180),
		outfitColor = Color3.fromRGB(40, 40, 40),
		bodyScale = 1.2,
		hasHorns = true,
	},
	Frieza_Final = {
		skinColor = Color3.fromRGB(240, 230, 250),
		outfitColor = nil,
		bodyScale = 0.85,
		accentColor = Color3.fromRGB(180, 50, 200),
		hasTail = true,
	},
	CellJunior = {
		skinColor = Color3.fromRGB(80, 150, 255),
		outfitColor = nil,
		bodyScale = 0.5,
	},
	PerfectCell = {
		skinColor = Color3.fromRGB(100, 180, 100),
		outfitColor = nil,
		bodyScale = 1.15,
		accentColor = Color3.fromRGB(40, 40, 40),
	},
	Dabura = {
		skinColor = Color3.fromRGB(200, 80, 80),
		outfitColor = Color3.fromRGB(50, 50, 120),
		bodyScale = 1.2,
		hasHorns = true,
	},
	Pui_Pui = {
		skinColor = Color3.fromRGB(180, 150, 200),
		outfitColor = Color3.fromRGB(60, 40, 80),
		bodyScale = 1.0,
	},
	Yakon = {
		skinColor = Color3.fromRGB(80, 150, 80),
		outfitColor = nil,
		bodyScale = 1.5,
	},
	KidBuu_Boss = {
		skinColor = Color3.fromRGB(255, 150, 180),
		outfitColor = nil,
		bodyScale = 0.9,
		hasHeadTentacle = true,
		outfitPantsColor = Color3.fromRGB(255, 255, 255),
	},
	FriezaSoldierElite = {
		skinColor = Color3.fromRGB(180, 200, 180),
		outfitColor = Color3.fromRGB(80, 30, 100),
		bodyScale = 1.05,
	},
	GoldenFrieza_Boss = {
		skinColor = Color3.fromRGB(255, 215, 0),
		outfitColor = nil,
		bodyScale = 0.9,
		material = Enum.Material.Neon,
		hasTail = true,
	},
	Whis = {
		skinColor = Color3.fromRGB(180, 200, 240),
		outfitColor = Color3.fromRGB(50, 50, 80),
		bodyScale = 1.15,
		hairColor = Color3.fromRGB(200, 220, 255),
	},
	Beerus = {
		skinColor = Color3.fromRGB(150, 100, 200),
		outfitColor = Color3.fromRGB(40, 40, 80),
		bodyScale = 1.0,
		hasEars = true,
	},
	UniverseFighter = {
		skinColor = Color3.fromRGB(200, 180, 160),
		outfitColor = Color3.fromRGB(math.random(50, 200), math.random(50, 200), math.random(50, 200)),
		bodyScale = 1.0,
	},
	Jiren = {
		skinColor = Color3.fromRGB(180, 180, 180),
		outfitColor = Color3.fromRGB(200, 30, 30),
		bodyScale = 1.3,
		bald = true,
	},
	Broly_FullPower = {
		skinColor = Color3.fromRGB(220, 200, 160),
		outfitColor = Color3.fromRGB(100, 50, 150),
		bodyScale = 1.6,
		hairColor = Color3.fromRGB(50, 200, 50),
	},
	Botamo = {
		skinColor = Color3.fromRGB(255, 220, 50),
		outfitColor = nil,
		bodyScale = 1.4,
	},
	Frost = {
		skinColor = Color3.fromRGB(200, 220, 240),
		outfitColor = nil,
		bodyScale = 0.85,
		hasTail = true,
	},
	Magetta = {
		skinColor = Color3.fromRGB(150, 150, 150),
		outfitColor = nil,
		bodyScale = 1.6,
		material = Enum.Material.Metal,
	},
	Cabba = {
		skinColor = Color3.fromRGB(240, 210, 170),
		outfitColor = Color3.fromRGB(80, 80, 120),
		bodyScale = 0.85,
		hairColor = Color3.fromRGB(20, 20, 20),
	},
	Hit = {
		skinColor = Color3.fromRGB(150, 120, 200),
		outfitColor = Color3.fromRGB(40, 30, 80),
		bodyScale = 1.1,
		bald = true,
	},
	Raditz = {
		skinColor = Color3.fromRGB(230, 200, 160),
		outfitColor = Color3.fromRGB(30, 30, 80),
		bodyScale = 1.2,
		hairColor = Color3.fromRGB(20, 20, 20),
		hairStyle = "Long",
		hasTail = true,
	},
}

function NPCModelBuilder.Build(npcId: string, displayName: string, hp: number, position: Vector3, isBoss: boolean?): Model
	local visuals = NPC_VISUALS[npcId] or {
		skinColor = Color3.fromRGB(200, 180, 160),
		outfitColor = Color3.fromRGB(100, 100, 100),
		bodyScale = 1.0,
	}

	local scale = visuals.bodyScale or 1
	local mat = visuals.material or Enum.Material.SmoothPlastic

	local model = Instance.new("Model")
	model.Name = displayName

	-- HumanoidRootPart (invisible anchor)
	local rootPart = Instance.new("Part")
	rootPart.Name = "HumanoidRootPart"
	rootPart.Size = Vector3.new(2 * scale, 2 * scale, 1 * scale)
	rootPart.Transparency = 1
	rootPart.CanCollide = false
	rootPart.Anchored = false
	rootPart.Position = position + Vector3.new(0, 3 * scale, 0)
	rootPart.Parent = model

	-- Torso
	local torso = Instance.new("Part")
	torso.Name = "Torso"
	torso.Size = Vector3.new(2 * scale, 2 * scale, 1 * scale)
	torso.Color = visuals.outfitColor or visuals.skinColor
	torso.Material = mat
	torso.Anchored = false
	torso.CanCollide = true
	torso.Parent = model

	local torsoWeld = Instance.new("WeldConstraint")
	torsoWeld.Part0 = rootPart
	torsoWeld.Part1 = torso
	torsoWeld.Parent = torso
	torso.CFrame = rootPart.CFrame

	-- Head
	local head = Instance.new("Part")
	head.Name = "Head"
	head.Size = Vector3.new(1.2 * scale, 1.2 * scale, 1.2 * scale)
	head.Shape = Enum.PartType.Ball
	head.Color = visuals.skinColor
	head.Material = mat
	head.Anchored = false
	head.CanCollide = false
	head.Parent = model

	local headWeld = Instance.new("WeldConstraint")
	headWeld.Part0 = torso
	headWeld.Part1 = head
	headWeld.Parent = head
	head.CFrame = torso.CFrame * CFrame.new(0, 1.5 * scale, 0)

	-- Face
	local face = Instance.new("Decal")
	face.Name = "Face"
	face.Face = Enum.NormalId.Front
	face.Parent = head

	-- Eyes (simple parts)
	for _, xOff in ipairs({ -0.2, 0.2 }) do
		local eye = Instance.new("Part")
		eye.Name = "Eye"
		eye.Size = Vector3.new(0.15 * scale, 0.15 * scale, 0.05 * scale)
		eye.Shape = Enum.PartType.Ball
		eye.Color = Color3.new(0, 0, 0)
		eye.Material = Enum.Material.Neon
		eye.Anchored = false
		eye.CanCollide = false
		eye.Parent = model

		local eyeWeld = Instance.new("WeldConstraint")
		eyeWeld.Part0 = head
		eyeWeld.Part1 = eye
		eyeWeld.Parent = eye
		eye.CFrame = head.CFrame * CFrame.new(xOff * scale, 0.1 * scale, 0.55 * scale)
	end

	-- Arms
	for _, side in ipairs({ -1, 1 }) do
		local arm = Instance.new("Part")
		arm.Name = side == -1 and "Left Arm" or "Right Arm"
		arm.Size = Vector3.new(0.6 * scale, 2 * scale, 0.6 * scale)
		arm.Color = visuals.skinColor
		arm.Material = mat
		arm.Anchored = false
		arm.CanCollide = false
		arm.Parent = model

		local armWeld = Instance.new("WeldConstraint")
		armWeld.Part0 = torso
		armWeld.Part1 = arm
		armWeld.Parent = arm
		arm.CFrame = torso.CFrame * CFrame.new(side * 1.3 * scale, 0, 0)
	end

	-- Legs
	for _, side in ipairs({ -1, 1 }) do
		local leg = Instance.new("Part")
		leg.Name = side == -1 and "Left Leg" or "Right Leg"
		leg.Size = Vector3.new(0.7 * scale, 2 * scale, 0.7 * scale)
		leg.Color = visuals.outfitColor or visuals.skinColor
		leg.Material = mat
		leg.Anchored = false
		leg.CanCollide = false
		leg.Parent = model

		local legWeld = Instance.new("WeldConstraint")
		legWeld.Part0 = torso
		legWeld.Part1 = leg
		legWeld.Parent = leg
		leg.CFrame = torso.CFrame * CFrame.new(side * 0.5 * scale, -2 * scale, 0)
	end

	-- Humanoid
	local humanoid = Instance.new("Humanoid")
	humanoid.MaxHealth = hp
	humanoid.Health = hp
	humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.Viewer
	humanoid.HealthDisplayDistance = 100
	humanoid.NameDisplayDistance = 100
	humanoid.Parent = model

	model.PrimaryPart = rootPart

	-- Race features
	if visuals.hasTail then
		local tail = Instance.new("Part")
		tail.Name = "Tail"
		tail.Size = Vector3.new(0.25 * scale, 0.25 * scale, 1.5 * scale)
		tail.Color = visuals.skinColor
		tail.Material = mat
		tail.CanCollide = false
		tail.Anchored = false
		tail.Parent = model

		local tailWeld = Instance.new("WeldConstraint")
		tailWeld.Part0 = torso
		tailWeld.Part1 = tail
		tailWeld.Parent = tail
		tail.CFrame = torso.CFrame * CFrame.new(0, -0.5 * scale, -0.8 * scale)
	end

	if visuals.hasHorns then
		for _, xOff in ipairs({ -0.3, 0.3 }) do
			local horn = Instance.new("Part")
			horn.Name = "Horn"
			horn.Size = Vector3.new(0.15 * scale, 0.5 * scale, 0.15 * scale)
			horn.Color = Color3.fromRGB(220, 220, 230)
			horn.Material = Enum.Material.SmoothPlastic
			horn.CanCollide = false
			horn.Anchored = false
			horn.Parent = model

			local hornWeld = Instance.new("WeldConstraint")
			hornWeld.Part0 = head
			hornWeld.Part1 = horn
			hornWeld.Parent = horn
			horn.CFrame = head.CFrame * CFrame.new(xOff * scale, 0.5 * scale, 0)
		end
	end

	if visuals.hasHeadTentacle then
		local tent = Instance.new("Part")
		tent.Name = "HeadTentacle"
		tent.Size = Vector3.new(0.3 * scale, 0.3 * scale, 1.5 * scale)
		tent.Color = visuals.skinColor
		tent.Material = mat
		tent.CanCollide = false
		tent.Anchored = false
		tent.Parent = model

		local tentWeld = Instance.new("WeldConstraint")
		tentWeld.Part0 = head
		tentWeld.Part1 = tent
		tentWeld.Parent = tent
		tent.CFrame = head.CFrame * CFrame.new(0, 0.4 * scale, -0.5 * scale) * CFrame.Angles(math.rad(-50), 0, 0)
	end

	if visuals.hasEars then
		for _, xOff in ipairs({ -0.5, 0.5 }) do
			local ear = Instance.new("Part")
			ear.Name = "Ear"
			ear.Size = Vector3.new(0.15 * scale, 0.6 * scale, 0.3 * scale)
			ear.Color = visuals.skinColor
			ear.Material = mat
			ear.CanCollide = false
			ear.Anchored = false
			ear.Parent = model

			local earWeld = Instance.new("WeldConstraint")
			earWeld.Part0 = head
			earWeld.Part1 = ear
			earWeld.Parent = horn
			ear.CFrame = head.CFrame * CFrame.new(xOff * scale, 0.5 * scale, 0)
		end
	end

	-- Hair
	if visuals.hairColor and not visuals.bald then
		local hairParts = {
			{ size = Vector3.new(1.2, 0.5, 1.2), offset = Vector3.new(0, 0.6, 0) },
			{ size = Vector3.new(0.4, 0.8, 0.3), offset = Vector3.new(0.3, 0.9, 0) },
			{ size = Vector3.new(0.4, 0.9, 0.3), offset = Vector3.new(-0.3, 0.95, 0) },
		}
		for i, def in ipairs(hairParts) do
			local hairPart = Instance.new("Part")
			hairPart.Name = "Hair_" .. i
			hairPart.Size = def.size * scale
			hairPart.Color = visuals.hairColor
			hairPart.Material = Enum.Material.SmoothPlastic
			hairPart.CanCollide = false
			hairPart.Anchored = false
			hairPart.Parent = model

			local hairWeld = Instance.new("WeldConstraint")
			hairWeld.Part0 = head
			hairWeld.Part1 = hairPart
			hairWeld.Parent = hairPart
			hairPart.CFrame = head.CFrame * CFrame.new(def.offset * scale)
		end
	end

	-- Health bar billboard
	local billboard = Instance.new("BillboardGui")
	billboard.Name = "HealthDisplay"
	billboard.Size = UDim2.new(4, 0, 0.5, 0)
	billboard.StudsOffset = Vector3.new(0, 3 * scale, 0)
	billboard.AlwaysOnTop = true
	billboard.Parent = head

	local nameLabel = Instance.new("TextLabel")
	nameLabel.Size = UDim2.new(1, 0, 0.5, 0)
	nameLabel.Position = UDim2.new(0, 0, 0, 0)
	nameLabel.BackgroundTransparency = 1
	nameLabel.Text = displayName .. (isBoss and " [BOSS]" or "")
	nameLabel.TextColor3 = isBoss and Color3.fromRGB(255, 50, 50) or Color3.new(1, 1, 1)
	nameLabel.TextStrokeTransparency = 0
	nameLabel.TextScaled = true
	nameLabel.Font = Enum.Font.GothamBold
	nameLabel.Parent = billboard

	local hpBg = Instance.new("Frame")
	hpBg.Size = UDim2.new(1, 0, 0.3, 0)
	hpBg.Position = UDim2.new(0, 0, 0.6, 0)
	hpBg.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
	hpBg.BorderSizePixel = 0
	hpBg.Parent = billboard

	local hpBar = Instance.new("Frame")
	hpBar.Name = "HPBar"
	hpBar.Size = UDim2.fromScale(1, 1)
	hpBar.BackgroundColor3 = isBoss and Color3.fromRGB(255, 50, 50) or Color3.fromRGB(50, 255, 50)
	hpBar.BorderSizePixel = 0
	hpBar.Parent = hpBg

	Instance.new("UICorner", hpBg).CornerRadius = UDim.new(0, 3)
	Instance.new("UICorner", hpBar).CornerRadius = UDim.new(0, 3)

	-- ProximityPrompt for interaction
	local prompt = Instance.new("ProximityPrompt")
	prompt.ActionText = "Fight"
	prompt.ObjectText = displayName
	prompt.HoldDuration = 0
	prompt.MaxActivationDistance = 15
	prompt.Parent = rootPart

	-- Boss aura
	if isBoss then
		local aura = Instance.new("ParticleEmitter")
		aura.Color = ColorSequence.new(Color3.fromRGB(255, 50, 50))
		aura.Size = NumberSequence.new({ NumberSequenceKeypoint.new(0, 3 * scale), NumberSequenceKeypoint.new(1, 0) })
		aura.Lifetime = NumberRange.new(0.3, 0.6)
		aura.Rate = 30
		aura.Speed = NumberRange.new(3, 6)
		aura.SpreadAngle = Vector2.new(180, 180)
		aura.LightEmission = 1
		aura.Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.5), NumberSequenceKeypoint.new(1, 1) })
		aura.Parent = rootPart

		local bossLight = Instance.new("PointLight")
		bossLight.Color = Color3.fromRGB(255, 50, 50)
		bossLight.Brightness = 1.5
		bossLight.Range = 20 * scale
		bossLight.Parent = rootPart
	end

	-- Anchor the whole model for now (NPCService manages AI)
	rootPart.Anchored = true
	for _, part in ipairs(model:GetDescendants()) do
		if part:IsA("BasePart") and part ~= rootPart then
			part.Anchored = false
		end
	end

	return model
end

return NPCModelBuilder
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="75">
          <Properties>
            <string name="Name">StatCalculator</string>
            <string name="Source"><![CDATA[--[[
	StatCalculator.lua
	Pure functions for calculating effective player stats.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Stats = require(ReplicatedStorage.Shared.Constants.Stats)
local Transformations = require(ReplicatedStorage.Shared.Constants.Transformations)
local Races = require(ReplicatedStorage.Shared.Constants.Races)

local StatCalculator = {}

-- Calculate effective stats considering all modifiers
function StatCalculator.Calculate(
	baseStats: { [string]: number },
	level: number,
	allocatedPoints: { [string]: number },
	currentTransformation: string,
	raceName: string,
	rebirths: number
): { [string]: number }
	local effective = {}

	for _, statName in ipairs(Stats.Names) do
		local base = baseStats[statName] or 0
		local allocated = (allocatedPoints[statName] or 0) * (Stats.POINT_MULTIPLIERS[statName] or 1)
		local levelGrowth = (Stats.LEVEL_GROWTH[statName] or 0) * (level - 1)

		effective[statName] = base + allocated + levelGrowth
	end

	-- Apply race passive stat bonus (if applicable)
	local race = Races:Get(raceName)
	if race and race.passive.id == "Adaptability" then
		local bonus = race.passive.statPointBonus
		for statName in pairs(allocatedPoints) do
			local allocated = (allocatedPoints[statName] or 0) * (Stats.POINT_MULTIPLIERS[statName] or 1)
			effective[statName] = effective[statName] + (allocated * bonus)
		end
	end

	-- Apply rebirth bonus
	if rebirths > 0 then
		local rebirthMult = 1 + (rebirths * Stats.REBIRTH_BONUS_PERCENT)
		for _, statName in ipairs(Stats.Names) do
			effective[statName] = effective[statName] * rebirthMult
		end
	end

	-- Apply transformation multipliers
	if currentTransformation ~= "Base" then
		local form = Transformations:Get(currentTransformation)
		if form and form.statMultipliers then
			for statName, mult in pairs(form.statMultipliers) do
				if effective[statName] then
					effective[statName] = effective[statName] * mult
				end
			end
		end
	end

	-- Floor all values
	for statName, value in pairs(effective) do
		effective[statName] = math.floor(value)
	end

	return effective
end

-- Calculate max HP from effective Health stat
function StatCalculator.GetMaxHP(effectiveHealth: number): number
	return effectiveHealth
end

-- Calculate max Ki (base 100 + KiPower scaling)
function StatCalculator.GetMaxKi(effectiveKiPower: number): number
	return 100 + math.floor(effectiveKiPower * 2)
end

-- Calculate max Stamina
function StatCalculator.GetMaxStamina(effectiveStamina: number): number
	return effectiveStamina
end

-- Calculate movement speed multiplier
function StatCalculator.GetSpeedMultiplier(effectiveSpeed: number): number
	return 1.0 + (effectiveSpeed / 200) -- Diminishing scaling
end

return StatCalculator
]]></string>
          </Properties>
        </Item>
      </Item>
      <Item class="Folder" referent="76">
        <Properties>
          <string name="Name">Services</string>
        </Properties>
        <Item class="ModuleScript" referent="77">
          <Properties>
            <string name="Name">CharacterService</string>
            <string name="Source"><![CDATA[--[[
	CharacterService.lua
	Manages player character appearance, spawning, and visual updates.
	Applies appearance on spawn and after transformations.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local CharacterBuilder = require(script.Parent.Parent.Modules.CharacterBuilder)

local CharacterService = Knit.CreateService({
	Name = "CharacterService",
	Client = {},
})

function CharacterService:KnitInit() end

function CharacterService:KnitStart()
	local DataService = Knit.GetService("DataService")

	-- Apply appearance when character spawns
	Players.PlayerAdded:Connect(function(player)
		player.CharacterAdded:Connect(function(character)
			-- Wait for character to fully load
			local humanoid = character:WaitForChild("Humanoid", 10)
			if not humanoid then return end
			humanoid:WaitForChild("Animator", 5)

			-- Small delay for everything to replicate
			task.wait(0.5)

			local data = DataService:GetData(player)
			if data and data.CharacterCreated then
				CharacterBuilder.ApplyAppearance(player, data)
			end
		end)
	end)

	-- Also handle players already connected
	for _, player in ipairs(Players:GetPlayers()) do
		if player.Character then
			local data = DataService:GetData(player)
			if data and data.CharacterCreated then
				CharacterBuilder.ApplyAppearance(player, data)
			end
		end
	end
end

function CharacterService:RefreshAppearance(player: Player)
	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if data then
		CharacterBuilder.ApplyAppearance(player, data)
	end
end

function CharacterService:ApplyTransformation(player: Player, formId: string)
	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if data then
		CharacterBuilder.ApplyTransformation(player, formId, data)
	end
end

return CharacterService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="78">
          <Properties>
            <string name="Name">CombatService</string>
            <string name="Source"><![CDATA[--[[
	CombatService.lua
	Server-authoritative combat system.
	Receives client intents, validates, resolves damage, broadcasts results.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Signal = require(ReplicatedStorage.Packages.Signal)

local Moves = require(ReplicatedStorage.Shared.Constants.Moves)
local Settings = require(ReplicatedStorage.Shared.Constants.Settings)
local CombatUtil = require(ReplicatedStorage.Shared.Utils.CombatUtil)

local HitValidator = require(script.Parent.Parent.Modules.HitValidator)
local DamageResolver = require(script.Parent.Parent.Modules.DamageResolver)
local StatCalculator = require(script.Parent.Parent.Modules.StatCalculator)

local CombatService = Knit.CreateService({
	Name = "CombatService",
	Client = {
		CombatResult = Knit.CreateSignal(),
		BeamStruggleSync = Knit.CreateSignal(),
	},
})

-- Per-player combat state
local combatStates: { [Player]: {
	state: string,
	hp: number,
	maxHp: number,
	ki: number,
	maxKi: number,
	stamina: number,
	maxStamina: number,
	effectiveStats: { [string]: number },
	comboCount: number,
	lastCombatTime: number,
	lastPosition: Vector3,
} } = {}

-- Active beam struggles
local beamStruggles: { [string]: {
	player1: Player,
	player2: Player,
	player1Taps: number,
	player2Taps: number,
	startTime: number,
} } = {}

function CombatService:KnitInit()
	-- Nothing to init
end

function CombatService:KnitStart()
	local DataService = Knit.GetService("DataService")

	-- Initialize combat state when profile loads
	DataService.ProfileLoaded:Connect(function(player, data)
		self:_initCombatState(player, data)
	end)

	DataService.ProfileReleased:Connect(function(player)
		self:_cleanupPlayer(player)
	end)

	-- Listen for combat intents from clients
	self.Client.CombatResult:Connect(function() end) -- Server->Client only

	-- Main game loop: regen + sync + beam struggles
	RunService.Heartbeat:Connect(function(dt)
		self:_tickRegeneration(dt)
		self:_tickKiCharging(dt)
		self:_tickBeamStruggles(dt)
		self:_syncCombatStateToClients()
		self:_syncHumanoidHealth()
	end)
end

function CombatService:_initCombatState(player: Player, data: any)
	HitValidator.InitPlayer(player)

	local effectiveStats = StatCalculator.Calculate(
		data.Stats,
		data.Level,
		data.Stats,
		data.CurrentTransformation,
		data.Race,
		data.Rebirths
	)

	combatStates[player] = {
		state = "Idle",
		hp = StatCalculator.GetMaxHP(effectiveStats.Health),
		maxHp = StatCalculator.GetMaxHP(effectiveStats.Health),
		ki = StatCalculator.GetMaxKi(effectiveStats.KiPower),
		maxKi = StatCalculator.GetMaxKi(effectiveStats.KiPower),
		stamina = StatCalculator.GetMaxStamina(effectiveStats.Stamina),
		maxStamina = StatCalculator.GetMaxStamina(effectiveStats.Stamina),
		effectiveStats = effectiveStats,
		comboCount = 0,
		lastCombatTime = 0,
		lastPosition = Vector3.zero,
	}
end

function CombatService:_cleanupPlayer(player: Player)
	combatStates[player] = nil
	HitValidator.CleanupPlayer(player)
end

function CombatService:_tickRegeneration(dt: number)
	local now = tick()
	for player, combat in pairs(combatStates) do
		local outOfCombat = (now - combat.lastCombatTime) > Settings.Combat.COMBAT_TIMEOUT

		-- Stamina regen: slow in combat (3/sec), fast out of combat (15/sec)
		if combat.stamina < combat.maxStamina then
			local regenRate = outOfCombat and 15 or 3
			combat.stamina = math.min(combat.maxStamina, combat.stamina + regenRate * dt)
		end

		-- HP regen: only out of combat, slow (1% per sec) — and Namekian passive
		if outOfCombat and combat.hp < combat.maxHp then
			local hpRegen = 0
			local DataService = Knit.GetService("DataService")
			local data = DataService:GetData(player)
			if data then
				local Races = require(ReplicatedStorage.Shared.Constants.Races)
				local race = Races:Get(data.Race)
				if race and race.passive.id == "Regeneration" then
					hpRegen = combat.maxHp * race.passive.regenPercent * dt
				end
			end
			-- Base small HP regen out of combat for everyone
			hpRegen = hpRegen + combat.maxHp * 0.005 * dt
			combat.hp = math.min(combat.maxHp, combat.hp + hpRegen)
		end

		-- Ki auto-regen for Androids
		local DataService = Knit.GetService("DataService")
		local data = DataService:GetData(player)
		if data and data.Race == "Android" then
			local Races = require(ReplicatedStorage.Shared.Constants.Races)
			local race = Races:Get("Android")
			if race then
				combat.ki = math.min(combat.maxKi, combat.ki + combat.maxKi * race.passive.kiRegenPercent * dt)
			end
		end
	end
end

-- Ki charging: when state is "Charging", add ki per tick
function CombatService:_tickKiCharging(dt: number)
	for player, combat in pairs(combatStates) do
		if combat.state == "Charging" and combat.ki < combat.maxKi then
			combat.ki = math.min(combat.maxKi, combat.ki + Settings.Combat.KI_CHARGE_RATE * dt)
		end
	end
end

-- Sync combat HP/Ki/Stamina to each player's client every frame
function CombatService:_syncCombatStateToClients()
	for player, combat in pairs(combatStates) do
		if player and player.Parent then
			self.Client.CombatResult:Fire(player, {
				type = "state_sync",
				hp = combat.hp,
				maxHp = combat.maxHp,
				ki = combat.ki,
				maxKi = combat.maxKi,
				stamina = combat.stamina,
				maxStamina = combat.maxStamina,
				state = combat.state,
			})
		end
	end
end

-- Keep Humanoid.Health in sync so the character actually dies/respawns properly
function CombatService:_syncHumanoidHealth()
	for player, combat in pairs(combatStates) do
		local character = player.Character
		if not character then continue end
		local humanoid = character:FindFirstChildOfClass("Humanoid")
		if not humanoid then continue end

		-- Scale our HP to Humanoid's 100-based system
		local hpPercent = if combat.maxHp > 0 then combat.hp / combat.maxHp else 0
		humanoid.MaxHealth = combat.maxHp
		humanoid.Health = combat.hp

		-- If humanoid dies externally (fell off map, etc.), sync back
		if humanoid.Health <= 0 and combat.hp > 0 then
			combat.hp = 0
			combat.state = "Dead"
		end
	end
end

function CombatService:_tickBeamStruggles(dt: number)
	local now = tick()
	for id, struggle in pairs(beamStruggles) do
		if (now - struggle.startTime) >= Settings.Combat.BEAM_STRUGGLE_DURATION then
			self:_resolveBeamStruggle(id)
		end
	end
end

function CombatService:_resolveBeamStruggle(struggleId: string)
	local struggle = beamStruggles[struggleId]
	if not struggle then return end

	local state1 = combatStates[struggle.player1]
	local state2 = combatStates[struggle.player2]

	if not state1 or not state2 then
		beamStruggles[struggleId] = nil
		return
	end

	local winner = CombatUtil.ResolveBeamStruggle(
		state1.effectiveStats.KiPower,
		struggle.player1Taps,
		state2.effectiveStats.KiPower,
		struggle.player2Taps,
		Settings.Combat.BEAM_STRUGGLE_TAP_WEIGHT
	)

	local loser = if winner == 1 then struggle.player2 else struggle.player1
	local winnerPlayer = if winner == 1 then struggle.player1 else struggle.player2
	local winnerState = combatStates[winnerPlayer]
	local loserState = combatStates[loser]

	-- Loser takes big damage
	local damage = math.floor(winnerState.effectiveStats.KiPower * 5)
	loserState.hp = math.max(0, loserState.hp - damage)
	loserState.state = "Knocked"

	-- Reset states
	winnerState.state = "Idle"

	-- Broadcast result
	self.Client.BeamStruggleSync:Fire(struggle.player1, {
		winner = winner,
		damage = damage,
	})
	self.Client.BeamStruggleSync:Fire(struggle.player2, {
		winner = winner,
		damage = damage,
	})

	beamStruggles[struggleId] = nil

	-- Check death
	if loserState.hp <= 0 then
		self:_handleDeath(loser, winnerPlayer)
	end
end

function CombatService:_handleDeath(deadPlayer: Player, killerPlayer: Player?)
	local state = combatStates[deadPlayer]
	if state then
		state.state = "Dead"
	end

	-- Respawn after delay
	task.delay(5, function()
		if deadPlayer and deadPlayer:IsDescendantOf(Players) then
			local character = deadPlayer.Character
			if character then
				local humanoid = character:FindFirstChildOfClass("Humanoid")
				if humanoid then
					humanoid.Health = humanoid.MaxHealth
				end
			end
			if state then
				state.hp = state.maxHp
				state.ki = state.maxKi
				state.stamina = state.maxStamina
				state.state = "Idle"
			end
		end
	end)

	-- Award XP/Zeni to killer
	if killerPlayer then
		local DataService = Knit.GetService("DataService")
		local ProgressionService = Knit.GetService("ProgressionService")
		if ProgressionService then
			-- Will be connected when ProgressionService exists
		end
	end
end

-- Public API

function CombatService:GetCombatState(player: Player)
	return combatStates[player]
end

function CombatService:SetState(player: Player, newState: string)
	local combat = combatStates[player]
	if combat then
		combat.state = newState
	end
end

function CombatService:RefreshStats(player: Player)
	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if not data then return end

	local combat = combatStates[player]
	if not combat then return end

	local effectiveStats = StatCalculator.Calculate(
		data.Stats,
		data.Level,
		data.Stats,
		data.CurrentTransformation,
		data.Race,
		data.Rebirths
	)

	combat.effectiveStats = effectiveStats
	combat.maxHp = StatCalculator.GetMaxHP(effectiveStats.Health)
	combat.maxKi = StatCalculator.GetMaxKi(effectiveStats.KiPower)
	combat.maxStamina = StatCalculator.GetMaxStamina(effectiveStats.Stamina)
end

function CombatService:DealDamage(target: Player, damage: number, knockback: Vector3?, hitstun: number?)
	local state = combatStates[target]
	if not state then return end

	state.hp = math.max(0, state.hp - damage)
	state.lastCombatTime = tick()

	if hitstun and hitstun > 0 then
		state.state = "Stunned"
		task.delay(hitstun, function()
			if combatStates[target] and combatStates[target].state == "Stunned" then
				combatStates[target].state = "Idle"
			end
		end)
	end

	-- Apply knockback to character
	if knockback and knockback.Magnitude > 0 then
		local character = target.Character
		if character then
			local rootPart = character:FindFirstChild("HumanoidRootPart")
			if rootPart then
				local bv = Instance.new("BodyVelocity")
				bv.Velocity = knockback
				bv.MaxForce = Vector3.new(1e6, 1e6, 1e6)
				bv.P = 1e4
				bv.Parent = rootPart
				task.delay(0.2, function()
					bv:Destroy()
				end)
			end
		end
	end

	-- Check death
	if state.hp <= 0 then
		self:_handleDeath(target, nil)
	end
end

-- Client intent handlers

function CombatService.Client:MeleeAttack(player: Player, targetPlayer: Player, moveId: string)
	local combat = combatStates[player]
	if not combat then return end

	local attackerChar = player.Character
	local targetChar = if targetPlayer then targetPlayer.Character else nil
	if not attackerChar or not targetChar then return end

	local valid, err = HitValidator.ValidateMelee(attackerChar, targetChar, moveId, combat.state)
	if not valid then return end

	local move = Moves:Get(moveId)
	if not move or not move.hits then return end

	HitValidator.RecordUse(player, moveId)
	combat.state = "Attacking"
	combat.lastCombatTime = tick()

	local targetCombat = combatStates[targetPlayer]
	if not targetCombat then return end
	targetCombat.lastCombatTime = tick()

	local isBlocking = targetCombat.state == "Blocking"

	-- Process each hit in the combo
	for i, hitData in ipairs(move.hits) do
		task.delay(hitData.frame / 60, function()
			if not combatStates[player] or not combatStates[targetPlayer] then return end

			local result = DamageResolver.ResolveMelee(
				combat.effectiveStats,
				targetCombat.effectiveStats,
				hitData,
				targetCombat.state == "Blocking"
			)

			-- Apply knockback direction
			local direction = CombatUtil.GetKnockbackDirection(
				attackerChar:GetPivot().Position,
				targetChar:GetPivot().Position
			)
			local worldKnockback = direction * result.knockback.Z + Vector3.new(0, result.knockback.Y, 0)

			self.Server:DealDamage(targetPlayer, result.damage, worldKnockback, result.hitstun)

			-- Launcher
			if result.launcher then
				targetCombat.state = "Aerial"
			end

			-- Guard break
			if result.guardBreak then
				targetCombat.state = "Stunned"
				task.delay(Settings.Combat.GUARD_BREAK_STUN, function()
					if combatStates[targetPlayer] and combatStates[targetPlayer].state == "Stunned" then
						combatStates[targetPlayer].state = "Idle"
					end
				end)
			end

			-- Broadcast to both players
			self.Server.Client.CombatResult:Fire(player, {
				type = "melee",
				hitIndex = i,
				target = targetPlayer,
				damage = result.damage,
				blocked = result.blocked,
				guardBreak = result.guardBreak,
				launcher = result.launcher,
			})
			self.Server.Client.CombatResult:Fire(targetPlayer, {
				type = "melee_received",
				hitIndex = i,
				attacker = player,
				damage = result.damage,
				blocked = result.blocked,
				guardBreak = result.guardBreak,
				launcher = result.launcher,
			})
		end)
	end

	-- Return to idle after combo
	local lastHit = move.hits[#move.hits]
	task.delay((lastHit.frame / 60) + 0.5, function()
		if combatStates[player] and combatStates[player].state == "Attacking" then
			combatStates[player].state = "Idle"
		end
	end)
end

function CombatService.Client:KiBlast(player: Player, direction: Vector3, moveId: string)
	local combat = combatStates[player]
	if not combat then return end

	local valid, err = HitValidator.ValidateKiBlast(player, moveId, combat.state, combat.ki)
	if not valid then return end

	local move = Moves:Get(moveId)
	if not move then return end

	-- Deduct ki
	local kiCost = move.kiCost or move.kiCostBase or 0
	combat.ki = combat.ki - kiCost
	combat.lastCombatTime = tick()

	HitValidator.RecordUse(player, moveId)

	-- Broadcast ki blast creation to all clients
	self.Server.Client.CombatResult:FireAll({
		type = "ki_blast",
		attacker = player,
		moveId = moveId,
		direction = direction,
		origin = player.Character and player.Character:GetPivot().Position or Vector3.zero,
	})
end

function CombatService.Client:Block(player: Player, isBlocking: boolean)
	local combat = combatStates[player]
	if not combat then return end

	if isBlocking then
		if combat.state == "Idle" then
			combat.state = "Blocking"
		end
	else
		if combat.state == "Blocking" then
			combat.state = "Idle"
		end
	end
end

function CombatService.Client:Vanish(player: Player)
	local combat = combatStates[player]
	if not combat then return end

	local valid, err = HitValidator.ValidateStaminaAction(combat.state, "Vanish", combat.stamina)
	if not valid then return end

	combat.stamina = combat.stamina - Settings.Combat.VANISH_STAMINA_COST
	combat.state = "Dodging"

	-- I-frames
	task.delay(Settings.Combat.VANISH_I_FRAMES, function()
		if combatStates[player] and combatStates[player].state == "Dodging" then
			combatStates[player].state = "Idle"
		end
	end)

	-- Broadcast vanish to clients
	self.Server.Client.CombatResult:FireAll({
		type = "vanish",
		player = player,
	})
end

function CombatService.Client:ChargeKi(player: Player, isCharging: boolean)
	local combat = combatStates[player]
	if not combat then return end

	if isCharging then
		if combat.state == "Idle" then
			combat.state = "Charging"
		end
	else
		if combat.state == "Charging" then
			combat.state = "Idle"
		end
	end
end

function CombatService.Client:BeamStruggleTap(player: Player, struggleId: string)
	local struggle = beamStruggles[struggleId]
	if not struggle then return end

	if struggle.player1 == player then
		struggle.player1Taps = struggle.player1Taps + 1
	elseif struggle.player2 == player then
		struggle.player2Taps = struggle.player2Taps + 1
	end
end

-- ==========================================
-- NPC COMBAT
-- ==========================================

function CombatService.Client:MeleeAttackNPC(player: Player, npcModel: Model, moveId: string)
	local combat = combatStates[player]
	if not combat then return end

	-- Validate state
	local validStates = { Idle = true, Attacking = true }
	if not validStates[combat.state] then return end

	local attackerChar = player.Character
	if not attackerChar then return end

	-- Validate NPC exists and is in workspace
	if not npcModel or not npcModel.Parent then return end
	local npcHumanoid = npcModel:FindFirstChildOfClass("Humanoid")
	if not npcHumanoid or npcHumanoid.Health <= 0 then return end

	-- Range check
	local attackerRoot = attackerChar:FindFirstChild("HumanoidRootPart")
	local npcRoot = npcModel:FindFirstChild("HumanoidRootPart")
	if not attackerRoot or not npcRoot then return end

	local move = Moves:Get(moveId)
	if not move or not move.hits then return end

	local range = move.range or 8
	if (attackerRoot.Position - npcRoot.Position).Magnitude > range + 3 then return end

	-- Cooldown check
	if not HitValidator.IsOffCooldown(player, moveId) then return end
	HitValidator.RecordUse(player, moveId)

	combat.state = "Attacking"
	combat.lastCombatTime = tick()

	-- Track combo
	combat.comboCount = (combat.comboCount or 0)
	local comboTimeout = tick() - (combat.lastHitTime or 0)
	if comboTimeout > 2 then
		combat.comboCount = 0
		combat.comboDamage = 0
	end

	-- Process hits
	for i, hitData in ipairs(move.hits) do
		task.delay(hitData.frame / 60, function()
			if not combatStates[player] then return end
			if not npcModel.Parent then return end

			local npcHum = npcModel:FindFirstChildOfClass("Humanoid")
			if not npcHum or npcHum.Health <= 0 then return end

			-- Calculate damage
			local rawDamage = CombatUtil.CalculateDamage(combat.effectiveStats, hitData.damageMultiplier, false)

			-- Combo bonus: +5% per hit, max 50%
			combat.comboCount = combat.comboCount + 1
			local comboMult = 1 + math.min(0.5, (combat.comboCount - 1) * 0.05)
			local damage = math.max(1, math.floor(rawDamage * comboMult))

			-- Critical hit: 10% chance + Speed scaling
			local critChance = 0.10 + (combat.effectiveStats.Speed or 0) / 2000
			local isCrit = math.random() < critChance
			if isCrit then
				damage = math.floor(damage * 1.5)
			end

			combat.comboDamage = (combat.comboDamage or 0) + damage
			combat.lastHitTime = tick()

			-- Apply damage to NPC
			npcHum.Health = npcHum.Health - damage

			-- Apply knockback
			if hitData.knockback and npcRoot and npcRoot.Anchored then
				-- Temporarily unanchor for knockback
				npcRoot.Anchored = false
				local direction = CombatUtil.GetKnockbackDirection(attackerRoot.Position, npcRoot.Position)
				local worldKnockback = direction * hitData.knockback.Z + Vector3.new(0, hitData.knockback.Y, 0)

				local bv = Instance.new("BodyVelocity")
				bv.Velocity = worldKnockback
				bv.MaxForce = Vector3.new(1e6, 1e6, 1e6)
				bv.P = 1e4
				bv.Parent = npcRoot
				task.delay(0.3, function()
					bv:Destroy()
					if npcRoot and npcRoot.Parent then
						npcRoot.Anchored = true
					end
				end)
			end

			-- Update visual HP bar on NPC
			local head = npcModel:FindFirstChild("Head")
			if head then
				local billboard = head:FindFirstChild("HealthDisplay")
				if billboard then
					local hpBg = billboard:FindFirstChildOfClass("Frame")
					if hpBg then
						local hpBar = hpBg:FindFirstChild("HPBar")
						if hpBar and npcHum.MaxHealth > 0 then
							hpBar.Size = UDim2.fromScale(math.max(0, npcHum.Health / npcHum.MaxHealth), 1)
						end
					end
				end
			end

			-- Broadcast to attacker
			self.Server.Client.CombatResult:Fire(player, {
				type = "npc_hit",
				hitIndex = i,
				damage = damage,
				blocked = false,
				guardBreak = false,
				launcher = hitData.launcher or false,
				comboCount = combat.comboCount,
				comboDamage = combat.comboDamage,
				isCrit = isCrit,
				npcModel = npcModel,
			})

			-- Check NPC death
			if npcHum.Health <= 0 then
				self.Server.Client.CombatResult:Fire(player, {
					type = "npc_died",
					npcModel = npcModel,
					damage = damage,
				})

				-- Notify NPCSpawnService
				local NPCSpawnService = Knit.GetService("NPCSpawnService")
				if NPCSpawnService and NPCSpawnService._handleNPCDeath then
					NPCSpawnService:_handleNPCDeath(npcModel, player)
				end
			end
		end)
	end

	-- Return to idle
	local lastHit = move.hits[#move.hits]
	task.delay((lastHit.frame / 60) + 0.5, function()
		if combatStates[player] and combatStates[player].state == "Attacking" then
			combatStates[player].state = "Idle"
		end
	end)
end

return CombatService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="79">
          <Properties>
            <string name="Name">DataService</string>
            <string name="Source"><![CDATA[--[[
	DataService.lua
	Handles all player data persistence via ProfileService.
	Wraps ProfileService in a Knit service for easy access.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local ProfileService = require(ReplicatedStorage.Packages.ProfileService)
local Signal = require(ReplicatedStorage.Packages.Signal)

local Races = require(ReplicatedStorage.Shared.Constants.Races)
local Moves = require(ReplicatedStorage.Shared.Constants.Moves)
local Settings = require(ReplicatedStorage.Shared.Constants.Settings)

local DataService = Knit.CreateService({
	Name = "DataService",
	Client = {
		PlayerDataUpdate = Knit.CreateSignal(),
	},
})

-- Profile template (default data for new players)
local PROFILE_TEMPLATE = {
	Race = "",                    -- Set during character creation
	Level = 1,
	XP = 0,
	Zeni = 0,
	Rebirths = 0,

	Stats = {
		Melee = 0,
		KiPower = 0,
		Defense = 0,
		Speed = 0,
		Health = 0,
		Stamina = 0,
	},
	StatPoints = 3,               -- Starting points

	UnlockedTransformations = {},
	CurrentTransformation = "Base",

	UnlockedMoves = {},           -- Populated on character creation
	MoveLoadout = { "LightCombo", "KiBlast", nil, nil, nil, "Vanish" },

	Appearance = {
		SkinColor = { R = 255, G = 200, B = 150 },
		HairStyle = "Default",
		HairColor = { R = 30, G = 30, B = 30 },
		Outfit = "Default",
		OutfitColor = { R = 100, G = 100, B = 200 },
	},

	QuestProgress = {},
	CompletedSagas = {},

	PvpElo = 1000,
	PvpWins = 0,
	PvpLosses = 0,

	CharacterCreated = false,     -- Flag for first-time setup
}

local PROFILE_STORE_NAME = "PlayerData_v1"

-- Runtime storage
local profileStore = nil
local profiles: { [Player]: any } = {}

-- Signals
DataService.ProfileLoaded = Signal.new()
DataService.ProfileReleased = Signal.new()

function DataService:KnitInit()
	profileStore = ProfileService.GetProfileStore(PROFILE_STORE_NAME, PROFILE_TEMPLATE)
end

function DataService:KnitStart()
	-- Handle existing players (in case of late start)
	for _, player in ipairs(Players:GetPlayers()) do
		task.spawn(function()
			self:_loadProfile(player)
		end)
	end

	Players.PlayerAdded:Connect(function(player)
		self:_loadProfile(player)
	end)

	Players.PlayerRemoving:Connect(function(player)
		self:_releaseProfile(player)
	end)

	-- Auto-save loop
	task.spawn(function()
		while true do
			task.wait(Settings.Misc.AUTO_SAVE_INTERVAL)
			-- ProfileService auto-saves, but we can force reconcile here if needed
		end
	end)
end

function DataService:_loadProfile(player: Player)
	local profile = profileStore:LoadProfileAsync("Player_" .. player.UserId)

	if profile == nil then
		player:Kick("Failed to load your data. Please rejoin.")
		return
	end

	profile:AddUserId(player.UserId)
	profile:Reconcile() -- Fill in missing fields from template

	profile:ListenToRelease(function()
		profiles[player] = nil
		self.ProfileReleased:Fire(player)
		player:Kick("Your data session has ended. Please rejoin.")
	end)

	-- Check if player is still in game
	if not player:IsDescendantOf(Players) then
		profile:Release()
		return
	end

	profiles[player] = profile
	self.ProfileLoaded:Fire(player, profile.Data)

	-- Send initial data to client
	self.Client.PlayerDataUpdate:Fire(player, profile.Data)
end

function DataService:_releaseProfile(player: Player)
	local profile = profiles[player]
	if profile then
		profile:Release()
	end
end

-- Public API

function DataService:GetProfile(player: Player)
	return profiles[player]
end

function DataService:GetData(player: Player)
	local profile = profiles[player]
	if profile then
		return profile.Data
	end
	return nil
end

function DataService:UpdateData(player: Player, callback: (data: any) -> ())
	local profile = profiles[player]
	if not profile then
		return false
	end

	callback(profile.Data)

	-- Notify client of changes
	self.Client.PlayerDataUpdate:Fire(player, profile.Data)
	return true
end

function DataService:IsCharacterCreated(player: Player): boolean
	local data = self:GetData(player)
	return data and data.CharacterCreated or false
end

-- Client-facing methods

function DataService.Client:RequestPlayerData(player: Player)
	return self.Server:GetData(player)
end

function DataService.Client:SubmitCharacterCreation(player: Player, raceChoice: string, appearance: any)
	local data = self.Server:GetData(player)
	if not data then
		return false, "Data not loaded"
	end

	if data.CharacterCreated then
		return false, "Character already created"
	end

	if not Races:IsValid(raceChoice) then
		return false, "Invalid race"
	end

	local race = Races:Get(raceChoice)
	local defaultMoves = Moves:GetDefaultMoves()

	self.Server:UpdateData(player, function(d)
		d.Race = raceChoice
		d.CharacterCreated = true
		d.UnlockedMoves = defaultMoves

		-- Apply race base stats
		for stat, value in pairs(race.baseStats) do
			d.Stats[stat] = value
		end
	end)

	-- Apply appearance
	if appearance then
		self.Server:UpdateData(player, function(d)
			if appearance.SkinColor then
				d.Appearance.SkinColor = appearance.SkinColor
			end
			if appearance.HairStyle then
				d.Appearance.HairStyle = appearance.HairStyle
			end
			if appearance.HairColor then
				d.Appearance.HairColor = appearance.HairColor
			end
			if appearance.Outfit then
				d.Appearance.Outfit = appearance.Outfit
			end
			if appearance.OutfitColor then
				d.Appearance.OutfitColor = appearance.OutfitColor
			end
		end)
	end

	return true, "Character created successfully"
end

return DataService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="80">
          <Properties>
            <string name="Name">LeaderboardService</string>
            <string name="Source"><![CDATA[--[[
	LeaderboardService.lua
	Manages global leaderboards using OrderedDataStore.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local LeaderboardService = Knit.CreateService({
	Name = "LeaderboardService",
	Client = {},
})

local LEADERBOARD_TYPES = { "ELO", "Level", "Rebirths" }
local UPDATE_INTERVAL = 60 -- Update leaderboards every 60 seconds

local stores: { [string]: OrderedDataStore } = {}
local cachedLeaderboards: { [string]: { { userId: number, value: number } } } = {}

function LeaderboardService:KnitInit()
	for _, lbType in ipairs(LEADERBOARD_TYPES) do
		stores[lbType] = DataStoreService:GetOrderedDataStore("Leaderboard_" .. lbType)
		cachedLeaderboards[lbType] = {}
	end
end

function LeaderboardService:KnitStart()
	-- Periodic update loop
	task.spawn(function()
		while true do
			self:_refreshAll()
			task.wait(UPDATE_INTERVAL)
		end
	end)
end

function LeaderboardService:_refreshAll()
	for _, lbType in ipairs(LEADERBOARD_TYPES) do
		local success, err = pcall(function()
			local pages = stores[lbType]:GetSortedAsync(false, 50)
			local data = pages:GetCurrentPage()

			local entries = {}
			for rank, entry in ipairs(data) do
				table.insert(entries, {
					userId = tonumber(entry.key),
					value = entry.value,
					rank = rank,
				})
			end

			cachedLeaderboards[lbType] = entries
		end)

		if not success then
			warn("[LeaderboardService] Failed to refresh " .. lbType .. ": " .. tostring(err))
		end
	end
end

function LeaderboardService:UpdatePlayerScore(player: Player, lbType: string, value: number)
	if not stores[lbType] then return end

	local success, err = pcall(function()
		stores[lbType]:SetAsync(tostring(player.UserId), value)
	end)

	if not success then
		warn("[LeaderboardService] Failed to update " .. lbType .. " for " .. player.Name .. ": " .. tostring(err))
	end
end

function LeaderboardService:GetLeaderboard(lbType: string): { { userId: number, value: number, rank: number } }
	return cachedLeaderboards[lbType] or {}
end

-- Client methods

function LeaderboardService.Client:GetLeaderboard(player: Player, lbType: string)
	return self.Server:GetLeaderboard(lbType)
end

return LeaderboardService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="81">
          <Properties>
            <string name="Name">MapService</string>
            <string name="Source"><![CDATA[--[[
	MapService.lua
	Procedurally generates all world zones: terrain, structures, spawn points, portals.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local MapService = Knit.CreateService({
	Name = "MapService",
	Client = {},
})

function MapService:KnitInit() end

function MapService:KnitStart()
	self:_setupLighting()
	self:_buildWestCity()
	self:_buildWastelandArena()
	self:_buildKamisLookout()
	self:_buildHyperbolicTimeChamber()
	self:_buildNamek()
	self:_buildSpace()
	self:_buildBeerusPlanet()
	self:_buildTournamentOfPower()
	self:_buildZonePortals()
	self:_buildSpawnArea()
	print("[MapService] All zones generated!")
end

function MapService:_setupLighting()
	Lighting.Ambient = Color3.fromRGB(80, 80, 100)
	Lighting.OutdoorAmbient = Color3.fromRGB(120, 120, 140)
	Lighting.Brightness = 2
	Lighting.ClockTime = 14
	Lighting.GeographicLatitude = 30
	Lighting.FogEnd = 10000

	-- Atmosphere
	local atmosphere = Instance.new("Atmosphere")
	atmosphere.Density = 0.3
	atmosphere.Offset = 0.25
	atmosphere.Color = Color3.fromRGB(199, 199, 210)
	atmosphere.Decay = Color3.fromRGB(92, 100, 120)
	atmosphere.Glare = 0
	atmosphere.Haze = 1
	atmosphere.Parent = Lighting

	-- Bloom
	local bloom = Instance.new("BloomEffect")
	bloom.Intensity = 0.5
	bloom.Size = 24
	bloom.Threshold = 0.9
	bloom.Parent = Lighting

	-- Sun rays
	local sunRays = Instance.new("SunRaysEffect")
	sunRays.Intensity = 0.1
	sunRays.Spread = 0.5
	sunRays.Parent = Lighting
end

-- ==========================================
-- HELPER: Create a part with common properties
-- ==========================================
local function makePart(props)
	local part = Instance.new("Part")
	part.Anchored = true
	part.CanCollide = props.CanCollide ~= false
	part.Size = props.Size or Vector3.new(4, 4, 4)
	part.Position = props.Position or Vector3.new(0, 0, 0)
	part.Color = props.Color or Color3.fromRGB(128, 128, 128)
	part.Material = props.Material or Enum.Material.SmoothPlastic
	part.Name = props.Name or "Part"
	part.Transparency = props.Transparency or 0
	part.Shape = props.Shape or Enum.PartType.Block
	if props.CFrame then
		part.CFrame = props.CFrame
	end
	part.Parent = props.Parent or workspace
	return part
end

local function makeWedge(props)
	local part = Instance.new("WedgePart")
	part.Anchored = true
	part.Size = props.Size or Vector3.new(4, 4, 4)
	part.Position = props.Position or Vector3.new(0, 0, 0)
	part.Color = props.Color or Color3.fromRGB(128, 128, 128)
	part.Material = props.Material or Enum.Material.SmoothPlastic
	part.Name = props.Name or "Wedge"
	if props.CFrame then
		part.CFrame = props.CFrame
	end
	part.Parent = props.Parent or workspace
	return part
end

local function makeSpawnPoint(position, zoneName, parent)
	local spawn = Instance.new("SpawnLocation")
	spawn.Anchored = true
	spawn.Size = Vector3.new(8, 1, 8)
	spawn.Position = position
	spawn.Color = Color3.fromRGB(255, 255, 100)
	spawn.Material = Enum.Material.Neon
	spawn.Name = zoneName .. "_Spawn"
	spawn.Transparency = 0.5
	spawn.CanCollide = true
	spawn.Parent = parent
	return spawn
end

local function makeZoneLabel(text, position, parent)
	local part = Instance.new("Part")
	part.Anchored = true
	part.CanCollide = false
	part.Size = Vector3.new(20, 6, 0.1)
	part.Position = position + Vector3.new(0, 15, 0)
	part.Transparency = 1
	part.Name = text .. "_Label"
	part.Parent = parent

	local gui = Instance.new("SurfaceGui")
	gui.Face = Enum.NormalId.Front
	gui.Parent = part

	local label = Instance.new("TextLabel")
	label.Size = UDim2.fromScale(1, 1)
	label.BackgroundTransparency = 1
	label.Text = text
	label.TextColor3 = Color3.new(1, 1, 1)
	label.TextStrokeTransparency = 0
	label.TextStrokeColor3 = Color3.new(0, 0, 0)
	label.TextScaled = true
	label.Font = Enum.Font.GothamBold
	label.Parent = gui

	-- Also face the back
	local gui2 = gui:Clone()
	gui2.Face = Enum.NormalId.Back
	gui2.Parent = part

	return part
end

local function makeTree(position, parent, treeColor)
	treeColor = treeColor or Color3.fromRGB(50, 140, 50)
	local folder = Instance.new("Model")
	folder.Name = "Tree"
	folder.Parent = parent

	local trunk = makePart({
		Name = "Trunk",
		Size = Vector3.new(2, 10, 2),
		Position = position + Vector3.new(0, 5, 0),
		Color = Color3.fromRGB(100, 70, 40),
		Material = Enum.Material.Wood,
		Parent = folder,
	})

	local leaves = makePart({
		Name = "Leaves",
		Size = Vector3.new(8, 8, 8),
		Position = position + Vector3.new(0, 12, 0),
		Color = treeColor,
		Material = Enum.Material.Grass,
		Shape = Enum.PartType.Ball,
		Parent = folder,
	})

	folder.PrimaryPart = trunk
	return folder
end

local function makeBuilding(position, size, color, name, parent, roofColor)
	local folder = Instance.new("Model")
	folder.Name = name
	folder.Parent = parent

	local base = makePart({
		Name = "Base",
		Size = size,
		Position = position + Vector3.new(0, size.Y / 2, 0),
		Color = color,
		Material = Enum.Material.Concrete,
		Parent = folder,
	})

	-- Roof
	local roof = makePart({
		Name = "Roof",
		Size = Vector3.new(size.X + 2, 1, size.Z + 2),
		Position = position + Vector3.new(0, size.Y + 0.5, 0),
		Color = roofColor or Color3.fromRGB(80, 80, 80),
		Material = Enum.Material.Slate,
		Parent = folder,
	})

	-- Windows
	for i = 1, math.max(1, math.floor(size.X / 8)) do
		local windowX = position.X - size.X / 2 + (i * size.X / (math.floor(size.X / 8) + 1))
		for floor = 1, math.max(1, math.floor(size.Y / 8)) do
			local windowY = position.Y + (floor * 6) - 2
			local window = makePart({
				Name = "Window",
				Size = Vector3.new(3, 3, 0.2),
				Position = Vector3.new(windowX, windowY, position.Z + size.Z / 2 + 0.1),
				Color = Color3.fromRGB(180, 220, 255),
				Material = Enum.Material.Neon,
				Transparency = 0.3,
				Parent = folder,
			})
		end
	end

	-- Door
	local door = makePart({
		Name = "Door",
		Size = Vector3.new(4, 6, 0.3),
		Position = Vector3.new(position.X, position.Y + 3, position.Z + size.Z / 2 + 0.15),
		Color = Color3.fromRGB(139, 90, 43),
		Material = Enum.Material.Wood,
		Parent = folder,
	})

	folder.PrimaryPart = base
	return folder
end

local function makeRock(position, size, parent, color)
	local rock = makePart({
		Name = "Rock",
		Size = size or Vector3.new(6, 4, 5),
		Position = position,
		Color = color or Color3.fromRGB(130, 120, 110),
		Material = Enum.Material.Slate,
		Parent = parent,
	})
	return rock
end

-- ==========================================
-- WEST CITY (Starting Zone) - Center of map
-- ==========================================
function MapService:_buildWestCity()
	local zone = Instance.new("Folder")
	zone.Name = "WestCity"
	zone.Parent = workspace

	-- Ground platform (expanded)
	makePart({
		Name = "Ground",
		Size = Vector3.new(800, 2, 800),
		Position = Vector3.new(0, -1, 0),
		Color = Color3.fromRGB(100, 160, 80),
		Material = Enum.Material.Grass,
		Parent = zone,
	})

	-- Roads
	for i = -200, 200, 40 do
		makePart({
			Name = "Road_NS",
			Size = Vector3.new(12, 0.2, 500),
			Position = Vector3.new(i, 0.1, 0),
			Color = Color3.fromRGB(60, 60, 60),
			Material = Enum.Material.Asphalt,
			Parent = zone,
		})
	end
	for i = -200, 200, 40 do
		makePart({
			Name = "Road_EW",
			Size = Vector3.new(500, 0.2, 12),
			Position = Vector3.new(0, 0.1, i),
			Color = Color3.fromRGB(60, 60, 60),
			Material = Enum.Material.Asphalt,
			Parent = zone,
		})
	end

	-- Sidewalks along roads
	for i = -200, 200, 40 do
		makePart({
			Name = "Sidewalk",
			Size = Vector3.new(16, 0.3, 500),
			Position = Vector3.new(i, 0.15, 0),
			Color = Color3.fromRGB(180, 180, 170),
			Material = Enum.Material.Concrete,
			Parent = zone,
		})
	end

	-- Capsule Corp (main landmark)
	local capsuleCorp = Instance.new("Model")
	capsuleCorp.Name = "CapsuleCorp"
	capsuleCorp.Parent = zone

	-- Main dome
	makePart({
		Name = "MainDome",
		Size = Vector3.new(40, 40, 40),
		Position = Vector3.new(0, 20, -50),
		Color = Color3.fromRGB(220, 220, 240),
		Material = Enum.Material.SmoothPlastic,
		Shape = Enum.PartType.Ball,
		Parent = capsuleCorp,
	})
	-- Dome base
	makePart({
		Name = "DomeBase",
		Size = Vector3.new(50, 10, 50),
		Position = Vector3.new(0, 5, -50),
		Color = Color3.fromRGB(200, 200, 220),
		Material = Enum.Material.Concrete,
		Parent = capsuleCorp,
	})
	-- "CAPSULE CORP" sign
	makeZoneLabel("CAPSULE CORP", Vector3.new(0, 25, -25), capsuleCorp)

	-- City buildings
	local buildingConfigs = {
		{ pos = Vector3.new(60, 0, 60), size = Vector3.new(30, 40, 30), color = Color3.fromRGB(180, 180, 200) },
		{ pos = Vector3.new(-60, 0, 60), size = Vector3.new(25, 55, 25), color = Color3.fromRGB(200, 190, 170) },
		{ pos = Vector3.new(60, 0, -120), size = Vector3.new(35, 30, 35), color = Color3.fromRGB(170, 200, 190) },
		{ pos = Vector3.new(-60, 0, -120), size = Vector3.new(28, 45, 28), color = Color3.fromRGB(190, 180, 200) },
		{ pos = Vector3.new(120, 0, 0), size = Vector3.new(25, 60, 25), color = Color3.fromRGB(210, 200, 180) },
		{ pos = Vector3.new(-120, 0, 0), size = Vector3.new(30, 35, 30), color = Color3.fromRGB(180, 190, 210) },
		{ pos = Vector3.new(120, 0, 120), size = Vector3.new(20, 25, 20), color = Color3.fromRGB(200, 200, 200) },
		{ pos = Vector3.new(-120, 0, 120), size = Vector3.new(35, 50, 35), color = Color3.fromRGB(190, 190, 200) },
		{ pos = Vector3.new(0, 0, 150), size = Vector3.new(40, 20, 30), color = Color3.fromRGB(210, 210, 190) },
		{ pos = Vector3.new(180, 0, 60), size = Vector3.new(22, 70, 22), color = Color3.fromRGB(180, 180, 200) },
		{ pos = Vector3.new(-180, 0, 60), size = Vector3.new(30, 40, 30), color = Color3.fromRGB(200, 190, 180) },
		{ pos = Vector3.new(180, 0, -60), size = Vector3.new(28, 35, 28), color = Color3.fromRGB(190, 200, 190) },
	}

	for i, cfg in ipairs(buildingConfigs) do
		makeBuilding(cfg.pos, cfg.size, cfg.color, "Building_" .. i, zone)
	end

	-- Trees scattered in parks
	for i = 1, 30 do
		local x = math.random(-220, 220)
		local z = math.random(-220, 220)
		-- Avoid roads
		if math.abs(x % 40) > 8 and math.abs(z % 40) > 8 then
			makeTree(Vector3.new(x, 0, z), zone)
		end
	end

	-- Street lamps
	for i = -200, 200, 20 do
		for _, offset in ipairs({ -7, 7 }) do
			local pole = makePart({
				Name = "LampPole",
				Size = Vector3.new(0.5, 10, 0.5),
				Position = Vector3.new(i + offset, 5, 0),
				Color = Color3.fromRGB(80, 80, 80),
				Material = Enum.Material.Metal,
				Parent = zone,
			})
			local lamp = makePart({
				Name = "Lamp",
				Size = Vector3.new(2, 1, 2),
				Position = Vector3.new(i + offset, 10.5, 0),
				Color = Color3.fromRGB(255, 255, 200),
				Material = Enum.Material.Neon,
				Parent = zone,
			})
			local light = Instance.new("PointLight")
			light.Color = Color3.fromRGB(255, 255, 200)
			light.Brightness = 1
			light.Range = 30
			light.Parent = lamp
		end
	end

	-- Training ground (circular arena)
	makePart({
		Name = "TrainingGround",
		Size = Vector3.new(60, 1, 60),
		Position = Vector3.new(0, 0.5, 100),
		Color = Color3.fromRGB(200, 180, 140),
		Material = Enum.Material.Sandstone,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})
	makeZoneLabel("Training Grounds", Vector3.new(0, 2, 100), zone)

	-- Shop building
	local shop = makeBuilding(
		Vector3.new(-80, 0, -60),
		Vector3.new(20, 15, 20),
		Color3.fromRGB(255, 200, 100),
		"Shop",
		zone,
		Color3.fromRGB(200, 50, 50)
	)
	makeZoneLabel("SHOP", Vector3.new(-80, 10, -60), zone)

	-- ============ EXPANDED WEST CITY CONTENT ============

	-- Central Fountain/Plaza
	makePart({
		Name = "FountainBase",
		Size = Vector3.new(20, 2, 20),
		Position = Vector3.new(0, 1, 0),
		Color = Color3.fromRGB(200, 200, 210),
		Material = Enum.Material.Marble,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})
	makePart({
		Name = "FountainPillar",
		Size = Vector3.new(3, 8, 3),
		Position = Vector3.new(0, 6, 0),
		Color = Color3.fromRGB(200, 200, 210),
		Material = Enum.Material.Marble,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})
	makePart({
		Name = "FountainTop",
		Size = Vector3.new(6, 6, 6),
		Position = Vector3.new(0, 11, 0),
		Color = Color3.fromRGB(80, 150, 230),
		Material = Enum.Material.Glass,
		Shape = Enum.PartType.Ball,
		Transparency = 0.3,
		Parent = zone,
	})
	local fLight = Instance.new("PointLight")
	fLight.Color = Color3.fromRGB(80, 150, 255)
	fLight.Brightness = 2
	fLight.Range = 30
	fLight.Parent = zone:FindFirstChild("FountainTop") or zone

	-- Park area (northeast)
	makePart({
		Name = "ParkGrass",
		Size = Vector3.new(150, 0.5, 150),
		Position = Vector3.new(250, 0.25, -250),
		Color = Color3.fromRGB(80, 180, 60),
		Material = Enum.Material.Grass,
		Parent = zone,
	})
	for i = 1, 20 do
		makeTree(Vector3.new(250 + math.random(-60, 60), 0.5, -250 + math.random(-60, 60)), zone)
	end
	-- Park benches
	for i = 1, 6 do
		makePart({
			Name = "Bench_" .. i,
			Size = Vector3.new(6, 1.5, 2),
			Position = Vector3.new(250 + math.random(-50, 50), 0.75 + 0.5, -250 + math.random(-50, 50)),
			Color = Color3.fromRGB(120, 80, 40),
			Material = Enum.Material.Wood,
			Parent = zone,
		})
	end
	-- Pond in park
	makePart({
		Name = "Pond",
		Size = Vector3.new(30, 0.5, 30),
		Position = Vector3.new(260, 0.4, -260),
		Color = Color3.fromRGB(60, 130, 200),
		Material = Enum.Material.Glass,
		Transparency = 0.3,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})

	-- Stadium/Arena (southeast)
	local stadium = Instance.new("Model")
	stadium.Name = "Stadium"
	stadium.Parent = zone
	-- Arena floor
	makePart({
		Name = "ArenaFloor",
		Size = Vector3.new(80, 1, 80),
		Position = Vector3.new(250, 0.5, 250),
		Color = Color3.fromRGB(200, 180, 140),
		Material = Enum.Material.Sandstone,
		Parent = stadium,
	})
	-- Stadium seating (tiered walls)
	for tier = 1, 3 do
		local size = 80 + tier * 15
		makePart({
			Name = "StadiumTier_" .. tier,
			Size = Vector3.new(size, tier * 4, size),
			Position = Vector3.new(250, tier * 2, 250),
			Color = Color3.fromRGB(160, 160, 170),
			Material = Enum.Material.Concrete,
			Parent = stadium,
		})
	end
	makeZoneLabel("PvP Arena", Vector3.new(250, 15, 250), zone)

	-- Residential area (northwest) — smaller houses
	for i = 1, 8 do
		local row = math.floor((i - 1) / 4)
		local col = (i - 1) % 4
		makeBuilding(
			Vector3.new(-250 + col * 35, 0, -250 + row * 40),
			Vector3.new(15, 10, 15),
			Color3.fromRGB(200 + math.random(-20, 20), 190 + math.random(-20, 20), 170 + math.random(-20, 20)),
			"House_" .. i,
			zone,
			Color3.fromRGB(150, 80, 60)
		)
	end
	makeZoneLabel("Residential District", Vector3.new(-250, 12, -230), zone)

	-- Market area (southwest)
	for i = 1, 5 do
		makePart({
			Name = "MarketStall_" .. i,
			Size = Vector3.new(8, 6, 6),
			Position = Vector3.new(-250 + i * 15, 3, 250),
			Color = Color3.fromRGB(
				math.random(150, 255),
				math.random(100, 200),
				math.random(50, 150)
			),
			Material = Enum.Material.Fabric,
			Parent = zone,
		})
		-- Stall roof
		makePart({
			Name = "StallRoof_" .. i,
			Size = Vector3.new(10, 0.5, 8),
			Position = Vector3.new(-250 + i * 15, 6.5, 250),
			Color = Color3.fromRGB(200, 50, 50),
			Material = Enum.Material.Fabric,
			Parent = zone,
		})
	end
	makeZoneLabel("Market", Vector3.new(-210, 8, 250), zone)

	-- More distant skyscrapers for city feel
	for i = 1, 10 do
		local angle = math.rad(i * 36)
		local dist = math.random(280, 370)
		local height = math.random(30, 100)
		local width = math.random(15, 30)
		makeBuilding(
			Vector3.new(math.cos(angle) * dist, 0, math.sin(angle) * dist),
			Vector3.new(width, height, width),
			Color3.fromRGB(
				160 + math.random(-30, 30),
				160 + math.random(-30, 30),
				180 + math.random(-20, 20)
			),
			"Skyscraper_" .. i,
			zone
		)
	end

	-- Highway overpass
	makePart({
		Name = "HighwayDeck",
		Size = Vector3.new(800, 1, 16),
		Position = Vector3.new(0, 15, -350),
		Color = Color3.fromRGB(70, 70, 70),
		Material = Enum.Material.Asphalt,
		Parent = zone,
	})
	-- Highway pillars
	for i = -350, 350, 50 do
		makePart({
			Name = "HighwayPillar",
			Size = Vector3.new(3, 15, 3),
			Position = Vector3.new(i, 7.5, -350),
			Color = Color3.fromRGB(140, 140, 140),
			Material = Enum.Material.Concrete,
			Parent = zone,
		})
	end

	makeSpawnPoint(Vector3.new(0, 1, 0), "WestCity", zone)
	makeZoneLabel("West City", Vector3.new(0, 0, -380), zone)
end

-- ==========================================
-- WASTELAND ARENA
-- ==========================================
function MapService:_buildWastelandArena()
	local zone = Instance.new("Folder")
	zone.Name = "WastelandArena"
	zone.Parent = workspace

	local OFFSET = Vector3.new(600, 0, 0)

	-- Desert ground (expanded)
	makePart({
		Name = "Ground",
		Size = Vector3.new(600, 2, 600),
		Position = OFFSET + Vector3.new(0, -1, 0),
		Color = Color3.fromRGB(194, 163, 108),
		Material = Enum.Material.Sand,
		Parent = zone,
	})

	-- Rocky terrain - large boulders
	local rockPositions = {
		{ pos = Vector3.new(80, 0, 80), size = Vector3.new(20, 15, 18) },
		{ pos = Vector3.new(-70, 0, 50), size = Vector3.new(15, 25, 12) },
		{ pos = Vector3.new(40, 0, -90), size = Vector3.new(25, 10, 20) },
		{ pos = Vector3.new(-100, 0, -80), size = Vector3.new(30, 20, 25) },
		{ pos = Vector3.new(150, 0, 0), size = Vector3.new(18, 30, 15) },
		{ pos = Vector3.new(-150, 0, 30), size = Vector3.new(22, 18, 20) },
		{ pos = Vector3.new(0, 0, 150), size = Vector3.new(35, 12, 30) },
		{ pos = Vector3.new(0, 0, -150), size = Vector3.new(20, 35, 18) },
	}

	for i, cfg in ipairs(rockPositions) do
		makeRock(
			OFFSET + cfg.pos + Vector3.new(0, cfg.size.Y / 2, 0),
			cfg.size,
			zone,
			Color3.fromRGB(
				130 + math.random(-20, 20),
				110 + math.random(-20, 20),
				90 + math.random(-20, 20)
			)
		)
	end

	-- Cliff walls on edges
	makePart({
		Name = "CliffNorth",
		Size = Vector3.new(400, 50, 10),
		Position = OFFSET + Vector3.new(0, 25, -200),
		Color = Color3.fromRGB(150, 120, 90),
		Material = Enum.Material.Rock,
		Parent = zone,
	})
	makePart({
		Name = "CliffSouth",
		Size = Vector3.new(400, 50, 10),
		Position = OFFSET + Vector3.new(0, 25, 200),
		Color = Color3.fromRGB(150, 120, 90),
		Material = Enum.Material.Rock,
		Parent = zone,
	})

	-- Battle crater in center
	makePart({
		Name = "Crater",
		Size = Vector3.new(50, 4, 50),
		Position = OFFSET + Vector3.new(0, -3, 0),
		Color = Color3.fromRGB(100, 80, 60),
		Material = Enum.Material.Slate,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})

	-- Scattered small rocks
	for i = 1, 40 do
		makeRock(
			OFFSET + Vector3.new(math.random(-180, 180), math.random(1, 3), math.random(-180, 180)),
			Vector3.new(math.random(2, 6), math.random(2, 4), math.random(2, 6)),
			zone
		)
	end

	makeSpawnPoint(OFFSET + Vector3.new(0, 1, 80), "WastelandArena", zone)
	makeZoneLabel("Wasteland Arena", OFFSET + Vector3.new(0, 0, -180), zone)
end

-- ==========================================
-- KAMI'S LOOKOUT
-- ==========================================
function MapService:_buildKamisLookout()
	local zone = Instance.new("Folder")
	zone.Name = "KamisLookout"
	zone.Parent = workspace

	local OFFSET = Vector3.new(0, 500, 600)

	-- Main circular platform
	makePart({
		Name = "MainPlatform",
		Size = Vector3.new(120, 4, 120),
		Position = OFFSET + Vector3.new(0, 0, 0),
		Color = Color3.fromRGB(240, 230, 210),
		Material = Enum.Material.Marble,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})

	-- Pillar (tower supporting the lookout)
	makePart({
		Name = "Pillar",
		Size = Vector3.new(15, 500, 15),
		Position = OFFSET + Vector3.new(0, -250, 0),
		Color = Color3.fromRGB(200, 190, 170),
		Material = Enum.Material.Concrete,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})

	-- Palace building
	local palace = Instance.new("Model")
	palace.Name = "Palace"
	palace.Parent = zone

	makePart({
		Name = "PalaceBase",
		Size = Vector3.new(40, 20, 40),
		Position = OFFSET + Vector3.new(0, 12, -20),
		Color = Color3.fromRGB(240, 230, 210),
		Material = Enum.Material.Marble,
		Parent = palace,
	})
	makePart({
		Name = "PalaceDome",
		Size = Vector3.new(30, 20, 30),
		Position = OFFSET + Vector3.new(0, 28, -20),
		Color = Color3.fromRGB(50, 150, 100),
		Material = Enum.Material.SmoothPlastic,
		Shape = Enum.PartType.Ball,
		Parent = palace,
	})
	-- Spire
	makePart({
		Name = "Spire",
		Size = Vector3.new(3, 20, 3),
		Position = OFFSET + Vector3.new(0, 45, -20),
		Color = Color3.fromRGB(255, 215, 0),
		Material = Enum.Material.Neon,
		Parent = palace,
	})

	-- Decorative pillars around edge
	for angle = 0, 330, 30 do
		local rad = math.rad(angle)
		local x = math.cos(rad) * 55
		local z = math.sin(rad) * 55
		makePart({
			Name = "EdgePillar",
			Size = Vector3.new(3, 12, 3),
			Position = OFFSET + Vector3.new(x, 8, z),
			Color = Color3.fromRGB(240, 230, 210),
			Material = Enum.Material.Marble,
			Parent = zone,
		})
	end

	-- Palm trees
	for i = 1, 8 do
		local angle = math.rad(i * 45)
		local x = math.cos(angle) * 40
		local z = math.sin(angle) * 40
		makeTree(OFFSET + Vector3.new(x, 2, z), zone, Color3.fromRGB(60, 180, 60))
	end

	-- Hyperbolic Time Chamber entrance
	local htcDoor = makePart({
		Name = "HTC_Entrance",
		Size = Vector3.new(8, 12, 2),
		Position = OFFSET + Vector3.new(30, 8, 0),
		Color = Color3.fromRGB(255, 255, 255),
		Material = Enum.Material.Neon,
		Transparency = 0.3,
		Parent = zone,
	})
	makeZoneLabel("Time Chamber", OFFSET + Vector3.new(30, 8, 0), zone)

	-- Cloud floor below
	for i = 1, 20 do
		makePart({
			Name = "Cloud_" .. i,
			Size = Vector3.new(
				math.random(30, 80),
				math.random(5, 15),
				math.random(30, 80)
			),
			Position = OFFSET + Vector3.new(
				math.random(-200, 200),
				math.random(-50, -20),
				math.random(-200, 200)
			),
			Color = Color3.new(1, 1, 1),
			Material = Enum.Material.SmoothPlastic,
			Transparency = 0.4,
			CanCollide = false,
			Parent = zone,
		})
	end

	makeSpawnPoint(OFFSET + Vector3.new(0, 3, 30), "KamisLookout", zone)
	makeZoneLabel("Kami's Lookout", OFFSET + Vector3.new(0, 10, 50), zone)
end

-- ==========================================
-- HYPERBOLIC TIME CHAMBER
-- ==========================================
function MapService:_buildHyperbolicTimeChamber()
	local zone = Instance.new("Folder")
	zone.Name = "HyperbolicTimeChamber"
	zone.Parent = workspace

	local OFFSET = Vector3.new(0, 500, 1200)

	-- Infinite white floor
	makePart({
		Name = "Floor",
		Size = Vector3.new(2000, 2, 2000),
		Position = OFFSET + Vector3.new(0, -1, 0),
		Color = Color3.new(1, 1, 1),
		Material = Enum.Material.SmoothPlastic,
		Parent = zone,
	})

	-- Living quarters
	local quarters = Instance.new("Model")
	quarters.Name = "LivingQuarters"
	quarters.Parent = zone

	makePart({
		Name = "Floor",
		Size = Vector3.new(30, 1, 30),
		Position = OFFSET + Vector3.new(0, 0.5, 0),
		Color = Color3.fromRGB(200, 180, 140),
		Material = Enum.Material.Wood,
		Parent = quarters,
	})
	-- Beds
	for _, xOff in ipairs({ -8, 8 }) do
		makePart({
			Name = "Bed",
			Size = Vector3.new(5, 2, 8),
			Position = OFFSET + Vector3.new(xOff, 1, -10),
			Color = Color3.fromRGB(200, 200, 220),
			Material = Enum.Material.Fabric,
			Parent = quarters,
		})
	end
	-- Hourglass
	makePart({
		Name = "Hourglass",
		Size = Vector3.new(3, 8, 3),
		Position = OFFSET + Vector3.new(0, 4, -12),
		Color = Color3.fromRGB(255, 215, 0),
		Material = Enum.Material.Glass,
		Shape = Enum.PartType.Cylinder,
		Parent = quarters,
	})

	-- Zone-specific fog to create infinite void feel
	-- (client-side fog adjustment would handle this in game)

	makeSpawnPoint(OFFSET + Vector3.new(0, 1, 5), "HyperbolicTimeChamber", zone)
	makeZoneLabel("Hyperbolic Time Chamber (3x XP)", OFFSET + Vector3.new(0, 5, 20), zone)
end

-- ==========================================
-- PLANET NAMEK
-- ==========================================
function MapService:_buildNamek()
	local zone = Instance.new("Folder")
	zone.Name = "Namek"
	zone.Parent = workspace

	local OFFSET = Vector3.new(2000, 0, 0)

	-- Green-tinted ground (expanded)
	makePart({
		Name = "Ground",
		Size = Vector3.new(700, 2, 700),
		Position = OFFSET + Vector3.new(0, -1, 0),
		Color = Color3.fromRGB(80, 180, 100),
		Material = Enum.Material.Grass,
		Parent = zone,
	})

	-- Namekian water (green water)
	makePart({
		Name = "Water",
		Size = Vector3.new(150, 1, 200),
		Position = OFFSET + Vector3.new(150, -0.5, 0),
		Color = Color3.fromRGB(50, 200, 100),
		Material = Enum.Material.Glass,
		Transparency = 0.4,
		CanCollide = false,
		Parent = zone,
	})

	-- Namekian houses (dome-shaped)
	local housePositions = {
		Vector3.new(0, 0, -60),
		Vector3.new(-80, 0, 30),
		Vector3.new(60, 0, 80),
		Vector3.new(-40, 0, -120),
		Vector3.new(100, 0, -40),
	}
	for i, pos in ipairs(housePositions) do
		local house = Instance.new("Model")
		house.Name = "NamekianHouse_" .. i
		house.Parent = zone

		makePart({
			Name = "Dome",
			Size = Vector3.new(18, 18, 18),
			Position = OFFSET + pos + Vector3.new(0, 9, 0),
			Color = Color3.fromRGB(60, 160, 80),
			Material = Enum.Material.SmoothPlastic,
			Shape = Enum.PartType.Ball,
			Parent = house,
		})
		makePart({
			Name = "Base",
			Size = Vector3.new(20, 4, 20),
			Position = OFFSET + pos + Vector3.new(0, 2, 0),
			Color = Color3.fromRGB(80, 140, 90),
			Material = Enum.Material.Concrete,
			Parent = house,
		})
		makePart({
			Name = "Door",
			Size = Vector3.new(4, 6, 1),
			Position = OFFSET + pos + Vector3.new(0, 3, 10),
			Color = Color3.fromRGB(40, 100, 50),
			Material = Enum.Material.Wood,
			Parent = house,
		})
	end

	-- Namekian trees (tall thin, blue-ish tops)
	for i = 1, 25 do
		local x = math.random(-200, 200)
		local z = math.random(-200, 200)
		makeTree(OFFSET + Vector3.new(x, 0, z), zone, Color3.fromRGB(50, 120, 180))
	end

	-- Dragon Ball pedestals (3 visible)
	for i, pos in ipairs({
		Vector3.new(0, 0, 0),
		Vector3.new(-120, 0, -100),
		Vector3.new(80, 0, 120),
	}) do
		local pedestal = makePart({
			Name = "DragonBallPedestal_" .. i,
			Size = Vector3.new(4, 3, 4),
			Position = OFFSET + pos + Vector3.new(0, 1.5, 0),
			Color = Color3.fromRGB(180, 160, 120),
			Material = Enum.Material.Marble,
			Parent = zone,
		})
		local ball = makePart({
			Name = "DragonBall_" .. i,
			Size = Vector3.new(3, 3, 3),
			Position = OFFSET + pos + Vector3.new(0, 4.5, 0),
			Color = Color3.fromRGB(255, 165, 0),
			Material = Enum.Material.Neon,
			Shape = Enum.PartType.Ball,
			Parent = zone,
		})
		local light = Instance.new("PointLight")
		light.Color = Color3.fromRGB(255, 165, 0)
		light.Brightness = 2
		light.Range = 15
		light.Parent = ball
	end

	-- Guru's house (larger)
	local guru = Instance.new("Model")
	guru.Name = "GuruHouse"
	guru.Parent = zone

	makePart({
		Name = "Dome",
		Size = Vector3.new(30, 30, 30),
		Position = OFFSET + Vector3.new(-100, 15, -150),
		Color = Color3.fromRGB(80, 180, 100),
		Material = Enum.Material.SmoothPlastic,
		Shape = Enum.PartType.Ball,
		Parent = guru,
	})
	makePart({
		Name = "Base",
		Size = Vector3.new(35, 6, 35),
		Position = OFFSET + Vector3.new(-100, 3, -150),
		Color = Color3.fromRGB(100, 160, 110),
		Material = Enum.Material.Concrete,
		Parent = guru,
	})
	makeZoneLabel("Guru's House", OFFSET + Vector3.new(-100, 20, -150), zone)

	makeSpawnPoint(OFFSET + Vector3.new(0, 1, 30), "Namek", zone)
	makeZoneLabel("Planet Namek", OFFSET + Vector3.new(0, 0, -230), zone)
end

-- ==========================================
-- DEEP SPACE
-- ==========================================
function MapService:_buildSpace()
	local zone = Instance.new("Folder")
	zone.Name = "Space"
	zone.Parent = workspace

	local OFFSET = Vector3.new(4000, 0, 0)

	-- Dark metallic platform (space station)
	makePart({
		Name = "StationFloor",
		Size = Vector3.new(300, 3, 300),
		Position = OFFSET + Vector3.new(0, -1.5, 0),
		Color = Color3.fromRGB(40, 40, 50),
		Material = Enum.Material.DiamondPlate,
		Parent = zone,
	})

	-- Frieza's ship (central structure)
	local ship = Instance.new("Model")
	ship.Name = "FriezaShip"
	ship.Parent = zone

	-- Saucer shape
	makePart({
		Name = "Saucer",
		Size = Vector3.new(80, 10, 80),
		Position = OFFSET + Vector3.new(0, 20, 0),
		Color = Color3.fromRGB(180, 170, 200),
		Material = Enum.Material.Metal,
		Shape = Enum.PartType.Cylinder,
		Parent = ship,
	})
	-- Tower
	makePart({
		Name = "Tower",
		Size = Vector3.new(20, 40, 20),
		Position = OFFSET + Vector3.new(0, 45, 0),
		Color = Color3.fromRGB(160, 150, 180),
		Material = Enum.Material.Metal,
		Shape = Enum.PartType.Cylinder,
		Parent = ship,
	})
	-- Dome top
	makePart({
		Name = "DomeTop",
		Size = Vector3.new(25, 15, 25),
		Position = OFFSET + Vector3.new(0, 68, 0),
		Color = Color3.fromRGB(200, 50, 50),
		Material = Enum.Material.SmoothPlastic,
		Shape = Enum.PartType.Ball,
		Parent = ship,
	})
	-- Landing legs
	for angle = 0, 270, 90 do
		local rad = math.rad(angle)
		makePart({
			Name = "Leg_" .. angle,
			Size = Vector3.new(5, 25, 5),
			Position = OFFSET + Vector3.new(math.cos(rad) * 35, 7, math.sin(rad) * 35),
			Color = Color3.fromRGB(120, 120, 130),
			Material = Enum.Material.Metal,
			Parent = ship,
		})
	end

	-- Space debris / asteroids floating
	for i = 1, 30 do
		makePart({
			Name = "Asteroid_" .. i,
			Size = Vector3.new(
				math.random(5, 20),
				math.random(5, 15),
				math.random(5, 20)
			),
			Position = OFFSET + Vector3.new(
				math.random(-200, 200),
				math.random(30, 100),
				math.random(-200, 200)
			),
			Color = Color3.fromRGB(
				80 + math.random(0, 40),
				70 + math.random(0, 40),
				70 + math.random(0, 40)
			),
			Material = Enum.Material.Slate,
			CanCollide = false,
			Parent = zone,
		})
	end

	-- Star field (glowing dots)
	for i = 1, 100 do
		makePart({
			Name = "Star_" .. i,
			Size = Vector3.new(1, 1, 1),
			Position = OFFSET + Vector3.new(
				math.random(-500, 500),
				math.random(50, 300),
				math.random(-500, 500)
			),
			Color = Color3.new(1, 1, 1),
			Material = Enum.Material.Neon,
			Shape = Enum.PartType.Ball,
			CanCollide = false,
			Parent = zone,
		})
	end

	-- Purple/dark skybox feel via walls
	for _, dir in ipairs({
		{ Vector3.new(0, 150, -250), Vector3.new(500, 300, 1) },
		{ Vector3.new(0, 150, 250), Vector3.new(500, 300, 1) },
		{ Vector3.new(-250, 150, 0), Vector3.new(1, 300, 500) },
		{ Vector3.new(250, 150, 0), Vector3.new(1, 300, 500) },
	}) do
		makePart({
			Name = "SpaceWall",
			Size = dir[2],
			Position = OFFSET + dir[1],
			Color = Color3.fromRGB(10, 5, 30),
			Material = Enum.Material.SmoothPlastic,
			Parent = zone,
		})
	end

	makeSpawnPoint(OFFSET + Vector3.new(0, 1, 80), "Space", zone)
	makeZoneLabel("Deep Space - Frieza's Domain", OFFSET + Vector3.new(0, 30, -130), zone)
end

-- ==========================================
-- BEERUS'S PLANET
-- ==========================================
function MapService:_buildBeerusPlanet()
	local zone = Instance.new("Folder")
	zone.Name = "BeerusPlanet"
	zone.Parent = workspace

	local OFFSET = Vector3.new(6000, 0, 0)

	-- Upside-down tree planet (iconic shape)
	-- Main ground platform (the top of the "tree")
	makePart({
		Name = "Ground",
		Size = Vector3.new(200, 4, 200),
		Position = OFFSET + Vector3.new(0, 0, 0),
		Color = Color3.fromRGB(140, 200, 100),
		Material = Enum.Material.Grass,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})

	-- Giant tree trunk going down
	makePart({
		Name = "TreeTrunk",
		Size = Vector3.new(40, 300, 40),
		Position = OFFSET + Vector3.new(0, -150, 0),
		Color = Color3.fromRGB(120, 80, 40),
		Material = Enum.Material.Wood,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})

	-- Beerus's palace
	local palace = Instance.new("Model")
	palace.Name = "BeerusPalace"
	palace.Parent = zone

	-- Egyptian/pyramid-ish palace
	makePart({
		Name = "PalaceBase",
		Size = Vector3.new(60, 15, 60),
		Position = OFFSET + Vector3.new(0, 9.5, -30),
		Color = Color3.fromRGB(200, 180, 130),
		Material = Enum.Material.Sandstone,
		Parent = palace,
	})
	makePart({
		Name = "PalaceMiddle",
		Size = Vector3.new(45, 12, 45),
		Position = OFFSET + Vector3.new(0, 23, -30),
		Color = Color3.fromRGB(210, 190, 140),
		Material = Enum.Material.Sandstone,
		Parent = palace,
	})
	makePart({
		Name = "PalaceTop",
		Size = Vector3.new(30, 10, 30),
		Position = OFFSET + Vector3.new(0, 34, -30),
		Color = Color3.fromRGB(220, 200, 150),
		Material = Enum.Material.Sandstone,
		Parent = palace,
	})
	-- Spires
	for _, xz in ipairs({ { -25, -5 }, { 25, -5 }, { -25, -55 }, { 25, -55 } }) do
		makePart({
			Name = "Spire",
			Size = Vector3.new(5, 30, 5),
			Position = OFFSET + Vector3.new(xz[1], 17, xz[2]),
			Color = Color3.fromRGB(200, 170, 100),
			Material = Enum.Material.Sandstone,
			Parent = palace,
		})
		makePart({
			Name = "SpireTop",
			Size = Vector3.new(3, 3, 3),
			Position = OFFSET + Vector3.new(xz[1], 33, xz[2]),
			Color = Color3.fromRGB(180, 50, 200),
			Material = Enum.Material.Neon,
			Shape = Enum.PartType.Ball,
			Parent = palace,
		})
	end

	-- Koi pond
	makePart({
		Name = "KoiPond",
		Size = Vector3.new(25, 1, 25),
		Position = OFFSET + Vector3.new(50, 0, 40),
		Color = Color3.fromRGB(80, 150, 220),
		Material = Enum.Material.Glass,
		Transparency = 0.3,
		Shape = Enum.PartType.Cylinder,
		Parent = zone,
	})

	-- Cherry blossom trees (pink)
	for i = 1, 12 do
		local angle = math.rad(i * 30)
		local r = math.random(50, 90)
		makeTree(
			OFFSET + Vector3.new(math.cos(angle) * r, 2, math.sin(angle) * r),
			zone,
			Color3.fromRGB(255, 150, 180)
		)
	end

	-- Oracle fish pond
	makeZoneLabel("Oracle Fish", OFFSET + Vector3.new(50, 3, 40), zone)

	-- Training courtyard
	makePart({
		Name = "TrainingYard",
		Size = Vector3.new(40, 0.5, 40),
		Position = OFFSET + Vector3.new(-50, 2.5, 50),
		Color = Color3.fromRGB(220, 210, 180),
		Material = Enum.Material.Marble,
		Parent = zone,
	})
	makeZoneLabel("Training Courtyard", OFFSET + Vector3.new(-50, 3, 50), zone)

	makeSpawnPoint(OFFSET + Vector3.new(0, 3, 50), "BeerusPlanet", zone)
	makeZoneLabel("Beerus's Planet", OFFSET + Vector3.new(0, 10, 90), zone)
end

-- ==========================================
-- TOURNAMENT OF POWER
-- ==========================================
function MapService:_buildTournamentOfPower()
	local zone = Instance.new("Folder")
	zone.Name = "TournamentOfPower"
	zone.Parent = workspace

	local OFFSET = Vector3.new(8000, 0, 0)

	-- The arena stage (floating platform with rocky terrain)
	makePart({
		Name = "ArenaPlatform",
		Size = Vector3.new(300, 8, 300),
		Position = OFFSET + Vector3.new(0, -4, 0),
		Color = Color3.fromRGB(160, 140, 120),
		Material = Enum.Material.Slate,
		Parent = zone,
	})

	-- Uneven rocky terrain on the arena
	for i = 1, 50 do
		local x = math.random(-130, 130)
		local z = math.random(-130, 130)
		makeRock(
			OFFSET + Vector3.new(x, math.random(1, 8), z),
			Vector3.new(
				math.random(5, 20),
				math.random(5, 25),
				math.random(5, 20)
			),
			zone,
			Color3.fromRGB(
				140 + math.random(-30, 30),
				120 + math.random(-30, 30),
				100 + math.random(-30, 30)
			)
		)
	end

	-- Pillars/ruins scattered
	for i = 1, 15 do
		local x = math.random(-120, 120)
		local z = math.random(-120, 120)
		makePart({
			Name = "Pillar_" .. i,
			Size = Vector3.new(4, math.random(10, 30), 4),
			Position = OFFSET + Vector3.new(x, math.random(5, 15), z),
			Color = Color3.fromRGB(180, 170, 150),
			Material = Enum.Material.Concrete,
			Parent = zone,
		})
	end

	-- Spectator benches (Zen-Oh's throne area)
	makePart({
		Name = "ZenOhPlatform",
		Size = Vector3.new(30, 3, 30),
		Position = OFFSET + Vector3.new(0, 60, -180),
		Color = Color3.fromRGB(100, 50, 200),
		Material = Enum.Material.Neon,
		Parent = zone,
	})
	-- Throne
	makePart({
		Name = "Throne",
		Size = Vector3.new(8, 10, 5),
		Position = OFFSET + Vector3.new(0, 68, -180),
		Color = Color3.fromRGB(255, 215, 0),
		Material = Enum.Material.Neon,
		Parent = zone,
	})

	-- Universe team benches (floating platforms around arena)
	for i = 1, 8 do
		local angle = math.rad(i * 45)
		local dist = 180
		makePart({
			Name = "UniverseBench_" .. i,
			Size = Vector3.new(20, 2, 10),
			Position = OFFSET + Vector3.new(math.cos(angle) * dist, 40, math.sin(angle) * dist),
			Color = Color3.fromRGB(150, 150, 200),
			Material = Enum.Material.SmoothPlastic,
			Parent = zone,
		})
	end

	-- Void below (dark)
	makePart({
		Name = "Void",
		Size = Vector3.new(1000, 1, 1000),
		Position = OFFSET + Vector3.new(0, -50, 0),
		Color = Color3.fromRGB(20, 10, 40),
		Material = Enum.Material.SmoothPlastic,
		CanCollide = false,
		Parent = zone,
	})

	-- Starfield around arena
	for i = 1, 80 do
		makePart({
			Name = "Star_" .. i,
			Size = Vector3.new(math.random(1, 3), math.random(1, 3), math.random(1, 3)),
			Position = OFFSET + Vector3.new(
				math.random(-400, 400),
				math.random(-30, 80),
				math.random(-400, 400)
			),
			Color = Color3.new(1, 1, 1),
			Material = Enum.Material.Neon,
			Shape = Enum.PartType.Ball,
			CanCollide = false,
			Parent = zone,
		})
	end

	makeSpawnPoint(OFFSET + Vector3.new(0, 1, 0), "TournamentOfPower", zone)
	makeZoneLabel("Tournament of Power", OFFSET + Vector3.new(0, 30, -130), zone)
end

-- ==========================================
-- ZONE PORTALS (Teleporters between zones)
-- ==========================================
function MapService:_buildZonePortals()
	local portals = Instance.new("Folder")
	portals.Name = "ZonePortals"
	portals.Parent = workspace

	local portalDefs = {
		-- From West City
		{ from = "WestCity", to = "WastelandArena", fromPos = Vector3.new(230, 0, 0), toPos = Vector3.new(600 - 180, 0, 0), levelReq = 10 },
		{ from = "WestCity", to = "KamisLookout", fromPos = Vector3.new(0, 0, -230), toPos = Vector3.new(0, 503, 630), levelReq = 20 },
		{ from = "WestCity", to = "Namek", fromPos = Vector3.new(-230, 0, 0), toPos = Vector3.new(2000, 0, 200), levelReq = 40 },
		-- From Wasteland
		{ from = "WastelandArena", to = "Space", fromPos = Vector3.new(600 + 180, 0, 0), toPos = Vector3.new(4000, 0, 120), levelReq = 60 },
		-- From Space
		{ from = "Space", to = "BeerusPlanet", fromPos = Vector3.new(4000 + 130, 0, 0), toPos = Vector3.new(6000, 3, 80), levelReq = 80 },
		-- From Beerus
		{ from = "BeerusPlanet", to = "TournamentOfPower", fromPos = Vector3.new(6000 + 90, 0, 0), toPos = Vector3.new(8000, 1, 100), levelReq = 100 },
	}

	for _, def in ipairs(portalDefs) do
		-- Create portal at 'from' location
		local portal = Instance.new("Model")
		portal.Name = "Portal_" .. def.from .. "_to_" .. def.to
		portal.Parent = portals

		-- Portal ring
		local ring = makePart({
			Name = "PortalRing",
			Size = Vector3.new(10, 14, 2),
			Position = def.fromPos + Vector3.new(0, 7, 0),
			Color = Color3.fromRGB(100, 50, 255),
			Material = Enum.Material.Neon,
			Transparency = 0.3,
			CanCollide = false,
			Parent = portal,
		})

		-- Swirl center
		local center = makePart({
			Name = "PortalCenter",
			Size = Vector3.new(8, 12, 0.5),
			Position = def.fromPos + Vector3.new(0, 7, 0),
			Color = Color3.fromRGB(150, 100, 255),
			Material = Enum.Material.Neon,
			Transparency = 0.5,
			CanCollide = false,
			Parent = portal,
		})

		-- Particle effect
		local particles = Instance.new("ParticleEmitter")
		particles.Color = ColorSequence.new(Color3.fromRGB(150, 100, 255))
		particles.Size = NumberSequence.new(1)
		particles.Lifetime = NumberRange.new(0.5, 1)
		particles.Rate = 30
		particles.Speed = NumberRange.new(2, 5)
		particles.SpreadAngle = Vector2.new(180, 180)
		particles.LightEmission = 1
		particles.Parent = center

		-- Light
		local light = Instance.new("PointLight")
		light.Color = Color3.fromRGB(150, 100, 255)
		light.Brightness = 2
		light.Range = 20
		light.Parent = center

		-- Label
		makeZoneLabel(def.to .. " (Lv. " .. def.levelReq .. "+)", def.fromPos, portal)

		-- Teleport touch detection
		local touchPart = makePart({
			Name = "TouchDetector",
			Size = Vector3.new(8, 12, 4),
			Position = def.fromPos + Vector3.new(0, 7, 0),
			Transparency = 1,
			CanCollide = false,
			Parent = portal,
		})

		-- Store destination as attributes
		touchPart:SetAttribute("DestinationX", def.toPos.X)
		touchPart:SetAttribute("DestinationY", def.toPos.Y + 5)
		touchPart:SetAttribute("DestinationZ", def.toPos.Z)
		touchPart:SetAttribute("LevelReq", def.levelReq)

		touchPart.Touched:Connect(function(hit)
			local character = hit.Parent
			local player = game.Players:GetPlayerFromCharacter(character)
			if not player then return end

			-- Debounce
			if touchPart:GetAttribute("_cd_" .. player.UserId) then return end
			touchPart:SetAttribute("_cd_" .. player.UserId, true)

			-- Level check
			local DataService = Knit.GetService("DataService")
			local data = DataService:GetData(player)
			if data and data.Level >= def.levelReq then
				local rootPart = character:FindFirstChild("HumanoidRootPart")
				if rootPart then
					rootPart.CFrame = CFrame.new(def.toPos + Vector3.new(0, 5, 0))
				end
			end

			task.delay(3, function()
				touchPart:SetAttribute("_cd_" .. player.UserId, nil)
			end)
		end)
	end
end

-- ==========================================
-- SPAWN AREA (first-time spawn hub)
-- ==========================================
function MapService:_buildSpawnArea()
	-- Initial spawn is in West City center
	-- Create a welcome area
	local hub = Instance.new("Folder")
	hub.Name = "SpawnHub"
	hub.Parent = workspace

	-- Welcome arch
	makePart({
		Name = "ArchLeft",
		Size = Vector3.new(3, 20, 3),
		Position = Vector3.new(-10, 10, 30),
		Color = Color3.fromRGB(255, 215, 0),
		Material = Enum.Material.Neon,
		Parent = hub,
	})
	makePart({
		Name = "ArchRight",
		Size = Vector3.new(3, 20, 3),
		Position = Vector3.new(10, 10, 30),
		Color = Color3.fromRGB(255, 215, 0),
		Material = Enum.Material.Neon,
		Parent = hub,
	})
	makePart({
		Name = "ArchTop",
		Size = Vector3.new(23, 3, 3),
		Position = Vector3.new(0, 21, 30),
		Color = Color3.fromRGB(255, 215, 0),
		Material = Enum.Material.Neon,
		Parent = hub,
	})
	makeZoneLabel("DRAGON BALL ULTIMATE", Vector3.new(0, 15, 30), hub)

	-- Info boards
	local infoPositions = {
		{ pos = Vector3.new(-20, 0, 20), text = "Controls:\nM1/M2 - Attack\nE - Ki Blast\nF - Block\nQ - Vanish\nC - Charge Ki\nT - Transform" },
		{ pos = Vector3.new(20, 0, 20), text = "Tips:\n- Train to gain XP\n- Allocate stat points\n- Unlock transformations\n- Complete saga quests\n- Challenge other players!" },
	}

	for _, info in ipairs(infoPositions) do
		local board = makePart({
			Name = "InfoBoard",
			Size = Vector3.new(12, 10, 0.5),
			Position = info.pos + Vector3.new(0, 7, 0),
			Color = Color3.fromRGB(40, 40, 60),
			Material = Enum.Material.SmoothPlastic,
			Parent = hub,
		})
		local gui = Instance.new("SurfaceGui")
		gui.Face = Enum.NormalId.Front
		gui.Parent = board
		local label = Instance.new("TextLabel")
		label.Size = UDim2.fromScale(1, 1)
		label.BackgroundTransparency = 1
		label.Text = info.text
		label.TextColor3 = Color3.new(1, 1, 1)
		label.TextSize = 20
		label.Font = Enum.Font.Gotham
		label.TextWrapped = true
		label.TextYAlignment = Enum.TextYAlignment.Top
		label.Parent = gui
	end
end

return MapService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="82">
          <Properties>
            <string name="Name">NPCService</string>
            <string name="Source"><![CDATA[--[[
	NPCService.lua
	Spawns and manages NPCs in the world.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Settings = require(ReplicatedStorage.Shared.Constants.Settings)

local NPCService = Knit.CreateService({
	Name = "NPCService",
	Client = {},
})

-- NPC definitions (will grow as content is added)
local NPC_DEFINITIONS = {
	TrainingDummy = {
		displayName = "Training Dummy",
		level = 1,
		hp = 50,
		damage = 5,
		xpReward = 20,
		zeniReward = 10,
		aggressive = false,
		respawnTime = 10,
	},
	Saibaman = {
		displayName = "Saibaman",
		level = 5,
		hp = 300,
		damage = 25,
		xpReward = 50,
		zeniReward = 25,
		aggressive = true,
		respawnTime = 15,
	},
	Nappa = {
		displayName = "Nappa",
		level = 15,
		hp = 1500,
		damage = 65,
		xpReward = 300,
		zeniReward = 150,
		aggressive = true,
		respawnTime = 60,
		isBoss = false,
	},
	Vegeta_Saiyan = {
		displayName = "Vegeta (Saiyan Saga)",
		level = 25,
		hp = 3000,
		damage = 120,
		xpReward = 800,
		zeniReward = 400,
		aggressive = true,
		respawnTime = 120,
		isBoss = true,
	},
	FriezaSoldier = {
		displayName = "Frieza Soldier",
		level = 30,
		hp = 600,
		damage = 55,
		xpReward = 100,
		zeniReward = 50,
		aggressive = true,
		respawnTime = 10,
	},
	Guldo = {
		displayName = "Guldo",
		level = 35,
		hp = 1200,
		damage = 80,
		xpReward = 200,
		zeniReward = 100,
		aggressive = true,
		respawnTime = 60,
	},
	Recoome = {
		displayName = "Recoome",
		level = 40,
		hp = 1800,
		damage = 100,
		xpReward = 300,
		zeniReward = 150,
		aggressive = true,
		respawnTime = 60,
	},
	Burter = {
		displayName = "Burter",
		level = 40,
		hp = 1600,
		damage = 95,
		xpReward = 280,
		zeniReward = 140,
		aggressive = true,
		respawnTime = 60,
	},
	Jeice = {
		displayName = "Jeice",
		level = 40,
		hp = 1600,
		damage = 95,
		xpReward = 280,
		zeniReward = 140,
		aggressive = true,
		respawnTime = 60,
	},
	CaptainGinyu = {
		displayName = "Captain Ginyu",
		level = 45,
		hp = 2500,
		damage = 140,
		xpReward = 500,
		zeniReward = 250,
		aggressive = true,
		respawnTime = 90,
		isBoss = true,
	},
	Frieza_Final = {
		displayName = "Frieza (Final Form)",
		level = 55,
		hp = 8000,
		damage = 250,
		xpReward = 2000,
		zeniReward = 1000,
		aggressive = true,
		respawnTime = 180,
		isBoss = true,
	},
	CellJunior = {
		displayName = "Cell Junior",
		level = 60,
		hp = 1200,
		damage = 130,
		xpReward = 250,
		zeniReward = 120,
		aggressive = true,
		respawnTime = 15,
	},
	PerfectCell = {
		displayName = "Perfect Cell",
		level = 80,
		hp = 15000,
		damage = 350,
		xpReward = 5000,
		zeniReward = 2500,
		aggressive = true,
		respawnTime = 300,
		isBoss = true,
	},
	Dabura = {
		displayName = "Dabura",
		level = 90,
		hp = 6000,
		damage = 280,
		xpReward = 1500,
		zeniReward = 750,
		aggressive = true,
		respawnTime = 120,
	},
	Pui_Pui = {
		displayName = "Pui Pui",
		level = 85,
		hp = 2500,
		damage = 180,
		xpReward = 600,
		zeniReward = 300,
		aggressive = true,
		respawnTime = 60,
	},
	Yakon = {
		displayName = "Yakon",
		level = 88,
		hp = 3500,
		damage = 220,
		xpReward = 800,
		zeniReward = 400,
		aggressive = true,
		respawnTime = 60,
	},
	KidBuu_Boss = {
		displayName = "Kid Buu",
		level = 120,
		hp = 25000,
		damage = 500,
		xpReward = 8000,
		zeniReward = 4000,
		aggressive = true,
		respawnTime = 600,
		isBoss = true,
	},
}

-- Active NPC instances
local activeNPCs: { [Model]: { definition: any, currentHp: number, spawnPosition: Vector3 } } = {}

function NPCService:KnitInit() end

function NPCService:KnitStart()
	-- NPCs will be spawned based on zone loading
	-- For now, this service provides the framework
	print("[NPCService] Ready. NPC definitions loaded: " .. tostring(self:_countDefinitions()))
end

function NPCService:_countDefinitions(): number
	local count = 0
	for _ in pairs(NPC_DEFINITIONS) do
		count = count + 1
	end
	return count
end

function NPCService:GetDefinition(npcId: string)
	return NPC_DEFINITIONS[npcId]
end

function NPCService:DamageNPC(npcModel: Model, damage: number, attackerPlayer: Player): boolean
	local npc = activeNPCs[npcModel]
	if not npc then return false end

	npc.currentHp = npc.currentHp - damage

	if npc.currentHp <= 0 then
		self:_handleNPCDeath(npcModel, attackerPlayer)
		return true -- NPC died
	end

	return false
end

function NPCService:_handleNPCDeath(npcModel: Model, killerPlayer: Player)
	local npc = activeNPCs[npcModel]
	if not npc then return end

	local def = npc.definition

	-- Award rewards
	local ProgressionService = Knit.GetService("ProgressionService")
	local QuestService = Knit.GetService("QuestService")

	ProgressionService:AwardXP(killerPlayer, def.xpReward)
	ProgressionService:AwardZeni(killerPlayer, def.zeniReward)

	-- Update quest objectives
	-- Find the NPC ID from definition
	for npcId, d in pairs(NPC_DEFINITIONS) do
		if d == def then
			QuestService:UpdateObjective(killerPlayer, "defeat_npc", npcId, 1)
			break
		end
	end

	-- Majin passive: restore HP/Ki on kill
	local DataService = Knit.GetService("DataService")
	local CombatService = Knit.GetService("CombatService")
	local data = DataService:GetData(killerPlayer)
	if data and data.Race == "Majin" then
		local combat = CombatService:GetCombatState(killerPlayer)
		local race = require(ReplicatedStorage.Shared.Constants.Races):Get("Majin")
		if combat and race then
			combat.hp = math.min(combat.maxHp, combat.hp + combat.maxHp * race.passive.hpRestore)
			combat.ki = math.min(combat.maxKi, combat.ki + combat.maxKi * race.passive.kiRestore)
		end
	end

	-- Remove NPC
	activeNPCs[npcModel] = nil
	npcModel:Destroy()

	-- Respawn after delay
	task.delay(def.respawnTime, function()
		self:_spawnNPC(def, npc.spawnPosition)
	end)
end

function NPCService:_spawnNPC(definition: any, position: Vector3)
	-- Create a basic NPC model (placeholder — real models would come from assets)
	local model = Instance.new("Model")
	model.Name = definition.displayName

	local rootPart = Instance.new("Part")
	rootPart.Name = "HumanoidRootPart"
	rootPart.Size = Vector3.new(2, 2, 1)
	rootPart.Position = position
	rootPart.Anchored = true
	rootPart.Parent = model

	local humanoid = Instance.new("Humanoid")
	humanoid.MaxHealth = definition.hp
	humanoid.Health = definition.hp
	humanoid.Parent = model

	model.PrimaryPart = rootPart
	model.Parent = workspace

	activeNPCs[model] = {
		definition = definition,
		currentHp = definition.hp,
		spawnPosition = position,
	}

	return model
end

function NPCService:SpawnNPCById(npcId: string, position: Vector3): Model?
	local def = NPC_DEFINITIONS[npcId]
	if not def then
		warn("[NPCService] Unknown NPC ID: " .. npcId)
		return nil
	end
	return self:_spawnNPC(def, position)
end

return NPCService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="83">
          <Properties>
            <string name="Name">NPCSpawnService</string>
            <string name="Source"><![CDATA[--[[
	NPCSpawnService.lua
	Spawns NPC models in their respective zones using NPCModelBuilder.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Knit = require(ReplicatedStorage.Packages.Knit)
local NPCModelBuilder = require(script.Parent.Parent.Modules.NPCModelBuilder)

local NPCSpawnService = Knit.CreateService({
	Name = "NPCSpawnService",
	Client = {},
})

-- NPC spawn definitions: { npcId, zone, position, count }
local SPAWN_TABLE = {
	-- West City
	{ npcId = "TrainingDummy", zone = "WestCity", positions = {
		Vector3.new(10, 1, 100), Vector3.new(-10, 1, 100), Vector3.new(20, 1, 110),
		Vector3.new(-20, 1, 110), Vector3.new(0, 1, 120),
	}},
	{ npcId = "Saibaman", zone = "WestCity", positions = {
		Vector3.new(150, 1, 150), Vector3.new(160, 1, 140), Vector3.new(140, 1, 160),
		Vector3.new(-150, 1, 150), Vector3.new(-160, 1, 140), Vector3.new(-140, 1, 160),
	}},
	-- Quest NPCs
	{ npcId = "Nappa", zone = "WestCity", positions = { Vector3.new(200, 1, -100) }, isBoss = false },
	{ npcId = "Vegeta_Saiyan", zone = "WestCity", positions = { Vector3.new(-200, 1, -150) }, isBoss = true },
	{ npcId = "Raditz", zone = "WestCity", positions = { Vector3.new(180, 1, 80) }},

	-- Wasteland Arena
	{ npcId = "Saibaman", zone = "WastelandArena", positions = {
		Vector3.new(620, 1, 50), Vector3.new(640, 1, -30), Vector3.new(580, 1, 70),
		Vector3.new(560, 1, -60), Vector3.new(650, 1, 20),
	}},

	-- Namek
	{ npcId = "FriezaSoldier", zone = "Namek", positions = {
		Vector3.new(2050, 1, 50), Vector3.new(2080, 1, -30), Vector3.new(2020, 1, 80),
		Vector3.new(1950, 1, -50), Vector3.new(1980, 1, 100),
		Vector3.new(2100, 1, -80), Vector3.new(2040, 1, 120),
		Vector3.new(1960, 1, -120), Vector3.new(2070, 1, -100), Vector3.new(2030, 1, 60),
	}},
	{ npcId = "Guldo", zone = "Namek", positions = { Vector3.new(2100, 1, 100) }},
	{ npcId = "Recoome", zone = "Namek", positions = { Vector3.new(2120, 1, 80) }},
	{ npcId = "Burter", zone = "Namek", positions = { Vector3.new(2140, 1, 60) }},
	{ npcId = "Jeice", zone = "Namek", positions = { Vector3.new(2160, 1, 40) }},
	{ npcId = "CaptainGinyu", zone = "Namek", positions = { Vector3.new(2130, 1, -50) }, isBoss = true },
	{ npcId = "Frieza_Final", zone = "Namek", positions = { Vector3.new(2000, 1, -180) }, isBoss = true },

	-- Wasteland - Cell saga
	{ npcId = "CellJunior", zone = "WastelandArena", positions = {
		Vector3.new(600, 1, -80), Vector3.new(610, 1, -100), Vector3.new(590, 1, -70),
		Vector3.new(620, 1, -90), Vector3.new(580, 1, -110),
		Vector3.new(630, 1, -75), Vector3.new(570, 1, -95),
	}},
	{ npcId = "PerfectCell", zone = "WastelandArena", positions = { Vector3.new(600, 1, -150) }, isBoss = true },

	-- Space
	{ npcId = "FriezaSoldierElite", zone = "Space", positions = {
		Vector3.new(4050, 1, 50), Vector3.new(4080, 1, -30), Vector3.new(4020, 1, 80),
		Vector3.new(3950, 1, -50), Vector3.new(3980, 1, 100),
	}},
	{ npcId = "GoldenFrieza_Boss", zone = "Space", positions = { Vector3.new(4000, 1, -100) }, isBoss = true },

	-- Beerus Planet
	{ npcId = "Whis", zone = "BeerusPlanet", positions = { Vector3.new(5950, 3, 30) }},
	{ npcId = "Beerus", zone = "BeerusPlanet", positions = { Vector3.new(6000, 3, -80) }, isBoss = true },

	-- Tournament of Power
	{ npcId = "UniverseFighter", zone = "TournamentOfPower", positions = {
		Vector3.new(8050, 1, 50), Vector3.new(8080, 1, -30), Vector3.new(8020, 1, 80),
		Vector3.new(7950, 1, -50), Vector3.new(7980, 1, 100),
		Vector3.new(8100, 1, -80), Vector3.new(8040, 1, 120),
		Vector3.new(7960, 1, -120), Vector3.new(8070, 1, -100), Vector3.new(8030, 1, 60),
	}},
	{ npcId = "Botamo", zone = "TournamentOfPower", positions = { Vector3.new(8050, 1, -50) }},
	{ npcId = "Frost", zone = "TournamentOfPower", positions = { Vector3.new(8080, 1, -70) }},
	{ npcId = "Magetta", zone = "TournamentOfPower", positions = { Vector3.new(7970, 1, -40) }},
	{ npcId = "Cabba", zone = "TournamentOfPower", positions = { Vector3.new(8020, 1, -90) }},
	{ npcId = "Hit", zone = "TournamentOfPower", positions = { Vector3.new(7950, 1, -80) }, isBoss = true },
	{ npcId = "Jiren", zone = "TournamentOfPower", positions = { Vector3.new(8000, 1, -130) }, isBoss = true },

	-- Broly
	{ npcId = "Broly_FullPower", zone = "Space", positions = { Vector3.new(4100, 1, 100) }, isBoss = true },

	-- Buu saga (Kami's Lookout area)
	{ npcId = "Dabura", zone = "KamisLookout", positions = { Vector3.new(30, 503, 580) }},
	{ npcId = "Pui_Pui", zone = "KamisLookout", positions = { Vector3.new(-30, 503, 560) }},
	{ npcId = "Yakon", zone = "KamisLookout", positions = { Vector3.new(0, 503, 550) }},
	{ npcId = "KidBuu_Boss", zone = "KamisLookout", positions = { Vector3.new(0, 503, 530) }, isBoss = true },
}

-- Active NPC references
local activeNPCs: { [Model]: { npcId: string, definition: any, currentHp: number, spawnPos: Vector3, respawnTime: number, isBoss: boolean } } = {}

function NPCSpawnService:KnitInit() end

function NPCSpawnService:KnitStart()
	-- Wait for map to generate
	task.wait(2)

	local NPCService = Knit.GetService("NPCService")

	-- Spawn all NPCs
	for _, spawnDef in ipairs(SPAWN_TABLE) do
		local definition = NPCService:GetDefinition(spawnDef.npcId)
		if not definition then
			warn("[NPCSpawnService] Unknown NPC: " .. spawnDef.npcId)
			continue
		end

		for _, pos in ipairs(spawnDef.positions) do
			self:_spawnNPC(spawnDef.npcId, definition, pos, spawnDef.isBoss)
		end
	end

	print("[NPCSpawnService] All NPCs spawned!")
end

function NPCSpawnService:_spawnNPC(npcId: string, definition: any, position: Vector3, isBoss: boolean?)
	local model = NPCModelBuilder.Build(
		npcId,
		definition.displayName,
		definition.hp,
		position,
		isBoss or definition.isBoss
	)
	model.Parent = workspace

	activeNPCs[model] = {
		npcId = npcId,
		definition = definition,
		currentHp = definition.hp,
		spawnPos = position,
		respawnTime = definition.respawnTime,
		isBoss = isBoss or definition.isBoss or false,
	}

	-- NPC AI: fight back when players are nearby and aggressive
	if definition.aggressive then
		task.spawn(function()
			self:_runNPCAI(model)
		end)
	end

	-- Keep ProximityPrompt for non-aggressive NPCs (quest givers, shops)
	local rootPart = model:FindFirstChild("HumanoidRootPart")
	if rootPart then
		local prompt = rootPart:FindFirstChildOfClass("ProximityPrompt")
		if prompt then
			if definition.aggressive then
				-- Remove "Fight" prompt for aggressive NPCs — combat is real-time now
				prompt:Destroy()
			else
				prompt.Triggered:Connect(function(player)
					self:_onNPCInteract(player, model)
				end)
			end
		end
	end

	return model
end

function NPCSpawnService:_onNPCInteract(player: Player, npcModel: Model)
	local npc = activeNPCs[npcModel]
	if not npc then return end

	-- Simple combat: deal damage to NPC based on player stats
	local CombatService = Knit.GetService("CombatService")
	local combat = CombatService:GetCombatState(player)
	if not combat then return end

	local damage = math.max(1, combat.effectiveStats.Melee * 2)
	npc.currentHp = npc.currentHp - damage

	-- Update health bar
	local humanoid = npcModel:FindFirstChildOfClass("Humanoid")
	if humanoid then
		humanoid.Health = math.max(0, npc.currentHp)
	end

	-- Update visual health bar
	local head = npcModel:FindFirstChild("Head")
	if head then
		local billboard = head:FindFirstChild("HealthDisplay")
		if billboard then
			local hpBg = billboard:FindFirstChildOfClass("Frame")
			if hpBg then
				local hpBar = hpBg:FindFirstChild("HPBar")
				if hpBar then
					hpBar.Size = UDim2.fromScale(math.max(0, npc.currentHp / npc.definition.hp), 1)
				end
			end
		end
	end

	-- Check death
	if npc.currentHp <= 0 then
		self:_handleNPCDeath(npcModel, player)
	end
end

function NPCSpawnService:_handleNPCDeath(npcModel: Model, killer: Player)
	local npc = activeNPCs[npcModel]
	if not npc then return end

	-- Award rewards
	local NPCService = Knit.GetService("NPCService")
	local ProgressionService = Knit.GetService("ProgressionService")
	local QuestService = Knit.GetService("QuestService")

	ProgressionService:AwardXP(killer, npc.definition.xpReward)
	ProgressionService:AwardZeni(killer, npc.definition.zeniReward)
	QuestService:UpdateObjective(killer, "defeat_npc", npc.npcId, 1)

	-- Majin passive
	local DataService = Knit.GetService("DataService")
	local CombatService = Knit.GetService("CombatService")
	local data = DataService:GetData(killer)
	if data and data.Race == "Majin" then
		local Races = require(ReplicatedStorage.Shared.Constants.Races)
		local combat = CombatService:GetCombatState(killer)
		local race = Races:Get("Majin")
		if combat and race then
			combat.hp = math.min(combat.maxHp, combat.hp + combat.maxHp * race.passive.hpRestore)
			combat.ki = math.min(combat.maxKi, combat.ki + combat.maxKi * race.passive.kiRestore)
		end
	end

	-- Store spawn info before cleanup
	local spawnPos = npc.spawnPos
	local npcId = npc.npcId
	local definition = npc.definition
	local isBoss = npc.isBoss
	local respawnTime = npc.respawnTime

	-- Remove
	activeNPCs[npcModel] = nil
	npcModel:Destroy()

	-- Respawn
	task.delay(respawnTime, function()
		self:_spawnNPC(npcId, definition, spawnPos, isBoss)
	end)
end

-- ==========================================
-- NPC AI: Simple aggro + attack loop
-- ==========================================
function NPCSpawnService:_runNPCAI(npcModel: Model)
	local Players = game:GetService("Players")

	while npcModel and npcModel.Parent and activeNPCs[npcModel] do
		local npc = activeNPCs[npcModel]
		if not npc then break end

		local npcRoot = npcModel:FindFirstChild("HumanoidRootPart")
		local npcHumanoid = npcModel:FindFirstChildOfClass("Humanoid")
		if not npcRoot or not npcHumanoid or npcHumanoid.Health <= 0 then break end

		-- Find nearest player in aggro range (20 studs)
		local nearestPlayer = nil
		local nearestDist = 20

		for _, player in ipairs(Players:GetPlayers()) do
			local character = player.Character
			if not character then continue end
			local humanoid = character:FindFirstChildOfClass("Humanoid")
			if not humanoid or humanoid.Health <= 0 then continue end

			local playerRoot = character:FindFirstChild("HumanoidRootPart")
			if not playerRoot then continue end

			local dist = (npcRoot.Position - playerRoot.Position).Magnitude
			if dist < nearestDist then
				nearestDist = dist
				nearestPlayer = player
			end
		end

		if nearestPlayer then
			local playerChar = nearestPlayer.Character
			if playerChar then
				local playerRoot = playerChar:FindFirstChild("HumanoidRootPart")
				if playerRoot and npcRoot then
					-- Face the player
					local direction = (playerRoot.Position - npcRoot.Position)
					direction = Vector3.new(direction.X, 0, direction.Z)
					if direction.Magnitude > 0.1 then
						npcRoot.CFrame = CFrame.lookAt(npcRoot.Position, npcRoot.Position + direction)
					end

					-- Attack if in melee range
					if nearestDist <= 8 then
						local CombatService = Knit.GetService("CombatService")
						local combat = CombatService:GetCombatState(nearestPlayer)
						if combat then
							local damage = npc.definition.damage or 10
							local isBlocking = combat.state == "Blocking"

							if isBlocking then
								damage = math.floor(damage * 0.4)
							end

							combat.hp = math.max(0, combat.hp - damage)
							combat.lastCombatTime = tick()

							-- Notify player of NPC attack
							CombatService.Client.CombatResult:Fire(nearestPlayer, {
								type = "melee_received",
								attacker = npcModel,
								damage = damage,
								blocked = isBlocking,
								guardBreak = false,
								launcher = false,
							})

							-- Check player death
							if combat.hp <= 0 then
								CombatService:_handleDeath(nearestPlayer, nil)
							end
						end
					end
				end
			end
		end

		-- Attack interval: bosses hit fast, normal NPCs moderate
		local attackInterval = npc.isBoss and 1.0 or 1.8
		task.wait(attackInterval)
	end
end

return NPCSpawnService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="84">
          <Properties>
            <string name="Name">ProgressionService</string>
            <string name="Source"><![CDATA[--[[
	ProgressionService.lua
	Handles XP, leveling, stat allocation, and rebirth.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Knit = require(ReplicatedStorage.Packages.Knit)
local Stats = require(ReplicatedStorage.Shared.Constants.Stats)
local Settings = require(ReplicatedStorage.Shared.Constants.Settings)
local Races = require(ReplicatedStorage.Shared.Constants.Races)

local ProgressionService = Knit.CreateService({
	Name = "ProgressionService",
	Client = {},
})

function ProgressionService:KnitInit() end
function ProgressionService:KnitStart() end

function ProgressionService:AwardXP(player: Player, amount: number)
	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if not data then return end

	-- Apply Human XP bonus
	local race = Races:Get(data.Race)
	if race and race.passive.id == "Adaptability" then
		amount = math.floor(amount * (1 + race.passive.xpBonus))
	end

	DataService:UpdateData(player, function(d)
		d.XP = d.XP + amount

		-- Check for level ups
		while d.Level < Settings.Progression.MAX_LEVEL do
			local required = Stats:GetRequiredXP(d.Level)
			if d.XP >= required then
				d.XP = d.XP - required
				d.Level = d.Level + 1
				d.StatPoints = d.StatPoints + Settings.Progression.STAT_POINTS_PER_LEVEL
			else
				break
			end
		end
	end)

	-- Refresh combat stats after leveling
	local CombatService = Knit.GetService("CombatService")
	CombatService:RefreshStats(player)
end

function ProgressionService:AwardZeni(player: Player, amount: number)
	local DataService = Knit.GetService("DataService")
	DataService:UpdateData(player, function(d)
		d.Zeni = d.Zeni + amount
	end)
end

function ProgressionService:AllocateStat(player: Player, statName: string, points: number): (boolean, string?)
	if not table.find(Stats.Names, statName) then
		return false, "Invalid stat name"
	end

	if points < 1 then
		return false, "Must allocate at least 1 point"
	end

	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if not data then return false, "Data not loaded" end

	if data.StatPoints < points then
		return false, "Not enough stat points"
	end

	DataService:UpdateData(player, function(d)
		d.StatPoints = d.StatPoints - points
		d.Stats[statName] = d.Stats[statName] + points
	end)

	local CombatService = Knit.GetService("CombatService")
	CombatService:RefreshStats(player)

	return true, nil
end

function ProgressionService:Rebirth(player: Player): (boolean, string?)
	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if not data then return false, "Data not loaded" end

	if data.Level < Settings.Progression.MAX_LEVEL then
		return false, "Must be max level to rebirth"
	end

	if data.Rebirths >= Settings.Progression.MAX_REBIRTHS then
		return false, "Maximum rebirths reached"
	end

	DataService:UpdateData(player, function(d)
		d.Rebirths = d.Rebirths + 1
		d.Level = 1
		d.XP = 0
		d.StatPoints = 3

		-- Reset allocated stats to base
		local race = Races:Get(d.Race)
		if race then
			for stat, value in pairs(race.baseStats) do
				d.Stats[stat] = value
			end
		end

		-- Keep: transformations, moves, zeni, quest progress, appearance
	end)

	local CombatService = Knit.GetService("CombatService")
	CombatService:RefreshStats(player)

	return true, nil
end

-- Client methods

function ProgressionService.Client:AllocateStat(player: Player, statName: string, points: number)
	return self.Server:AllocateStat(player, statName, points)
end

function ProgressionService.Client:RequestRebirth(player: Player)
	return self.Server:Rebirth(player)
end

return ProgressionService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="85">
          <Properties>
            <string name="Name">PvpService</string>
            <string name="Source"><![CDATA[--[[
	PvpService.lua
	Manages PvP matchmaking, arenas, and tournaments.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Knit = require(ReplicatedStorage.Packages.Knit)
local Settings = require(ReplicatedStorage.Shared.Constants.Settings)

local EloCalculator = require(script.Parent.Parent.Modules.EloCalculator)

local PvpService = Knit.CreateService({
	Name = "PvpService",
	Client = {
		PvpMatchStart = Knit.CreateSignal(),
		PvpMatchEnd = Knit.CreateSignal(),
	},
})

-- Queue: { { player: Player, elo: number, joinTime: number } }
local rankedQueue: { { player: Player, elo: number, joinTime: number } } = {}
local activeMatches: { [string]: {
	player1: Player,
	player2: Player,
	mode: string,
	round: number,
	player1Wins: number,
	player2Wins: number,
	startTime: number,
} } = {}

function PvpService:KnitInit() end

function PvpService:KnitStart()
	-- Matchmaking tick
	task.spawn(function()
		while true do
			task.wait(2) -- Check queue every 2 seconds
			self:_processQueue()
		end
	end)
end

function PvpService:_processQueue()
	if #rankedQueue < 2 then return end

	-- Sort by ELO for better matches
	table.sort(rankedQueue, function(a, b) return a.elo < b.elo end)

	-- Try to match adjacent players
	local i = 1
	while i < #rankedQueue do
		local a = rankedQueue[i]
		local b = rankedQueue[i + 1]

		local quality = EloCalculator.GetMatchQuality(a.elo, b.elo)
		local waitTime = tick() - math.min(a.joinTime, b.joinTime)

		-- Accept match if quality is decent or players have waited long enough
		if quality > 0.5 or waitTime > Settings.PvP.QUEUE_TIMEOUT then
			table.remove(rankedQueue, i + 1)
			table.remove(rankedQueue, i)
			self:_startMatch(a.player, b.player, "Ranked")
		else
			i = i + 1
		end
	end
end

function PvpService:_startMatch(player1: Player, player2: Player, mode: string)
	local matchId = player1.UserId .. "_" .. player2.UserId .. "_" .. tick()

	activeMatches[matchId] = {
		player1 = player1,
		player2 = player2,
		mode = mode,
		round = 1,
		player1Wins = 0,
		player2Wins = 0,
		startTime = tick(),
	}

	-- Notify both players
	self.Client.PvpMatchStart:Fire(player1, {
		matchId = matchId,
		opponent = player2,
		mode = mode,
	})
	self.Client.PvpMatchStart:Fire(player2, {
		matchId = matchId,
		opponent = player1,
		mode = mode,
	})

	-- TODO: Teleport players to arena
end

function PvpService:ReportRoundWin(matchId: string, winner: Player)
	local match = activeMatches[matchId]
	if not match then return end

	if winner == match.player1 then
		match.player1Wins = match.player1Wins + 1
	else
		match.player2Wins = match.player2Wins + 1
	end

	local requiredWins = math.ceil(Settings.PvP.MATCH_ROUNDS / 2)

	if match.player1Wins >= requiredWins then
		self:_endMatch(matchId, match.player1, match.player2)
	elseif match.player2Wins >= requiredWins then
		self:_endMatch(matchId, match.player2, match.player1)
	else
		match.round = match.round + 1
		-- Next round starts
	end
end

function PvpService:_endMatch(matchId: string, winner: Player, loser: Player)
	local match = activeMatches[matchId]
	if not match then return end

	local DataService = Knit.GetService("DataService")
	local ProgressionService = Knit.GetService("ProgressionService")

	-- Update ELO
	local winnerData = DataService:GetData(winner)
	local loserData = DataService:GetData(loser)

	if winnerData and loserData then
		local newWinnerElo, newLoserElo = EloCalculator.Calculate(winnerData.PvpElo, loserData.PvpElo)

		DataService:UpdateData(winner, function(d)
			d.PvpElo = newWinnerElo
			d.PvpWins = d.PvpWins + 1
		end)

		DataService:UpdateData(loser, function(d)
			d.PvpElo = newLoserElo
			d.PvpLosses = d.PvpLosses + 1
		end)
	end

	-- Award XP/Zeni
	ProgressionService:AwardXP(winner, Settings.Progression.PVP_WIN_XP_BASE)
	ProgressionService:AwardZeni(winner, Settings.Economy.PVP_WIN_ZENI)

	-- Notify players
	self.Client.PvpMatchEnd:Fire(winner, { result = "win", matchId = matchId })
	self.Client.PvpMatchEnd:Fire(loser, { result = "loss", matchId = matchId })

	activeMatches[matchId] = nil
end

-- Client methods

function PvpService.Client:JoinQueue(player: Player, mode: string)
	if mode ~= "Ranked" then return end

	-- Check not already in queue
	for _, entry in ipairs(rankedQueue) do
		if entry.player == player then return end
	end

	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if not data then return end

	table.insert(rankedQueue, {
		player = player,
		elo = data.PvpElo,
		joinTime = tick(),
	})
end

function PvpService.Client:LeaveQueue(player: Player)
	for i, entry in ipairs(rankedQueue) do
		if entry.player == player then
			table.remove(rankedQueue, i)
			return
		end
	end
end

return PvpService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="86">
          <Properties>
            <string name="Name">QuestService</string>
            <string name="Source"><![CDATA[--[[
	QuestService.lua
	Manages quest acceptance, progress tracking, and completion.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Knit = require(ReplicatedStorage.Packages.Knit)
local Sagas = require(ReplicatedStorage.Shared.Constants.Sagas)

local QuestService = Knit.CreateService({
	Name = "QuestService",
	Client = {
		QuestUpdate = Knit.CreateSignal(),
		QuestComplete = Knit.CreateSignal(),
	},
})

function QuestService:KnitInit() end
function QuestService:KnitStart() end

function QuestService:AcceptQuest(player: Player, questId: string): (boolean, string?)
	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if not data then return false, "Data not loaded" end

	local quest, saga = Sagas:GetQuestById(questId)
	if not quest then return false, "Quest not found" end

	-- Level check
	if data.Level < saga.levelRequirement then
		return false, "Level too low for this saga"
	end

	-- Already active
	if data.QuestProgress[questId] then
		return false, "Quest already active"
	end

	-- Initialize quest progress
	local objectives = {}
	for _, obj in ipairs(quest.objectives) do
		table.insert(objectives, {
			type = obj.type,
			npcId = obj.npcId,
			current = 0,
			target = obj.count or obj.duration or 1,
			completed = false,
		})
	end

	DataService:UpdateData(player, function(d)
		d.QuestProgress[questId] = {
			questId = questId,
			objectives = objectives,
			completed = false,
		}
	end)

	return true, nil
end

function QuestService:UpdateObjective(player: Player, objectiveType: string, npcId: string?, amount: number?)
	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if not data then return end

	local updated = false

	DataService:UpdateData(player, function(d)
		for questId, progress in pairs(d.QuestProgress) do
			if progress.completed then continue end

			for _, obj in ipairs(progress.objectives) do
				if obj.completed then continue end
				if obj.type ~= objectiveType then continue end
				if npcId and obj.npcId ~= npcId then continue end

				obj.current = obj.current + (amount or 1)
				if obj.current >= obj.target then
					obj.current = obj.target
					obj.completed = true
				end
				updated = true
			end

			-- Check if all objectives complete
			local allDone = true
			for _, obj in ipairs(progress.objectives) do
				if not obj.completed then
					allDone = false
					break
				end
			end

			if allDone then
				progress.completed = true
				self:_completeQuest(player, questId)
			end
		end
	end)

	if updated then
		self.Client.QuestUpdate:Fire(player, data.QuestProgress)
	end
end

function QuestService:_completeQuest(player: Player, questId: string)
	local quest, saga = Sagas:GetQuestById(questId)
	if not quest then return end

	local ProgressionService = Knit.GetService("ProgressionService")
	local TransformationService = Knit.GetService("TransformationService")
	local DataService = Knit.GetService("DataService")

	-- Award rewards
	if quest.rewards.xp then
		ProgressionService:AwardXP(player, quest.rewards.xp)
	end
	if quest.rewards.zeni then
		ProgressionService:AwardZeni(player, quest.rewards.zeni)
	end

	-- Unlock transformations
	if quest.rewards.transformations then
		for _, formId in ipairs(quest.rewards.transformations) do
			TransformationService:UnlockTransformation(player, formId)
		end
	end

	-- Unlock moves
	if quest.rewards.moves then
		DataService:UpdateData(player, function(d)
			for _, moveId in ipairs(quest.rewards.moves) do
				if not table.find(d.UnlockedMoves, moveId) then
					table.insert(d.UnlockedMoves, moveId)
				end
			end
		end)
	end

	-- Remove from active quests
	DataService:UpdateData(player, function(d)
		d.QuestProgress[questId] = nil
	end)

	-- Check if saga is complete
	local allSagaQuestsDone = true
	for _, q in ipairs(saga.quests) do
		if q.id ~= questId then
			local data = DataService:GetData(player)
			-- Check if other quests were completed previously (simplified)
		end
	end

	self.Client.QuestComplete:Fire(player, {
		questId = questId,
		rewards = quest.rewards,
	})
end

function QuestService:AbandonQuest(player: Player, questId: string)
	local DataService = Knit.GetService("DataService")
	DataService:UpdateData(player, function(d)
		d.QuestProgress[questId] = nil
	end)
end

-- Client methods

function QuestService.Client:AcceptQuest(player: Player, questId: string)
	return self.Server:AcceptQuest(player, questId)
end

function QuestService.Client:AbandonQuest(player: Player, questId: string)
	self.Server:AbandonQuest(player, questId)
end

return QuestService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="87">
          <Properties>
            <string name="Name">TransformationService</string>
            <string name="Source"><![CDATA[--[[
	TransformationService.lua
	Handles transformation requests, validation, stat application, and ki drain.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Transformations = require(ReplicatedStorage.Shared.Constants.Transformations)
local Races = require(ReplicatedStorage.Shared.Constants.Races)

local TransformationService = Knit.CreateService({
	Name = "TransformationService",
	Client = {
		TransformSync = Knit.CreateSignal(),
	},
})

-- Track active transformations and their ki drain
local activeTransforms: { [Player]: {
	formId: string,
	kiDrainPerSecond: number,
} } = {}

function TransformationService:KnitInit() end

function TransformationService:KnitStart()
	-- Ki drain tick
	RunService.Heartbeat:Connect(function(dt)
		self:_tickKiDrain(dt)
	end)
end

function TransformationService:_tickKiDrain(dt: number)
	local CombatService = Knit.GetService("CombatService")

	for player, transform in pairs(activeTransforms) do
		local combat = CombatService:GetCombatState(player)
		if not combat then
			activeTransforms[player] = nil
			continue
		end

		-- Apply race ki drain reduction if applicable
		local DataService = Knit.GetService("DataService")
		local data = DataService:GetData(player)
		local drainReduction = 0

		if data then
			local race = Races:Get(data.Race)
			if race and race.passive.id == "Suppression" then
				drainReduction = race.passive.kiDrainReduction
			elseif race and race.passive.id == "InfiniteEnergy" then
				drainReduction = race.passive.transformKiReduction
			end
		end

		local effectiveDrain = transform.kiDrainPerSecond * (1 - drainReduction)
		combat.ki = combat.ki - (effectiveDrain * dt)

		-- Revert if ki runs out
		if combat.ki <= 0 then
			combat.ki = 0
			self:RevertTransformation(player)
		end
	end
end

function TransformationService:RequestTransform(player: Player, formId: string): (boolean, string?)
	local DataService = Knit.GetService("DataService")
	local CombatService = Knit.GetService("CombatService")

	local data = DataService:GetData(player)
	if not data then
		return false, "Data not loaded"
	end

	local form = Transformations:Get(formId)
	if not form then
		return false, "Unknown transformation"
	end

	-- Race check
	local raceValid = false
	for _, race in ipairs(form.race) do
		if race == data.Race then
			raceValid = true
			break
		end
	end
	if not raceValid then
		return false, "Wrong race for this transformation"
	end

	-- Level check
	if data.Level < form.levelRequirement then
		return false, "Level too low (need " .. form.levelRequirement .. ")"
	end

	-- Prerequisite check
	if form.prerequisite then
		local hasPrereq = false
		for _, unlocked in ipairs(data.UnlockedTransformations) do
			if unlocked == form.prerequisite then
				hasPrereq = true
				break
			end
		end
		if not hasPrereq then
			return false, "Missing prerequisite: " .. form.prerequisite
		end
	end

	-- Unlock check
	local isUnlocked = false
	for _, unlocked in ipairs(data.UnlockedTransformations) do
		if unlocked == formId then
			isUnlocked = true
			break
		end
	end
	if not isUnlocked then
		return false, "Transformation not unlocked"
	end

	-- Ki check
	local combat = CombatService:GetCombatState(player)
	if not combat then
		return false, "Combat state not initialized"
	end

	-- Apply transformation
	DataService:UpdateData(player, function(d)
		d.CurrentTransformation = formId
	end)

	activeTransforms[player] = {
		formId = formId,
		kiDrainPerSecond = form.kiDrainPerSecond,
	}

	-- Refresh combat stats
	CombatService:RefreshStats(player)

	-- Broadcast to all clients
	self.Client.TransformSync:FireAll({
		player = player,
		formId = formId,
		auraColor = form.auraColor,
		auraId = form.auraId,
		hairChange = form.hairChange,
		bodyChange = form.bodyChange,
	})

	return true, nil
end

function TransformationService:RevertTransformation(player: Player)
	local DataService = Knit.GetService("DataService")
	local CombatService = Knit.GetService("CombatService")

	activeTransforms[player] = nil

	DataService:UpdateData(player, function(d)
		d.CurrentTransformation = "Base"
	end)

	CombatService:RefreshStats(player)

	self.Client.TransformSync:FireAll({
		player = player,
		formId = "Base",
		auraColor = nil,
		auraId = nil,
		hairChange = nil,
		bodyChange = nil,
	})
end

function TransformationService:UnlockTransformation(player: Player, formId: string): boolean
	local DataService = Knit.GetService("DataService")
	local data = DataService:GetData(player)
	if not data then return false end

	-- Check not already unlocked
	for _, unlocked in ipairs(data.UnlockedTransformations) do
		if unlocked == formId then
			return false
		end
	end

	DataService:UpdateData(player, function(d)
		table.insert(d.UnlockedTransformations, formId)
	end)

	return true
end

-- Client methods

function TransformationService.Client:RequestTransform(player: Player, formId: string)
	return self.Server:RequestTransform(player, formId)
end

function TransformationService.Client:RequestRevert(player: Player)
	self.Server:RevertTransformation(player)
end

return TransformationService
]]></string>
          </Properties>
        </Item>
        <Item class="ModuleScript" referent="88">
          <Properties>
            <string name="Name">WorldBossService</string>
            <string name="Source"><![CDATA[--[[
	WorldBossService.lua
	Spawns periodic world bosses that multiple players fight together.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Settings = require(ReplicatedStorage.Shared.Constants.Settings)
local LootTable = require(script.Parent.Parent.Modules.LootTable)

local WorldBossService = Knit.CreateService({
	Name = "WorldBossService",
	Client = {},
})

local WORLD_BOSSES = {
	{
		id = "GreatApe",
		displayName = "Great Ape Vegeta",
		hp = 50000,
		damage = 500,
		zone = "WastelandArena",
		levelRequirement = 50,
		lootTable = {
			{ item = { type = "zeni", amount = 5000 }, weight = 50 },
			{ item = { type = "xp", amount = 10000 }, weight = 30 },
			{ item = { type = "move", moveId = "GalickGun" }, weight = 15 },
			{ item = { type = "cosmetic", cosmeticId = "SaiyanArmor" }, weight = 5 },
		},
	},
	{
		id = "GoldenFrieza",
		displayName = "Golden Frieza (Raid)",
		hp = 200000,
		damage = 1500,
		zone = "Space",
		levelRequirement = 150,
		lootTable = {
			{ item = { type = "zeni", amount = 20000 }, weight = 40 },
			{ item = { type = "xp", amount = 50000 }, weight = 30 },
			{ item = { type = "move", moveId = "DeathBall" }, weight = 20 },
			{ item = { type = "cosmetic", cosmeticId = "GoldenAura" }, weight = 10 },
		},
	},
}

local activeBoss: {
	definition: any,
	currentHp: number,
	model: Model?,
	damageContributions: { [Player]: number },
} | nil = nil

local spawnTimer = 0

function WorldBossService:KnitInit() end

function WorldBossService:KnitStart()
	-- Boss spawn timer
	task.spawn(function()
		while true do
			task.wait(Settings.WorldBoss.SPAWN_INTERVAL)
			if not activeBoss and #Players:GetPlayers() >= Settings.WorldBoss.MIN_PLAYERS_TO_SPAWN then
				self:_spawnRandomBoss()
			end
		end
	end)
end

function WorldBossService:_spawnRandomBoss()
	local boss = WORLD_BOSSES[math.random(#WORLD_BOSSES)]

	activeBoss = {
		definition = boss,
		currentHp = boss.hp,
		model = nil, -- Would create a large boss model here
		damageContributions = {},
	}

	-- Announce to all players
	-- TODO: Use a notification system
	print("[WorldBoss] " .. boss.displayName .. " has appeared!")
end

function WorldBossService:DamageBoss(player: Player, damage: number): boolean
	if not activeBoss then return false end

	activeBoss.currentHp = activeBoss.currentHp - damage
	activeBoss.damageContributions[player] = (activeBoss.damageContributions[player] or 0) + damage

	if activeBoss.currentHp <= 0 then
		self:_handleBossDefeat()
		return true
	end

	return false
end

function WorldBossService:_handleBossDefeat()
	if not activeBoss then return end

	local totalDamage = 0
	for _, dmg in pairs(activeBoss.damageContributions) do
		totalDamage = totalDamage + dmg
	end

	local ProgressionService = Knit.GetService("ProgressionService")
	local DataService = Knit.GetService("DataService")

	-- Reward contributors
	for player, dmg in pairs(activeBoss.damageContributions) do
		local contribution = dmg / totalDamage
		if contribution >= Settings.WorldBoss.DAMAGE_CONTRIBUTION_THRESHOLD then
			-- Base rewards
			ProgressionService:AwardXP(player, Settings.Progression.WORLD_BOSS_PARTICIPATION_XP)
			ProgressionService:AwardZeni(player, Settings.Economy.WORLD_BOSS_ZENI)

			-- Roll loot
			local loot = LootTable.Roll(activeBoss.definition.lootTable)
			self:_grantLoot(player, loot)
		end
	end

	-- Clean up
	if activeBoss.model then
		activeBoss.model:Destroy()
	end
	activeBoss = nil

	print("[WorldBoss] Boss defeated!")
end

function WorldBossService:_grantLoot(player: Player, loot: any)
	local ProgressionService = Knit.GetService("ProgressionService")
	local DataService = Knit.GetService("DataService")

	if loot.type == "zeni" then
		ProgressionService:AwardZeni(player, loot.amount)
	elseif loot.type == "xp" then
		ProgressionService:AwardXP(player, loot.amount)
	elseif loot.type == "move" then
		DataService:UpdateData(player, function(d)
			if not table.find(d.UnlockedMoves, loot.moveId) then
				table.insert(d.UnlockedMoves, loot.moveId)
			end
		end)
	elseif loot.type == "cosmetic" then
		-- TODO: Cosmetic system
	end
end

return WorldBossService
]]></string>
          </Properties>
        </Item>
      </Item>
    </Item>
  </Item>
  <Item class="StarterPlayer" referent="89">
    <Properties>
      <string name="Name">StarterPlayer</string>
    </Properties>
    <Item class="StarterPlayerScripts" referent="90">
      <Properties>
        <string name="Name">StarterPlayerScripts</string>
      </Properties>
      <Item class="LocalScript" referent="91">
        <Properties>
          <string name="Name">Client</string>
          <string name="Source"><![CDATA[--[[
	Client Entry Point
	Bootstraps Knit framework and starts all controllers.
]]

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

-- Load all controllers
local Controllers = script.Controllers
for _, module in ipairs(Controllers:GetChildren()) do
	if module:IsA("ModuleScript") then
		require(module)
	end
end

-- Start Knit
Knit.Start():andThen(function()
	print("[Client] All controllers started successfully!")
end):catch(function(err)
	warn("[Client] Failed to start controllers:", err)
end)
]]></string>
        </Properties>
        <Item class="Folder" referent="92">
          <Properties>
            <string name="Name">Controllers</string>
          </Properties>
          <Item class="ModuleScript" referent="93">
            <Properties>
              <string name="Name">AudioController</string>
              <string name="Source"><![CDATA[--[[
	AudioController.lua
	Manages all game audio: combat SFX, music, ambient sounds.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local AudioController = Knit.CreateController({
	Name = "AudioController",
})

-- Sound asset IDs (placeholders — replace with actual Roblox audio IDs)
local SOUNDS = {
	-- Combat
	MeleeHit = { id = "rbxassetid://0", volume = 0.8 },
	BlockHit = { id = "rbxassetid://0", volume = 0.6 },
	GuardBreak = { id = "rbxassetid://0", volume = 1.0 },
	KiBlast = { id = "rbxassetid://0", volume = 0.7 },
	KiCharge = { id = "rbxassetid://0", volume = 0.5, looped = true },
	Vanish = { id = "rbxassetid://0", volume = 0.6 },
	BeamFire = { id = "rbxassetid://0", volume = 0.9 },
	Explosion = { id = "rbxassetid://0", volume = 1.0 },

	-- Transformation
	TransformStart = { id = "rbxassetid://0", volume = 1.0 },
	TransformComplete = { id = "rbxassetid://0", volume = 1.0 },
	AuraLoop = { id = "rbxassetid://0", volume = 0.3, looped = true },

	-- UI
	ButtonClick = { id = "rbxassetid://0", volume = 0.5 },
	QuestComplete = { id = "rbxassetid://0", volume = 0.8 },
	LevelUp = { id = "rbxassetid://0", volume = 1.0 },
}

-- Zone music (placeholders)
local ZONE_MUSIC = {
	WestCity = { id = "rbxassetid://0", volume = 0.3 },
	WastelandArena = { id = "rbxassetid://0", volume = 0.3 },
	KamisLookout = { id = "rbxassetid://0", volume = 0.3 },
	Namek = { id = "rbxassetid://0", volume = 0.3 },
	Space = { id = "rbxassetid://0", volume = 0.3 },
	BeerusPlanet = { id = "rbxassetid://0", volume = 0.3 },
	TournamentOfPower = { id = "rbxassetid://0", volume = 0.3 },
}

local soundCache: { [string]: Sound } = {}
local currentMusic: Sound? = nil

function AudioController:KnitInit()
	-- Create sound folder
	local folder = Instance.new("Folder")
	folder.Name = "GameSounds"
	folder.Parent = SoundService
end

function AudioController:KnitStart() end

function AudioController:PlaySound(soundName: string, position: Vector3?)
	local soundData = SOUNDS[soundName]
	if not soundData then return end

	local sound = Instance.new("Sound")
	sound.SoundId = soundData.id
	sound.Volume = soundData.volume or 1
	sound.PlayOnRemove = false

	if position then
		-- 3D sound
		local part = Instance.new("Part")
		part.Anchored = true
		part.CanCollide = false
		part.Transparency = 1
		part.Size = Vector3.new(0.1, 0.1, 0.1)
		part.Position = position
		part.Parent = workspace

		sound.Parent = part
		sound:Play()

		sound.Ended:Connect(function()
			part:Destroy()
		end)
	else
		-- 2D sound
		sound.Parent = SoundService:FindFirstChild("GameSounds")
		sound:Play()

		if not soundData.looped then
			sound.Ended:Connect(function()
				sound:Destroy()
			end)
		else
			sound.Looped = true
			soundCache[soundName] = sound
		end
	end

	return sound
end

function AudioController:StopSound(soundName: string)
	local sound = soundCache[soundName]
	if sound then
		sound:Stop()
		sound:Destroy()
		soundCache[soundName] = nil
	end
end

function AudioController:PlayZoneMusic(zoneId: string)
	local musicData = ZONE_MUSIC[zoneId]
	if not musicData then return end

	-- Crossfade
	if currentMusic then
		local oldMusic = currentMusic
		TweenService:Create(oldMusic, TweenInfo.new(1), { Volume = 0 }):Play()
		task.delay(1, function()
			oldMusic:Destroy()
		end)
	end

	local music = Instance.new("Sound")
	music.SoundId = musicData.id
	music.Volume = 0
	music.Looped = true
	music.Parent = SoundService:FindFirstChild("GameSounds")
	music:Play()

	TweenService:Create(music, TweenInfo.new(1), { Volume = musicData.volume }):Play()
	currentMusic = music
end

function AudioController:StopMusic()
	if currentMusic then
		TweenService:Create(currentMusic, TweenInfo.new(1), { Volume = 0 }):Play()
		task.delay(1, function()
			if currentMusic then
				currentMusic:Destroy()
				currentMusic = nil
			end
		end)
	end
end

return AudioController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="94">
            <Properties>
              <string name="Name">CameraController</string>
              <string name="Source"><![CDATA[--[[
	CameraController.lua
	Custom camera: lock-on targeting, screen shake, dramatic zoom.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local CameraController = Knit.CreateController({
	Name = "CameraController",
})

local localPlayer = Players.LocalPlayer
local camera = workspace.CurrentCamera

local lockOnTarget: Player? = nil
local shakeIntensity = 0
local shakeDuration = 0
local shakeElapsed = 0

function CameraController:KnitInit() end

function CameraController:KnitStart()
	RunService.RenderStepped:Connect(function(dt)
		self:_update(dt)
	end)
end

function CameraController:_update(dt: number)
	-- Screen shake
	if shakeElapsed < shakeDuration then
		shakeElapsed = shakeElapsed + dt
		local progress = shakeElapsed / shakeDuration
		local currentIntensity = shakeIntensity * (1 - progress)

		local offset = CFrame.new(
			math.random() * currentIntensity * 2 - currentIntensity,
			math.random() * currentIntensity * 2 - currentIntensity,
			0
		)
		camera.CFrame = camera.CFrame * offset
	end

	-- Lock-on camera
	if lockOnTarget then
		local targetChar = lockOnTarget.Character
		local myChar = localPlayer.Character
		if targetChar and myChar then
			local targetRoot = targetChar:FindFirstChild("HumanoidRootPart")
			local myRoot = myChar:FindFirstChild("HumanoidRootPart")
			if targetRoot and myRoot then
				-- Keep target in view
				local midpoint = (myRoot.Position + targetRoot.Position) / 2
				local dist = (myRoot.Position - targetRoot.Position).Magnitude
				local cameraDistance = math.max(10, dist * 0.8)

				local lookAt = CFrame.lookAt(
					midpoint + Vector3.new(0, cameraDistance * 0.3, cameraDistance * 0.5),
					midpoint
				)

				camera.CFrame = camera.CFrame:Lerp(lookAt, dt * 5)
			end
		else
			lockOnTarget = nil
		end
	end
end

function CameraController:SetLockOn(target: Player?)
	lockOnTarget = target
	if target then
		camera.CameraType = Enum.CameraType.Scriptable
	else
		camera.CameraType = Enum.CameraType.Custom
	end
end

function CameraController:ScreenShake(duration: number, intensity: number)
	shakeDuration = duration
	shakeIntensity = intensity
	shakeElapsed = 0
end

function CameraController:DramaticZoom(targetPosition: Vector3, duration: number, callback: (() -> ())?)
	camera.CameraType = Enum.CameraType.Scriptable

	local startCFrame = camera.CFrame
	local endCFrame = CFrame.lookAt(
		targetPosition + Vector3.new(0, 3, 8),
		targetPosition
	)

	local tweenInfo = TweenInfo.new(duration, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut)
	local proxy = Instance.new("CFrameValue")
	proxy.Value = startCFrame

	proxy.Changed:Connect(function()
		camera.CFrame = proxy.Value
	end)

	local tween = TweenService:Create(proxy, tweenInfo, { Value = endCFrame })
	tween:Play()
	tween.Completed:Connect(function()
		proxy:Destroy()
		if callback then
			callback()
		end
	end)
end

function CameraController:ResetCamera()
	camera.CameraType = Enum.CameraType.Custom
	lockOnTarget = nil
end

return CameraController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="95">
            <Properties>
              <string name="Name">CombatController</string>
              <string name="Source"><![CDATA[--[[
	CombatController.lua
	Client-side combat: sends intents to server for both Player and NPC targets.
	Plays predicted animations and handles combat result visuals.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Moves = require(ReplicatedStorage.Shared.Constants.Moves)

local CombatController = Knit.CreateController({
	Name = "CombatController",
})

local localPlayer = Players.LocalPlayer
local CombatService = nil

function CombatController:KnitInit() end

function CombatController:KnitStart()
	CombatService = Knit.GetService("CombatService")

	CombatService.CombatResult:Connect(function(result)
		self:_handleCombatResult(result)
	end)

	CombatService.BeamStruggleSync:Connect(function(result)
		self:_handleBeamStruggle(result)
	end)
end

-- Get the currently locked target model (NPC or Player character)
function CombatController:_getLockedTargetModel(): Model?
	local TargetController = Knit.GetController("TargetController")
	return TargetController:GetTarget()
end

-- Find any targetable model (Player or NPC) in front of the player
function CombatController:_getTargetModelInFront(): Model?
	local character = localPlayer.Character
	if not character then return nil end

	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return nil end

	local lookDir = rootPart.CFrame.LookVector
	local origin = rootPart.Position

	local nearest = nil
	local nearestDist = 15

	-- Search all models with Humanoids (players + NPCs)
	for _, model in ipairs(workspace:GetDescendants()) do
		if not model:IsA("Model") then continue end
		if model == character then continue end

		local humanoid = model:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then continue end

		local otherRoot = model:FindFirstChild("HumanoidRootPart")
		if not otherRoot then continue end

		local toTarget = (otherRoot.Position - origin)
		local dist = toTarget.Magnitude
		if dist > nearestDist then continue end

		local dot = lookDir:Dot(toTarget.Unit)
		if dot > 0.5 then
			nearestDist = dist
			nearest = model
		end
	end

	return nearest
end

-- Resolve a target model and send the appropriate server call
function CombatController:_attackTarget(moveId: string)
	local targetModel = self:_getLockedTargetModel() or self:_getTargetModelInFront()
	if not targetModel then return end

	-- Check if target is a Player or NPC
	local targetPlayer = Players:GetPlayerFromCharacter(targetModel)

	if targetPlayer then
		CombatService:MeleeAttack(targetPlayer, moveId)
	else
		-- NPC target — send model reference to server
		CombatService:MeleeAttackNPC(targetModel, moveId)
	end

	local VfxController = Knit.GetController("VfxController")
	VfxController:PlayMeleeEffect(moveId)
end

function CombatController:OnLightAttack(target: Player?)
	if target then
		CombatService:MeleeAttack(target, "LightCombo")
		local VfxController = Knit.GetController("VfxController")
		VfxController:PlayMeleeEffect("LightCombo")
		return
	end
	self:_attackTarget("LightCombo")
end

function CombatController:OnHeavyAttack(target: Player?)
	if target then
		CombatService:MeleeAttack(target, "HeavyAttack")
		local VfxController = Knit.GetController("VfxController")
		VfxController:PlayMeleeEffect("HeavyAttack")
		return
	end
	self:_attackTarget("HeavyAttack")
end

function CombatController:OnKiBlast()
	local character = localPlayer.Character
	if not character then return end
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	-- Aim toward locked target if available
	local targetModel = self:_getLockedTargetModel()
	local direction = rootPart.CFrame.LookVector

	if targetModel then
		local targetRoot = targetModel:FindFirstChild("HumanoidRootPart")
		if targetRoot then
			direction = (targetRoot.Position - rootPart.Position).Unit
		end
	end

	CombatService:KiBlast(direction, "KiBlast")
end

function CombatController:OnChargedKiBlastStart()
	local VfxController = Knit.GetController("VfxController")
	VfxController:StartChargeEffect()
end

function CombatController:OnChargedKiBlastRelease()
	local character = localPlayer.Character
	if not character then return end
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	local targetModel = self:_getLockedTargetModel()
	local direction = rootPart.CFrame.LookVector
	if targetModel then
		local targetRoot = targetModel:FindFirstChild("HumanoidRootPart")
		if targetRoot then
			direction = (targetRoot.Position - rootPart.Position).Unit
		end
	end

	CombatService:KiBlast(direction, "ChargedKiBlast")

	local VfxController = Knit.GetController("VfxController")
	VfxController:StopChargeEffect()
end

function CombatController:OnBlock(isBlocking: boolean)
	CombatService:Block(isBlocking)
end

function CombatController:OnVanish()
	CombatService:Vanish()
end

function CombatController:OnChargeKi(isCharging: boolean)
	CombatService:ChargeKi(isCharging)

	local VfxController = Knit.GetController("VfxController")
	if isCharging then
		VfxController:StartKiChargeEffect()
	else
		VfxController:StopKiChargeEffect()
	end
end

function CombatController:OnSkill(slotIndex: number)
	local DataService = Knit.GetService("DataService")
	DataService:RequestPlayerData():andThen(function(data)
		if not data then return end

		local moveId = data.MoveLoadout[slotIndex]
		if not moveId then return end

		local move = Moves:Get(moveId)
		if not move then return end

		if move.category == "Beam" or move.category == "Signature" or move.category == "Ultimate" then
			local character = localPlayer.Character
			if not character then return end
			local rootPart = character:FindFirstChild("HumanoidRootPart")
			if not rootPart then return end

			-- Aim toward locked target
			local targetModel = self:_getLockedTargetModel()
			local direction = rootPart.CFrame.LookVector
			if targetModel then
				local targetRoot = targetModel:FindFirstChild("HumanoidRootPart")
				if targetRoot then
					direction = (targetRoot.Position - rootPart.Position).Unit
				end
			end

			CombatService:KiBlast(direction, moveId)
		elseif move.category == "Evasive" then
			CombatService:Vanish()
		end
	end)
end

function CombatController:_handleCombatResult(result: any)
	local VfxController = Knit.GetController("VfxController")
	local AudioController = Knit.GetController("AudioController")
	local CameraController = Knit.GetController("CameraController")
	local UIController = Knit.GetController("UIController")

	-- ===== STATE SYNC: Update HUD bars every frame =====
	if result.type == "state_sync" then
		UIController:UpdateHealthBar(result.hp, result.maxHp)
		UIController:UpdateKiBar(result.ki, result.maxKi)
		UIController:UpdateStaminaBar(result.stamina, result.maxStamina)
		return
	end

	if result.type == "melee" or result.type == "melee_received" or result.type == "npc_hit" then
		if result.blocked then
			AudioController:PlaySound("BlockHit")
		else
			AudioController:PlaySound("MeleeHit")
			CameraController:ScreenShake(0.15, math.clamp(result.damage / 50, 0.1, 1.5))

			if result.launcher then
				VfxController:PlayLaunchEffect(result.target or result.attacker)
			end
		end

		if result.guardBreak then
			AudioController:PlaySound("GuardBreak")
			CameraController:ScreenShake(0.5, 1.0)
		end

		-- Combo counter
		if result.comboCount and result.comboCount > 1 then
			-- UIController will display this
			local UIController = Knit.GetController("UIController")
			if UIController.ShowComboCounter then
				UIController:ShowComboCounter(result.comboCount, result.comboDamage or 0)
			end
		end
	elseif result.type == "ki_blast" then
		VfxController:SpawnKiBlastProjectile(result.attacker, result.direction, result.moveId, result.origin)
		AudioController:PlaySound("KiBlast")
	elseif result.type == "vanish" then
		VfxController:PlayVanishEffect(result.player)
		AudioController:PlaySound("Vanish")
	elseif result.type == "npc_died" then
		AudioController:PlaySound("Explosion")
		CameraController:ScreenShake(0.3, 0.8)
	end
end

function CombatController:_handleBeamStruggle(result: any)
	local VfxController = Knit.GetController("VfxController")
	local CameraController = Knit.GetController("CameraController")

	VfxController:PlayBeamStruggleResult(result.winner)
	CameraController:ScreenShake(1.0, 2.0)
end

return CombatController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="96">
            <Properties>
              <string name="Name">DamageNumberController</string>
              <string name="Source"><![CDATA[--[[
	DamageNumberController.lua
	Spawns floating damage numbers above targets when they take damage.
	Supports: normal (white), ki (blue), critical (yellow), healing (green), status text.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local DamageNumberController = Knit.CreateController({
	Name = "DamageNumberController",
})

local localPlayer = Players.LocalPlayer
local COLORS = {
	melee = Color3.fromRGB(255, 255, 255),
	ki = Color3.fromRGB(100, 180, 255),
	critical = Color3.fromRGB(255, 255, 50),
	healing = Color3.fromRGB(80, 255, 80),
	guardBreak = Color3.fromRGB(255, 150, 50),
	status = Color3.fromRGB(200, 200, 255),
}

local activeNumbers = 0

function DamageNumberController:KnitInit() end

function DamageNumberController:KnitStart()
	local CombatService = Knit.GetService("CombatService")

	CombatService.CombatResult:Connect(function(result)
		self:_onCombatResult(result)
	end)
end

function DamageNumberController:_onCombatResult(result: any)
	if result.type == "npc_hit" or result.type == "melee" then
		local targetModel = result.npcModel or nil

		-- For player targets, find their character
		if result.target and typeof(result.target) == "Instance" and result.target:IsA("Player") then
			targetModel = result.target.Character
		end

		if not targetModel then return end

		local color = COLORS.melee
		local prefix = ""
		local suffix = ""

		if result.isCrit then
			color = COLORS.critical
			prefix = "CRIT! "
		end

		if result.blocked then
			color = Color3.fromRGB(150, 150, 150)
			suffix = " (Blocked)"
		end

		self:SpawnNumber(targetModel, prefix .. tostring(result.damage) .. suffix, color, result.isCrit)

		-- Guard break text
		if result.guardBreak then
			task.delay(0.1, function()
				self:SpawnText(targetModel, "GUARD BREAK!", COLORS.guardBreak, true)
			end)
		end

		-- Combo counter text (every 5 hits)
		if result.comboCount and result.comboCount > 0 and result.comboCount % 5 == 0 then
			task.delay(0.15, function()
				self:SpawnText(targetModel, result.comboCount .. " HIT COMBO!", Color3.fromRGB(255, 200, 50), true)
			end)
		end

	elseif result.type == "melee_received" then
		-- Show damage on our own character
		local myChar = localPlayer.Character
		if myChar then
			local color = result.blocked and Color3.fromRGB(150, 150, 150) or Color3.fromRGB(255, 80, 80)
			self:SpawnNumber(myChar, tostring(result.damage), color, false)
		end

	elseif result.type == "npc_died" then
		local targetModel = result.npcModel
		if targetModel then
			self:SpawnText(targetModel, "KO!", Color3.fromRGB(255, 50, 50), true)
		end
	end
end

function DamageNumberController:SpawnNumber(targetModel: Model, text: string, color: Color3, isBig: boolean?)
	local head = targetModel:FindFirstChild("Head") or targetModel:FindFirstChild("HumanoidRootPart")
	if not head then return end

	-- Stagger offset so numbers don't overlap
	activeNumbers = activeNumbers + 1
	local offset = Vector3.new(math.random(-10, 10) / 10, 2 + (activeNumbers % 3) * 0.8, math.random(-5, 5) / 10)

	local billboard = Instance.new("BillboardGui")
	billboard.Name = "DamageNumber"
	billboard.Size = isBig and UDim2.new(4, 0, 1.5, 0) or UDim2.new(3, 0, 1, 0)
	billboard.StudsOffset = offset
	billboard.AlwaysOnTop = true
	billboard.MaxDistance = 100
	billboard.Parent = head

	local label = Instance.new("TextLabel")
	label.Size = UDim2.fromScale(1, 1)
	label.BackgroundTransparency = 1
	label.Text = text
	label.TextColor3 = color
	label.TextStrokeColor3 = Color3.new(0, 0, 0)
	label.TextStrokeTransparency = 0
	label.TextSize = isBig and 28 or 22
	label.Font = Enum.Font.GothamBold
	label.TextScaled = false
	label.Parent = billboard

	-- Animate: float up + fade out
	local duration = 0.8

	-- Scale pop on crits
	if isBig then
		label.TextSize = 10
		TweenService:Create(label, TweenInfo.new(0.15, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
			TextSize = 32,
		}):Play()
	end

	-- Float upward
	TweenService:Create(billboard, TweenInfo.new(duration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
		StudsOffset = offset + Vector3.new(0, 2, 0),
	}):Play()

	-- Fade out
	TweenService:Create(label, TweenInfo.new(duration * 0.6, Enum.EasingStyle.Linear, Enum.EasingDirection.In, 0, false, duration * 0.4), {
		TextTransparency = 1,
		TextStrokeTransparency = 1,
	}):Play()

	-- Cleanup
	task.delay(duration + 0.1, function()
		billboard:Destroy()
		activeNumbers = math.max(0, activeNumbers - 1)
	end)
end

function DamageNumberController:SpawnText(targetModel: Model, text: string, color: Color3, isBig: boolean?)
	self:SpawnNumber(targetModel, text, color, isBig)
end

return DamageNumberController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="97">
            <Properties>
              <string name="Name">InputController</string>
              <string name="Source"><![CDATA[--[[
	InputController.lua
	Maps player input to combat intents and sends them to the server.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local InputController = Knit.CreateController({
	Name = "InputController",
})

-- Input bindings
local BINDINGS = {
	[Enum.UserInputType.MouseButton1] = "LightAttack",
	[Enum.UserInputType.MouseButton2] = "HeavyAttack",
	[Enum.KeyCode.E] = "KiBlast",
	[Enum.KeyCode.R] = "ChargedKiBlast",
	[Enum.KeyCode.F] = "Block",
	[Enum.KeyCode.Q] = "Vanish",
	[Enum.KeyCode.C] = "ChargeKi",
	[Enum.KeyCode.Z] = "Skill1",
	[Enum.KeyCode.X] = "Skill2",
	[Enum.KeyCode.V] = "Skill3",
	[Enum.KeyCode.B] = "Skill4",
	[Enum.KeyCode.N] = "Skill5",
	[Enum.KeyCode.G] = "Skill6",
	[Enum.KeyCode.T] = "Transform",
	[Enum.KeyCode.Tab] = "LockOn",
	[Enum.KeyCode.J] = "QuestLog",
	[Enum.KeyCode.K] = "MoveLoadout",
	[Enum.KeyCode.P] = "Settings",
}

-- State
local lockedTarget: Player? = nil
local isBlocking = false
local isChargingKi = false
local menuVisibility_QuestLog = false
local menuVisibility_MoveLoadout = false
local menuVisibility_Settings = false

function InputController:KnitInit() end

function InputController:KnitStart()
	local CombatController = Knit.GetController("CombatController")

	UserInputService.InputBegan:Connect(function(input, gameProcessed)
		if gameProcessed then return end

		local action = BINDINGS[input.UserInputType] or BINDINGS[input.KeyCode]
		if not action then return end

		if action == "LightAttack" then
			CombatController:OnLightAttack(lockedTarget)
		elseif action == "HeavyAttack" then
			CombatController:OnHeavyAttack(lockedTarget)
		elseif action == "KiBlast" then
			CombatController:OnKiBlast()
		elseif action == "ChargedKiBlast" then
			CombatController:OnChargedKiBlastStart()
		elseif action == "Block" then
			isBlocking = true
			CombatController:OnBlock(true)
		elseif action == "Vanish" then
			CombatController:OnVanish()
		elseif action == "ChargeKi" then
			isChargingKi = true
			CombatController:OnChargeKi(true)
		elseif action == "Skill1" then
			CombatController:OnSkill(1)
		elseif action == "Skill2" then
			CombatController:OnSkill(2)
		elseif action == "Skill3" then
			CombatController:OnSkill(3)
		elseif action == "Skill4" then
			CombatController:OnSkill(4)
		elseif action == "Skill5" then
			CombatController:OnSkill(5)
		elseif action == "Skill6" then
			CombatController:OnSkill(6)
		elseif action == "Transform" then
			local TransformationController = Knit.GetController("TransformationController")
			TransformationController:ToggleTransformWheel()
		elseif action == "LockOn" then
			self:_toggleLockOn()
		elseif action == "QuestLog" then
			local UIController = Knit.GetController("UIController")
			menuVisibility_QuestLog = not menuVisibility_QuestLog
			UIController:SetMenuVisible("QuestLog", menuVisibility_QuestLog)
		elseif action == "MoveLoadout" then
			local UIController = Knit.GetController("UIController")
			menuVisibility_MoveLoadout = not menuVisibility_MoveLoadout
			UIController:SetMenuVisible("MoveLoadout", menuVisibility_MoveLoadout)
		elseif action == "Settings" then
			local UIController = Knit.GetController("UIController")
			menuVisibility_Settings = not menuVisibility_Settings
			UIController:SetMenuVisible("Settings", menuVisibility_Settings)
		end
	end)

	UserInputService.InputEnded:Connect(function(input, gameProcessed)
		local action = BINDINGS[input.UserInputType] or BINDINGS[input.KeyCode]
		if not action then return end

		if action == "Block" then
			isBlocking = false
			CombatController:OnBlock(false)
		elseif action == "ChargeKi" then
			isChargingKi = false
			CombatController:OnChargeKi(false)
		elseif action == "ChargedKiBlast" then
			CombatController:OnChargedKiBlastRelease()
		end
	end)
end

function InputController:_toggleLockOn()
	local Players = game:GetService("Players")
	local localPlayer = Players.LocalPlayer
	local camera = workspace.CurrentCamera

	if lockedTarget then
		lockedTarget = nil
		local CameraController = Knit.GetController("CameraController")
		CameraController:SetLockOn(nil)
		return
	end

	-- Find nearest player in view
	local nearest = nil
	local nearestDist = 200

	for _, player in ipairs(Players:GetPlayers()) do
		if player == localPlayer then continue end
		local character = player.Character
		if not character then continue end

		local rootPart = character:FindFirstChild("HumanoidRootPart")
		if not rootPart then continue end

		local myChar = localPlayer.Character
		if not myChar then continue end
		local myRoot = myChar:FindFirstChild("HumanoidRootPart")
		if not myRoot then continue end

		local dist = (rootPart.Position - myRoot.Position).Magnitude
		if dist < nearestDist then
			nearestDist = dist
			nearest = player
		end
	end

	if nearest then
		lockedTarget = nearest
		local CameraController = Knit.GetController("CameraController")
		CameraController:SetLockOn(nearest)
	end
end

function InputController:GetLockedTarget(): Player?
	return lockedTarget
end

return InputController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="98">
            <Properties>
              <string name="Name">IntroController</string>
              <string name="Source"><![CDATA[--[[
	IntroController.lua
	Cinematic introduction sequence for first-time players.
	Shows game title, animated text, controls overview, and transitions to character creation.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local IntroController = Knit.CreateController({
	Name = "IntroController",
})

local localPlayer = Players.LocalPlayer
local playerGui = localPlayer:WaitForChild("PlayerGui")

function IntroController:KnitInit() end

function IntroController:KnitStart()
	local DataService = Knit.GetService("DataService")

	-- Wait for data to load
	DataService.PlayerDataUpdate:Connect(function(data)
		if data and not data.CharacterCreated then
			-- First time player — show intro
			task.wait(1)
			self:_playIntro()
		end
	end)
end

function IntroController:_playIntro()
	local gui = Instance.new("ScreenGui")
	gui.Name = "IntroSequence"
	gui.IgnoreGuiInset = true
	gui.DisplayOrder = 200
	gui.ResetOnSpawn = false
	gui.Parent = playerGui

	-- Full black screen
	local bg = Instance.new("Frame")
	bg.Size = UDim2.fromScale(1, 1)
	bg.BackgroundColor3 = Color3.new(0, 0, 0)
	bg.BorderSizePixel = 0
	bg.Parent = gui

	-- ==================
	-- PHASE 1: Title reveal
	-- ==================
	task.wait(0.5)

	-- Particle/energy lines streaking across
	for i = 1, 15 do
		local streak = Instance.new("Frame")
		streak.Size = UDim2.new(0, math.random(100, 300), 0, 2)
		streak.Position = UDim2.new(-0.3, 0, math.random(10, 90) / 100, 0)
		streak.BackgroundColor3 = Color3.fromRGB(
			math.random(200, 255),
			math.random(150, 255),
			math.random(0, 100)
		)
		streak.BorderSizePixel = 0
		streak.Parent = bg

		TweenService:Create(streak, TweenInfo.new(math.random(5, 12) / 10, Enum.EasingStyle.Linear), {
			Position = UDim2.new(1.3, 0, streak.Position.Y.Scale, 0),
		}):Play()

		task.delay(1.5, function()
			streak:Destroy()
		end)
	end

	task.wait(0.8)

	-- "DRAGON BALL" text
	local dragonBallLabel = Instance.new("TextLabel")
	dragonBallLabel.Size = UDim2.new(0.8, 0, 0, 60)
	dragonBallLabel.Position = UDim2.new(0.1, 0, 0.25, 0)
	dragonBallLabel.BackgroundTransparency = 1
	dragonBallLabel.Text = "DRAGON BALL"
	dragonBallLabel.TextColor3 = Color3.fromRGB(255, 165, 0)
	dragonBallLabel.TextStrokeColor3 = Color3.fromRGB(200, 80, 0)
	dragonBallLabel.TextStrokeTransparency = 0
	dragonBallLabel.TextSize = 70
	dragonBallLabel.Font = Enum.Font.GothamBold
	dragonBallLabel.TextTransparency = 1
	dragonBallLabel.Parent = bg

	TweenService:Create(dragonBallLabel, TweenInfo.new(1, Enum.EasingStyle.Cubic), {
		TextTransparency = 0,
	}):Play()

	task.wait(0.5)

	-- "ULTIMATE" text
	local ultimateLabel = Instance.new("TextLabel")
	ultimateLabel.Size = UDim2.new(0.8, 0, 0, 80)
	ultimateLabel.Position = UDim2.new(0.1, 0, 0.35, 0)
	ultimateLabel.BackgroundTransparency = 1
	ultimateLabel.Text = "ULTIMATE"
	ultimateLabel.TextColor3 = Color3.fromRGB(255, 215, 0)
	ultimateLabel.TextStrokeColor3 = Color3.fromRGB(200, 150, 0)
	ultimateLabel.TextStrokeTransparency = 0
	ultimateLabel.TextSize = 90
	ultimateLabel.Font = Enum.Font.GothamBold
	ultimateLabel.TextTransparency = 1
	ultimateLabel.Parent = bg

	-- Scale up animation
	ultimateLabel.TextSize = 20
	TweenService:Create(ultimateLabel, TweenInfo.new(0.8, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
		TextTransparency = 0,
		TextSize = 90,
	}):Play()

	-- Flash
	task.wait(0.3)
	local flash = Instance.new("Frame")
	flash.Size = UDim2.fromScale(1, 1)
	flash.BackgroundColor3 = Color3.fromRGB(255, 255, 200)
	flash.BackgroundTransparency = 0.5
	flash.BorderSizePixel = 0
	flash.Parent = bg
	TweenService:Create(flash, TweenInfo.new(0.5), { BackgroundTransparency = 1 }):Play()
	task.delay(0.6, function() flash:Destroy() end)

	task.wait(1.5)

	-- Subtitle
	local subtitleLabel = Instance.new("TextLabel")
	subtitleLabel.Size = UDim2.new(0.8, 0, 0, 30)
	subtitleLabel.Position = UDim2.new(0.1, 0, 0.48, 0)
	subtitleLabel.BackgroundTransparency = 1
	subtitleLabel.Text = "Your legend begins here..."
	subtitleLabel.TextColor3 = Color3.fromRGB(200, 200, 220)
	subtitleLabel.TextSize = 22
	subtitleLabel.Font = Enum.Font.Gotham
	subtitleLabel.TextTransparency = 1
	subtitleLabel.Parent = bg

	TweenService:Create(subtitleLabel, TweenInfo.new(1), { TextTransparency = 0 }):Play()

	task.wait(2)

	-- ==================
	-- PHASE 2: Story intro text
	-- ==================
	-- Fade out title
	for _, child in ipairs(bg:GetChildren()) do
		if child:IsA("TextLabel") then
			TweenService:Create(child, TweenInfo.new(0.5), { TextTransparency = 1 }):Play()
		end
	end
	task.wait(0.6)

	local storyLines = {
		"In a universe where warriors push beyond their limits...",
		"Where the power of gods can be unlocked through training...",
		"A new fighter rises to challenge the strongest in existence.",
		"Choose your race. Master your ki. Unlock legendary transformations.",
		"The fate of the universe is in your hands.",
	}

	for _, line in ipairs(storyLines) do
		local storyLabel = Instance.new("TextLabel")
		storyLabel.Size = UDim2.new(0.7, 0, 0, 40)
		storyLabel.Position = UDim2.new(0.15, 0, 0.45, 0)
		storyLabel.BackgroundTransparency = 1
		storyLabel.Text = ""
		storyLabel.TextColor3 = Color3.fromRGB(220, 220, 240)
		storyLabel.TextSize = 20
		storyLabel.Font = Enum.Font.Gotham
		storyLabel.TextWrapped = true
		storyLabel.TextTransparency = 0
		storyLabel.Parent = bg

		-- Typewriter effect
		for i = 1, #line do
			storyLabel.Text = string.sub(line, 1, i)
			task.wait(0.03)
		end

		task.wait(1.2)
		TweenService:Create(storyLabel, TweenInfo.new(0.4), { TextTransparency = 1 }):Play()
		task.wait(0.5)
		storyLabel:Destroy()
	end

	-- ==================
	-- PHASE 3: Controls overview
	-- ==================
	local controlsTitle = Instance.new("TextLabel")
	controlsTitle.Size = UDim2.new(0.6, 0, 0, 40)
	controlsTitle.Position = UDim2.new(0.2, 0, 0.12, 0)
	controlsTitle.BackgroundTransparency = 1
	controlsTitle.Text = "MASTER THESE CONTROLS"
	controlsTitle.TextColor3 = Color3.fromRGB(255, 215, 0)
	controlsTitle.TextSize = 28
	controlsTitle.Font = Enum.Font.GothamBold
	controlsTitle.TextTransparency = 1
	controlsTitle.Parent = bg

	TweenService:Create(controlsTitle, TweenInfo.new(0.5), { TextTransparency = 0 }):Play()
	task.wait(0.3)

	local controls = {
		{ key = "M1 / M2", action = "Light & Heavy Attacks", icon = Color3.fromRGB(255, 100, 100) },
		{ key = "E / R", action = "Ki Blast & Charged Blast", icon = Color3.fromRGB(100, 200, 255) },
		{ key = "F", action = "Block incoming attacks", icon = Color3.fromRGB(100, 255, 100) },
		{ key = "Q", action = "Vanish behind your enemy", icon = Color3.fromRGB(200, 150, 255) },
		{ key = "C", action = "Charge your Ki energy", icon = Color3.fromRGB(255, 255, 100) },
		{ key = "Z X V B", action = "Signature skills", icon = Color3.fromRGB(255, 150, 50) },
		{ key = "N", action = "Ultimate move", icon = Color3.fromRGB(255, 80, 80) },
		{ key = "G", action = "Evasive move", icon = Color3.fromRGB(80, 220, 100) },
		{ key = "T", action = "Transform to power up", icon = Color3.fromRGB(255, 215, 0) },
		{ key = "Tab", action = "Lock onto a target", icon = Color3.fromRGB(150, 200, 255) },
	}

	local controlFrames = {}
	for i, ctrl in ipairs(controls) do
		local ctrlFrame = Instance.new("Frame")
		ctrlFrame.Size = UDim2.new(0.4, 0, 0, 36)
		ctrlFrame.Position = UDim2.new(i <= 5 and 0.05 or 0.52, 0, 0.2 + ((i - 1) % 5) * 0.08, 0)
		ctrlFrame.BackgroundColor3 = Color3.fromRGB(30, 30, 50)
		ctrlFrame.BackgroundTransparency = 0.3
		ctrlFrame.BorderSizePixel = 0
		ctrlFrame.Parent = bg
		Instance.new("UICorner", ctrlFrame).CornerRadius = UDim.new(0, 8)

		-- Color indicator
		local indicator = Instance.new("Frame")
		indicator.Size = UDim2.new(0, 4, 0.7, 0)
		indicator.Position = UDim2.new(0, 5, 0.15, 0)
		indicator.BackgroundColor3 = ctrl.icon
		indicator.BorderSizePixel = 0
		indicator.Parent = ctrlFrame
		Instance.new("UICorner", indicator).CornerRadius = UDim.new(0, 2)

		local keyLabel = Instance.new("TextLabel")
		keyLabel.Size = UDim2.new(0.3, 0, 1, 0)
		keyLabel.Position = UDim2.new(0.05, 0, 0, 0)
		keyLabel.BackgroundTransparency = 1
		keyLabel.Text = ctrl.key
		keyLabel.TextColor3 = ctrl.icon
		keyLabel.TextSize = 16
		keyLabel.Font = Enum.Font.GothamBold
		keyLabel.TextXAlignment = Enum.TextXAlignment.Left
		keyLabel.Parent = ctrlFrame

		local actionLabel = Instance.new("TextLabel")
		actionLabel.Size = UDim2.new(0.6, 0, 1, 0)
		actionLabel.Position = UDim2.new(0.35, 0, 0, 0)
		actionLabel.BackgroundTransparency = 1
		actionLabel.Text = ctrl.action
		actionLabel.TextColor3 = Color3.fromRGB(200, 200, 220)
		actionLabel.TextSize = 14
		actionLabel.Font = Enum.Font.Gotham
		actionLabel.TextXAlignment = Enum.TextXAlignment.Left
		actionLabel.Parent = ctrlFrame

		-- Animate in with delay
		ctrlFrame.BackgroundTransparency = 1
		keyLabel.TextTransparency = 1
		actionLabel.TextTransparency = 1

		task.delay(i * 0.15, function()
			TweenService:Create(ctrlFrame, TweenInfo.new(0.3), { BackgroundTransparency = 0.3 }):Play()
			TweenService:Create(keyLabel, TweenInfo.new(0.3), { TextTransparency = 0 }):Play()
			TweenService:Create(actionLabel, TweenInfo.new(0.3), { TextTransparency = 0 }):Play()
		end)

		table.insert(controlFrames, ctrlFrame)
	end

	-- Extra tips at bottom
	task.wait(2)

	local tipsLabel = Instance.new("TextLabel")
	tipsLabel.Size = UDim2.new(0.8, 0, 0, 30)
	tipsLabel.Position = UDim2.new(0.1, 0, 0.65, 0)
	tipsLabel.BackgroundTransparency = 1
	tipsLabel.Text = "J - Quest Log  |  K - Move Loadout  |  P - Settings"
	tipsLabel.TextColor3 = Color3.fromRGB(150, 150, 180)
	tipsLabel.TextSize = 16
	tipsLabel.Font = Enum.Font.Gotham
	tipsLabel.TextTransparency = 1
	tipsLabel.Parent = bg
	TweenService:Create(tipsLabel, TweenInfo.new(0.5), { TextTransparency = 0 }):Play()

	-- ==================
	-- PHASE 4: Continue button
	-- ==================
	task.wait(1.5)

	local continueBtn = Instance.new("TextButton")
	continueBtn.Size = UDim2.new(0.3, 0, 0, 50)
	continueBtn.Position = UDim2.new(0.35, 0, 0.78, 0)
	continueBtn.BackgroundColor3 = Color3.fromRGB(255, 165, 0)
	continueBtn.Text = "BEGIN YOUR JOURNEY"
	continueBtn.TextColor3 = Color3.new(0, 0, 0)
	continueBtn.TextSize = 20
	continueBtn.Font = Enum.Font.GothamBold
	continueBtn.BorderSizePixel = 0
	continueBtn.TextTransparency = 1
	continueBtn.BackgroundTransparency = 1
	continueBtn.Parent = bg
	Instance.new("UICorner", continueBtn).CornerRadius = UDim.new(0, 10)

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

	-- Pulsing glow on button
	task.spawn(function()
		while continueBtn.Parent do
			TweenService:Create(continueBtn, TweenInfo.new(0.8, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {
				BackgroundColor3 = Color3.fromRGB(255, 200, 50),
			}):Play()
			task.wait(0.8)
			TweenService:Create(continueBtn, TweenInfo.new(0.8, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {
				BackgroundColor3 = Color3.fromRGB(255, 150, 0),
			}):Play()
			task.wait(0.8)
		end
	end)

	-- On click: fade out, open character creation
	continueBtn.MouseButton1Click:Connect(function()
		-- Fade everything out
		TweenService:Create(bg, TweenInfo.new(0.8), { BackgroundTransparency = 0 }):Play()
		for _, child in ipairs(bg:GetDescendants()) do
			if child:IsA("TextLabel") or child:IsA("TextButton") then
				TweenService:Create(child, TweenInfo.new(0.6), { TextTransparency = 1 }):Play()
			end
			if child:IsA("GuiObject") then
				TweenService:Create(child, TweenInfo.new(0.6), { BackgroundTransparency = 1 }):Play()
			end
		end

		task.wait(0.8)
		gui:Destroy()

		-- Open character creation
		local UIController = Knit.GetController("UIController")
		UIController:_showCharacterCreation()
	end)
end

return IntroController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="99">
            <Properties>
              <string name="Name">QuestController</string>
              <string name="Source"><![CDATA[--[[
	QuestController.lua
	Client-side quest tracking and UI updates.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Knit = require(ReplicatedStorage.Packages.Knit)

local QuestController = Knit.CreateController({
	Name = "QuestController",
})

local activeQuests = {}

function QuestController:KnitInit() end

function QuestController:KnitStart()
	local QuestService = Knit.GetService("QuestService")

	QuestService.QuestUpdate:Connect(function(questProgress)
		activeQuests = questProgress
		self:_refreshQuestUI()
	end)

	QuestService.QuestComplete:Connect(function(data)
		self:_showQuestComplete(data)
	end)
end

function QuestController:_refreshQuestUI()
	-- TODO: Update quest tracker UI on screen
end

function QuestController:_showQuestComplete(data: any)
	local AudioController = Knit.GetController("AudioController")
	AudioController:PlaySound("QuestComplete")

	-- TODO: Show quest completion popup with rewards
	print("[Quest] Completed: " .. data.questId)
end

function QuestController:AcceptQuest(questId: string)
	local QuestService = Knit.GetService("QuestService")
	QuestService:AcceptQuest(questId)
end

function QuestController:AbandonQuest(questId: string)
	local QuestService = Knit.GetService("QuestService")
	QuestService:AbandonQuest(questId)
end

return QuestController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="100">
            <Properties>
              <string name="Name">TargetController</string>
              <string name="Source"><![CDATA[--[[
	TargetController.lua
	NPC and Player targeting system.
	Walk near an NPC/Player, press Tab or click to lock on.
	All attacks auto-aim at the locked target.
	Shows a target indicator (diamond marker + name) above the locked target.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local TargetController = Knit.CreateController({
	Name = "TargetController",
})

local localPlayer = Players.LocalPlayer
local currentTarget: Model? = nil
local targetIndicator: BillboardGui? = nil
local nearbyIndicators: { [Model]: BillboardGui } = {}
local MAX_TARGET_RANGE = 50
local NEARBY_RANGE = 30
local renderConn = nil

function TargetController:KnitInit() end

function TargetController:KnitStart()
	-- Render loop for nearby indicators and target tracking
	renderConn = RunService.RenderStepped:Connect(function(dt)
		self:_updateNearbyIndicators()
		self:_updateTargetIndicator()
		self:_faceTarget()
	end)

	-- Click-to-target NPCs
	UserInputService.InputBegan:Connect(function(input, processed)
		if processed then return end

		-- Middle mouse or Tab to cycle targets
		if input.KeyCode == Enum.KeyCode.Tab then
			self:CycleTarget()
		end
	end)

	-- Mouse click on NPC to target
	local mouse = localPlayer:GetMouse()
	mouse.Button1Down:Connect(function()
		local target = mouse.Target
		if target then
			local model = target:FindFirstAncestorOfClass("Model")
			if model and model:FindFirstChildOfClass("Humanoid") and model ~= localPlayer.Character then
				self:SetTarget(model)
			end
		end
	end)
end

function TargetController:SetTarget(model: Model?)
	-- Remove old indicator
	if targetIndicator then
		targetIndicator:Destroy()
		targetIndicator = nil
	end

	currentTarget = model

	if model then
		self:_createTargetIndicator(model)

		-- Camera lock
		local CameraController = Knit.GetController("CameraController")
		local player = Players:GetPlayerFromCharacter(model)
		if player then
			CameraController:SetLockOn(player)
		end
	else
		local CameraController = Knit.GetController("CameraController")
		CameraController:SetLockOn(nil)
	end
end

function TargetController:GetTarget(): Model?
	-- Validate target still exists and is alive
	if currentTarget then
		if not currentTarget.Parent then
			self:SetTarget(nil)
			return nil
		end
		local humanoid = currentTarget:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then
			self:SetTarget(nil)
			return nil
		end

		-- Check range
		local myChar = localPlayer.Character
		if myChar then
			local myRoot = myChar:FindFirstChild("HumanoidRootPart")
			local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
			if myRoot and targetRoot then
				if (myRoot.Position - targetRoot.Position).Magnitude > MAX_TARGET_RANGE * 2 then
					self:SetTarget(nil)
					return nil
				end
			end
		end
	end
	return currentTarget
end

function TargetController:CycleTarget()
	local targets = self:_getTargetsInRange()

	if #targets == 0 then
		self:SetTarget(nil)
		return
	end

	-- Find current target index
	local currentIdx = 0
	for i, t in ipairs(targets) do
		if t == currentTarget then
			currentIdx = i
			break
		end
	end

	-- Cycle to next
	local nextIdx = (currentIdx % #targets) + 1
	self:SetTarget(targets[nextIdx])
end

function TargetController:_getTargetsInRange(): { Model }
	local myChar = localPlayer.Character
	if not myChar then return {} end

	local myRoot = myChar:FindFirstChild("HumanoidRootPart")
	if not myRoot then return {} end

	local targets = {}

	-- NPCs
	for _, model in ipairs(workspace:GetDescendants()) do
		if model:IsA("Model") and model ~= myChar and model:FindFirstChildOfClass("Humanoid") then
			local rootPart = model:FindFirstChild("HumanoidRootPart")
			if rootPart then
				local dist = (myRoot.Position - rootPart.Position).Magnitude
				if dist <= MAX_TARGET_RANGE then
					local humanoid = model:FindFirstChildOfClass("Humanoid")
					if humanoid and humanoid.Health > 0 then
						table.insert(targets, model)
					end
				end
			end
		end
	end

	-- Sort by distance
	table.sort(targets, function(a, b)
		local aRoot = a:FindFirstChild("HumanoidRootPart")
		local bRoot = b:FindFirstChild("HumanoidRootPart")
		if not aRoot or not bRoot then return false end
		return (myRoot.Position - aRoot.Position).Magnitude < (myRoot.Position - bRoot.Position).Magnitude
	end)

	return targets
end

function TargetController:_createTargetIndicator(model: Model)
	local head = model:FindFirstChild("Head")
	local rootPart = model:FindFirstChild("HumanoidRootPart")
	local attachTo = head or rootPart
	if not attachTo then return end

	local billboard = Instance.new("BillboardGui")
	billboard.Name = "TargetIndicator"
	billboard.Size = UDim2.new(3, 0, 1.5, 0)
	billboard.StudsOffset = Vector3.new(0, 4, 0)
	billboard.AlwaysOnTop = true
	billboard.Parent = attachTo

	-- Diamond/arrow indicator
	local arrow = Instance.new("TextLabel")
	arrow.Size = UDim2.new(1, 0, 0.5, 0)
	arrow.Position = UDim2.new(0, 0, 0, 0)
	arrow.BackgroundTransparency = 1
	arrow.Text = "V"
	arrow.TextColor3 = Color3.fromRGB(255, 50, 50)
	arrow.TextSize = 30
	arrow.Font = Enum.Font.GothamBold
	arrow.TextStrokeTransparency = 0
	arrow.TextStrokeColor3 = Color3.new(0, 0, 0)
	arrow.Parent = billboard

	-- Name
	local name = Instance.new("TextLabel")
	name.Size = UDim2.new(1, 0, 0.35, 0)
	name.Position = UDim2.new(0, 0, 0.45, 0)
	name.BackgroundTransparency = 1
	name.Text = "[ " .. model.Name .. " ]"
	name.TextColor3 = Color3.fromRGB(255, 80, 80)
	name.TextSize = 16
	name.Font = Enum.Font.GothamBold
	name.TextStrokeTransparency = 0
	name.TextStrokeColor3 = Color3.new(0, 0, 0)
	name.Parent = billboard

	-- Health info
	local humanoid = model:FindFirstChildOfClass("Humanoid")
	if humanoid then
		local hpLabel = Instance.new("TextLabel")
		hpLabel.Size = UDim2.new(1, 0, 0.2, 0)
		hpLabel.Position = UDim2.new(0, 0, 0.75, 0)
		hpLabel.BackgroundTransparency = 1
		hpLabel.Text = math.floor(humanoid.Health) .. " / " .. math.floor(humanoid.MaxHealth)
		hpLabel.TextColor3 = Color3.fromRGB(255, 150, 150)
		hpLabel.TextSize = 12
		hpLabel.Font = Enum.Font.Gotham
		hpLabel.TextStrokeTransparency = 0
		hpLabel.TextStrokeColor3 = Color3.new(0, 0, 0)
		hpLabel.Name = "HPLabel"
		hpLabel.Parent = billboard
	end

	-- Pulsing animation
	task.spawn(function()
		while billboard.Parent do
			TweenService:Create(arrow, TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {
				TextColor3 = Color3.fromRGB(255, 100, 100),
			}):Play()
			task.wait(0.5)
			TweenService:Create(arrow, TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {
				TextColor3 = Color3.fromRGB(255, 50, 50),
			}):Play()
			task.wait(0.5)
		end
	end)

	targetIndicator = billboard
end

function TargetController:_updateTargetIndicator()
	if not currentTarget or not targetIndicator then return end

	-- Update HP
	local humanoid = currentTarget:FindFirstChildOfClass("Humanoid")
	if humanoid then
		local hpLabel = targetIndicator:FindFirstChild("HPLabel")
		if hpLabel then
			hpLabel.Text = math.floor(humanoid.Health) .. " / " .. math.floor(humanoid.MaxHealth)
		end
	end
end

function TargetController:_updateNearbyIndicators()
	local myChar = localPlayer.Character
	if not myChar then return end

	local myRoot = myChar:FindFirstChild("HumanoidRootPart")
	if not myRoot then return end

	-- Find nearby targetable models
	local nearby = {}
	for _, model in ipairs(workspace:GetDescendants()) do
		if model:IsA("Model") and model ~= myChar and model:FindFirstChildOfClass("Humanoid") then
			local rootPart = model:FindFirstChild("HumanoidRootPart")
			if rootPart then
				local dist = (myRoot.Position - rootPart.Position).Magnitude
				if dist <= NEARBY_RANGE then
					local humanoid = model:FindFirstChildOfClass("Humanoid")
					if humanoid and humanoid.Health > 0 then
						nearby[model] = true

						-- Add subtle indicator if not already targeted
						if model ~= currentTarget and not nearbyIndicators[model] then
							local head = model:FindFirstChild("Head") or rootPart
							local bb = Instance.new("BillboardGui")
							bb.Name = "NearbyIndicator"
							bb.Size = UDim2.new(1, 0, 0.5, 0)
							bb.StudsOffset = Vector3.new(0, 3.5, 0)
							bb.AlwaysOnTop = false
							bb.Parent = head

							local dot = Instance.new("TextLabel")
							dot.Size = UDim2.fromScale(1, 1)
							dot.BackgroundTransparency = 1
							dot.Text = "o"
							dot.TextColor3 = Color3.fromRGB(200, 200, 200)
							dot.TextTransparency = 0.4
							dot.TextSize = 18
							dot.Font = Enum.Font.GothamBold
							dot.Parent = bb

							nearbyIndicators[model] = bb
						end
					end
				end
			end
		end
	end

	-- Remove indicators for models that are no longer nearby
	for model, bb in pairs(nearbyIndicators) do
		if not nearby[model] or model == currentTarget then
			bb:Destroy()
			nearbyIndicators[model] = nil
		end
	end
end

function TargetController:_faceTarget()
	if not currentTarget then return end

	local myChar = localPlayer.Character
	if not myChar then return end

	local myRoot = myChar:FindFirstChild("HumanoidRootPart")
	local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
	if not myRoot or not targetRoot then return end

	-- Smoothly rotate character to face target
	local direction = (targetRoot.Position - myRoot.Position)
	direction = Vector3.new(direction.X, 0, direction.Z) -- Keep level

	if direction.Magnitude > 0.1 then
		local targetCFrame = CFrame.lookAt(myRoot.Position, myRoot.Position + direction)
		myRoot.CFrame = myRoot.CFrame:Lerp(targetCFrame, 0.1)
	end
end

return TargetController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="101">
            <Properties>
              <string name="Name">TransformationController</string>
              <string name="Source"><![CDATA[--[[
	TransformationController.lua
	Client-side transformation visuals, cutscenes, and UI.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Transformations = require(ReplicatedStorage.Shared.Constants.Transformations)

local TransformationController = Knit.CreateController({
	Name = "TransformationController",
})

local localPlayer = Players.LocalPlayer
local transformWheelOpen = false

function TransformationController:KnitInit() end

function TransformationController:KnitStart()
	local TransformationService = Knit.GetService("TransformationService")

	-- Listen for transformation broadcasts
	TransformationService.TransformSync:Connect(function(data)
		self:_onTransformSync(data)
	end)
end

function TransformationController:_onTransformSync(data: any)
	local player = data.player
	local formId = data.formId

	local VfxController = Knit.GetController("VfxController")
	local AudioController = Knit.GetController("AudioController")

	if formId == "Base" then
		-- Revert
		VfxController:RemoveAura(player)
		AudioController:StopSound("AuraLoop")
	else
		-- Transform
		VfxController:SetAura(player, data.auraColor, data.auraId)

		if player == localPlayer then
			-- Play cutscene for local player
			self:_playTransformCutscene(data)
			AudioController:PlaySound("TransformComplete")
		end

		AudioController:PlaySound("AuraLoop")
	end
end

function TransformationController:_playTransformCutscene(data: any)
	local CameraController = Knit.GetController("CameraController")
	local character = localPlayer.Character
	if not character then return end

	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	-- Dramatic zoom
	CameraController:DramaticZoom(rootPart.Position, 1.5, function()
		-- Return to normal camera after cutscene
		task.delay(1.0, function()
			CameraController:ResetCamera()
		end)
	end)

	-- Screen flash effect
	local screenGui = localPlayer:FindFirstChildOfClass("PlayerGui")
	if screenGui then
		local flash = Instance.new("ScreenGui")
		flash.Name = "TransformFlash"
		flash.Parent = screenGui

		local frame = Instance.new("Frame")
		frame.Size = UDim2.fromScale(1, 1)
		frame.BackgroundColor3 = data.auraColor or Color3.new(1, 1, 1)
		frame.BackgroundTransparency = 0
		frame.BorderSizePixel = 0
		frame.Parent = flash

		-- Fade out
		local TweenService = game:GetService("TweenService")
		TweenService:Create(frame, TweenInfo.new(1.5), {
			BackgroundTransparency = 1,
		}):Play()

		task.delay(2, function()
			flash:Destroy()
		end)
	end
end

function TransformationController:ToggleTransformWheel()
	transformWheelOpen = not transformWheelOpen
	local UIController = Knit.GetController("UIController")
	UIController:SetMenuVisible("TransformationWheel", transformWheelOpen)
end

function TransformationController:RequestTransform(formId: string)
	local TransformationService = Knit.GetService("TransformationService")
	TransformationService:RequestTransform(formId):andThen(function(success, err)
		if not success then
			warn("[Transform] Failed: " .. tostring(err))
		end
	end)
end

function TransformationController:RequestRevert()
	local TransformationService = Knit.GetService("TransformationService")
	TransformationService:RequestRevert()
end

return TransformationController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="102">
            <Properties>
              <string name="Name">UIController</string>
              <string name="Source"><![CDATA[--[[
	UIController.lua
	Professional anime-fighter HUD inspired by Sparking Zero / FighterZ.
	Features: gradient HP bar, segmented Ki gauge, stamina bar, target frame,
	skill cooldown icons, combo counter, power level, status indicators.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local UIController = Knit.CreateController({
	Name = "UIController",
})

local localPlayer = Players.LocalPlayer
local playerGui = localPlayer:WaitForChild("PlayerGui")

local menuVisibility: { [string]: boolean } = {}
local hudElements: { [string]: any } = {}
local lastHpPercent = 1
local comboCounterVisible = false

-- ==========================================
-- HELPERS
-- ==========================================
local function makeFrame(props)
	local f = Instance.new("Frame")
	f.Size = props.Size or UDim2.new()
	f.Position = props.Position or UDim2.new()
	f.BackgroundColor3 = props.Color or Color3.fromRGB(30, 30, 40)
	f.BackgroundTransparency = props.Transparency or 0
	f.BorderSizePixel = 0
	f.Name = props.Name or "Frame"
	if props.Parent then f.Parent = props.Parent end
	if props.Corner then
		Instance.new("UICorner", f).CornerRadius = UDim.new(0, props.Corner)
	end
	return f
end

local function makeLabel(props)
	local l = Instance.new("TextLabel")
	l.Size = props.Size or UDim2.new(1, 0, 1, 0)
	l.Position = props.Position or UDim2.new()
	l.BackgroundTransparency = 1
	l.Text = props.Text or ""
	l.TextColor3 = props.Color or Color3.new(1, 1, 1)
	l.TextSize = props.TextSize or 14
	l.Font = props.Font or Enum.Font.GothamBold
	l.TextXAlignment = props.Align or Enum.TextXAlignment.Left
	l.TextStrokeTransparency = props.Stroke or 1
	l.TextStrokeColor3 = Color3.new(0, 0, 0)
	l.Name = props.Name or "Label"
	if props.Parent then l.Parent = props.Parent end
	return l
end

local function getHpColor(percent: number): Color3
	if percent > 0.6 then
		return Color3.fromRGB(50, 220, 80) -- Green
	elseif percent > 0.35 then
		return Color3.fromRGB(255, 200, 50) -- Yellow
	elseif percent > 0.15 then
		return Color3.fromRGB(255, 130, 30) -- Orange
	else
		return Color3.fromRGB(255, 50, 50) -- Red
	end
end

function UIController:KnitInit() end

function UIController:KnitStart()
	local DataService = Knit.GetService("DataService")

	DataService.PlayerDataUpdate:Connect(function(data)
		self:_refreshHUD(data)
	end)

	self:_createHUD()

	DataService:RequestPlayerData():andThen(function(data)
		if data and not data.CharacterCreated then
			-- IntroController handles intro -> character creation
		end
	end)
end

-- ==========================================
-- HUD CREATION
-- ==========================================
function UIController:_createHUD()
	local gui = Instance.new("ScreenGui")
	gui.Name = "GameHUD"
	gui.ResetOnSpawn = false
	gui.DisplayOrder = 5
	gui.Parent = playerGui
	hudElements.gui = gui

	-- ========== PLAYER STATUS (Top-Left) ==========
	local playerFrame = makeFrame({
		Name = "PlayerStatus",
		Size = UDim2.new(0, 320, 0, 95),
		Position = UDim2.new(0, 15, 0, 15),
		Color = Color3.fromRGB(15, 15, 25),
		Transparency = 0.15,
		Corner = 10,
		Parent = gui,
	})

	-- Glow border
	local playerStroke = Instance.new("UIStroke")
	playerStroke.Color = Color3.fromRGB(80, 80, 120)
	playerStroke.Thickness = 1.5
	playerStroke.Transparency = 0.3
	playerStroke.Parent = playerFrame

	-- Portrait circle
	local portrait = makeFrame({
		Name = "Portrait",
		Size = UDim2.new(0, 50, 0, 50),
		Position = UDim2.new(0, 8, 0, 8),
		Color = Color3.fromRGB(40, 40, 60),
		Corner = 25,
		Parent = playerFrame,
	})
	local portraitStroke = Instance.new("UIStroke")
	portraitStroke.Color = Color3.fromRGB(255, 215, 0)
	portraitStroke.Thickness = 2
	portraitStroke.Parent = portrait

	local raceIcon = makeLabel({
		Name = "RaceIcon",
		Text = "S",
		Size = UDim2.fromScale(1, 1),
		TextSize = 24,
		Color = Color3.fromRGB(255, 215, 0),
		Align = Enum.TextXAlignment.Center,
		Parent = portrait,
	})
	hudElements.raceIcon = raceIcon

	-- Level + Name
	local nameLabel = makeLabel({
		Name = "PlayerName",
		Text = "Lv.1 " .. localPlayer.Name,
		Size = UDim2.new(0, 200, 0, 18),
		Position = UDim2.new(0, 65, 0, 5),
		TextSize = 14,
		Color = Color3.new(1, 1, 1),
		Stroke = 0,
		Parent = playerFrame,
	})
	hudElements.nameLabel = nameLabel

	-- Transformation label
	local formLabel = makeLabel({
		Name = "FormLabel",
		Text = "Base Form",
		Size = UDim2.new(0, 150, 0, 14),
		Position = UDim2.new(0, 65, 0, 22),
		TextSize = 11,
		Color = Color3.fromRGB(180, 180, 220),
		Parent = playerFrame,
	})
	hudElements.formLabel = formLabel

	-- HP Bar
	local hpBg = makeFrame({
		Name = "HpBg",
		Size = UDim2.new(0, 240, 0, 16),
		Position = UDim2.new(0, 65, 0, 40),
		Color = Color3.fromRGB(25, 25, 35),
		Corner = 4,
		Parent = playerFrame,
	})
	Instance.new("UIStroke", hpBg).Color = Color3.fromRGB(60, 60, 80)

	-- HP damage flash layer (shows red briefly when hit)
	local hpDamageBar = makeFrame({
		Name = "HpDamage",
		Size = UDim2.fromScale(1, 1),
		Color = Color3.fromRGB(200, 50, 50),
		Corner = 4,
		Parent = hpBg,
	})
	hudElements.hpDamageBar = hpDamageBar

	-- HP actual bar
	local hpBar = makeFrame({
		Name = "HpBar",
		Size = UDim2.fromScale(1, 1),
		Color = Color3.fromRGB(50, 220, 80),
		Corner = 4,
		Parent = hpBg,
	})
	-- Gradient on HP bar
	local hpGradient = Instance.new("UIGradient")
	hpGradient.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(80, 255, 100)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(40, 200, 60)),
	})
	hpGradient.Parent = hpBar
	hudElements.hpBar = hpBar
	hudElements.hpGradient = hpGradient

	local hpText = makeLabel({
		Name = "HpText",
		Text = "100 / 100",
		Size = UDim2.fromScale(1, 1),
		TextSize = 11,
		Color = Color3.new(1, 1, 1),
		Align = Enum.TextXAlignment.Center,
		Stroke = 0,
		Parent = hpBg,
	})
	hpText.ZIndex = 5
	hudElements.hpText = hpText

	-- HP label
	makeLabel({
		Name = "HpLabel",
		Text = "HP",
		Size = UDim2.new(0, 20, 0, 16),
		Position = UDim2.new(0, 65 - 22, 0, 40),
		TextSize = 10,
		Color = Color3.fromRGB(255, 100, 100),
		Align = Enum.TextXAlignment.Center,
		Parent = playerFrame,
	})

	-- Ki Bar (segmented style like FighterZ)
	local kiBg = makeFrame({
		Name = "KiBg",
		Size = UDim2.new(0, 240, 0, 12),
		Position = UDim2.new(0, 65, 0, 60),
		Color = Color3.fromRGB(20, 20, 35),
		Corner = 3,
		Parent = playerFrame,
	})
	Instance.new("UIStroke", kiBg).Color = Color3.fromRGB(40, 60, 100)

	local kiBar = makeFrame({
		Name = "KiBar",
		Size = UDim2.fromScale(1, 1),
		Color = Color3.fromRGB(60, 150, 255),
		Corner = 3,
		Parent = kiBg,
	})
	local kiGradient = Instance.new("UIGradient")
	kiGradient.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(100, 200, 255)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(40, 120, 255)),
	})
	kiGradient.Parent = kiBar
	hudElements.kiBar = kiBar

	-- Ki segment lines (7 segments like FighterZ)
	for seg = 1, 6 do
		local line = makeFrame({
			Name = "KiSeg_" .. seg,
			Size = UDim2.new(0, 1, 1, 0),
			Position = UDim2.new(seg / 7, 0, 0, 0),
			Color = Color3.fromRGB(15, 15, 25),
			Transparency = 0.3,
			Parent = kiBg,
		})
		line.ZIndex = 4
	end

	makeLabel({
		Name = "KiLabel",
		Text = "KI",
		Size = UDim2.new(0, 20, 0, 12),
		Position = UDim2.new(0, 65 - 22, 0, 60),
		TextSize = 9,
		Color = Color3.fromRGB(100, 180, 255),
		Align = Enum.TextXAlignment.Center,
		Parent = playerFrame,
	})

	-- Stamina Bar (thin)
	local staBg = makeFrame({
		Name = "StaBg",
		Size = UDim2.new(0, 240, 0, 6),
		Position = UDim2.new(0, 65, 0, 76),
		Color = Color3.fromRGB(20, 25, 20),
		Corner = 2,
		Parent = playerFrame,
	})

	local staBar = makeFrame({
		Name = "StaBar",
		Size = UDim2.fromScale(1, 1),
		Color = Color3.fromRGB(80, 230, 80),
		Corner = 2,
		Parent = staBg,
	})
	hudElements.staBar = staBar

	-- Stamina cost markers (each vanish costs 25 stamina out of ~100)
	for marker = 1, 3 do
		makeFrame({
			Name = "StaMarker_" .. marker,
			Size = UDim2.new(0, 1, 1, 2),
			Position = UDim2.new(marker * 0.25, 0, 0, -1),
			Color = Color3.fromRGB(255, 255, 255),
			Transparency = 0.5,
			Parent = staBg,
		}).ZIndex = 4
	end

	makeLabel({
		Name = "StaLabel",
		Text = "ST",
		Size = UDim2.new(0, 20, 0, 6),
		Position = UDim2.new(0, 65 - 22, 0, 76),
		TextSize = 8,
		Color = Color3.fromRGB(80, 200, 80),
		Align = Enum.TextXAlignment.Center,
		Parent = playerFrame,
	})

	-- ========== TARGET FRAME (Top-Right, mirrors player) ==========
	local targetFrame = makeFrame({
		Name = "TargetFrame",
		Size = UDim2.new(0, 280, 0, 65),
		Position = UDim2.new(1, -295, 0, 15),
		Color = Color3.fromRGB(15, 15, 25),
		Transparency = 0.15,
		Corner = 10,
		Parent = gui,
	})
	targetFrame.Visible = false
	hudElements.targetFrame = targetFrame

	local targetStroke = Instance.new("UIStroke")
	targetStroke.Color = Color3.fromRGB(200, 60, 60)
	targetStroke.Thickness = 1.5
	targetStroke.Transparency = 0.3
	targetStroke.Parent = targetFrame

	local targetName = makeLabel({
		Name = "TargetName",
		Text = "Target",
		Size = UDim2.new(1, -15, 0, 18),
		Position = UDim2.new(0, 10, 0, 5),
		TextSize = 14,
		Color = Color3.fromRGB(255, 100, 100),
		Stroke = 0,
		Parent = targetFrame,
	})
	hudElements.targetName = targetName

	local targetHpBg = makeFrame({
		Name = "TargetHpBg",
		Size = UDim2.new(0, 250, 0, 14),
		Position = UDim2.new(0, 10, 0, 28),
		Color = Color3.fromRGB(25, 25, 35),
		Corner = 4,
		Parent = targetFrame,
	})
	Instance.new("UIStroke", targetHpBg).Color = Color3.fromRGB(80, 40, 40)

	local targetHpBar = makeFrame({
		Name = "TargetHpBar",
		Size = UDim2.fromScale(1, 1),
		Color = Color3.fromRGB(220, 50, 50),
		Corner = 4,
		Parent = targetHpBg,
	})
	hudElements.targetHpBar = targetHpBar

	local targetHpText = makeLabel({
		Name = "TargetHpText",
		Text = "",
		Size = UDim2.fromScale(1, 1),
		TextSize = 10,
		Align = Enum.TextXAlignment.Center,
		Stroke = 0,
		Parent = targetHpBg,
	})
	targetHpText.ZIndex = 5
	hudElements.targetHpText = targetHpText

	local targetLevelLabel = makeLabel({
		Name = "TargetLevel",
		Text = "",
		Size = UDim2.new(0, 100, 0, 14),
		Position = UDim2.new(0, 10, 0, 46),
		TextSize = 11,
		Color = Color3.fromRGB(180, 180, 200),
		Parent = targetFrame,
	})
	hudElements.targetLevelLabel = targetLevelLabel

	-- ========== MOVE BUBBLES (Bottom-Center) ==========
	-- Single container with skill bubbles. Basic controls shown inside each bubble's tooltip.

	local SLOT_COLORS = {
		Color3.fromRGB(50, 50, 100),
		Color3.fromRGB(50, 50, 100),
		Color3.fromRGB(50, 50, 100),
		Color3.fromRGB(50, 50, 100),
		Color3.fromRGB(100, 40, 40),
		Color3.fromRGB(40, 100, 50),
	}
	local SLOT_KEYS = { "[Z]", "[X]", "[V]", "[B]", "[N] ULT", "[G] EVD" }
	local SLOT_BORDER_COLORS = {
		Color3.fromRGB(100, 100, 220),
		Color3.fromRGB(100, 100, 220),
		Color3.fromRGB(100, 100, 220),
		Color3.fromRGB(100, 100, 220),
		Color3.fromRGB(255, 80, 80),
		Color3.fromRGB(80, 220, 100),
	}

	-- Master container — holds controls hint row + bubble row
	local bottomContainer = makeFrame({
		Name = "BottomContainer",
		Size = UDim2.new(0, 620, 0, 120),
		Position = UDim2.new(0.5, -310, 1, -128),
		Transparency = 1,
		Parent = gui,
	})

	-- Controls hint row (compact, above the bubbles)
	local controlsHint = makeFrame({
		Name = "ControlsHint",
		Size = UDim2.new(1, 0, 0, 22),
		Position = UDim2.new(0, 0, 0, 0),
		Color = Color3.fromRGB(15, 15, 25),
		Transparency = 0.3,
		Corner = 6,
		Parent = bottomContainer,
	})

	local controlsText = makeLabel({
		Name = "ControlsText",
		Text = "M1 Light  |  M2 Heavy  |  E Ki Blast  |  R Charged  |  F Block  |  Q Vanish  |  C Charge Ki  |  T Transform  |  Tab Lock-On  |  Z X V B Skills  |  N Ult  |  G Evade",
		Size = UDim2.fromScale(1, 1),
		TextSize = 10,
		Color = Color3.fromRGB(160, 160, 190),
		Align = Enum.TextXAlignment.Center,
		Font = Enum.Font.Gotham,
		Parent = controlsHint,
	})

	-- Bubble row
	local bubbleBar = makeFrame({
		Name = "BubbleBar",
		Size = UDim2.new(1, 0, 0, 90),
		Position = UDim2.new(0, 0, 0, 28),
		Transparency = 1,
		Parent = bottomContainer,
	})

	local bubbleLayout = Instance.new("UIListLayout")
	bubbleLayout.FillDirection = Enum.FillDirection.Horizontal
	bubbleLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
	bubbleLayout.VerticalAlignment = Enum.VerticalAlignment.Center
	bubbleLayout.Padding = UDim.new(0, 10)
	bubbleLayout.Parent = bubbleBar

	hudElements.skillSlots = {}

	-- Skill move bubbles (the 6 loadout slots)
	for i = 1, 6 do
		local bubble = makeFrame({
			Name = "SkillBubble_" .. i,
			Size = UDim2.new(0, 85, 0, 85),
			Color = SLOT_COLORS[i],
			Transparency = 0.15,
			Corner = 42,
			Parent = bubbleBar,
		})

		-- Glowing border
		local stroke = Instance.new("UIStroke")
		stroke.Color = SLOT_BORDER_COLORS[i]
		stroke.Thickness = 2.5
		stroke.Transparency = 0.2
		stroke.Parent = bubble

		-- Key binding label (top)
		makeLabel({
			Name = "KeyBind",
			Text = SLOT_KEYS[i],
			Size = UDim2.new(1, 0, 0, 14),
			Position = UDim2.new(0, 0, 0, 6),
			TextSize = 10,
			Color = SLOT_BORDER_COLORS[i],
			Align = Enum.TextXAlignment.Center,
			Font = Enum.Font.GothamBold,
			Parent = bubble,
		})

		-- Move name (center, big)
		local moveName = makeLabel({
			Name = "MoveName",
			Text = "Empty",
			Size = UDim2.new(1, -8, 0, 28),
			Position = UDim2.new(0, 4, 0, 20),
			TextSize = 11,
			Color = Color3.new(1, 1, 1),
			Align = Enum.TextXAlignment.Center,
			Font = Enum.Font.GothamBold,
			Parent = bubble,
		})
		moveName.TextWrapped = true

		-- Ki cost (bottom)
		local kiCost = makeLabel({
			Name = "KiCost",
			Text = "",
			Size = UDim2.new(1, 0, 0, 12),
			Position = UDim2.new(0, 0, 0, 50),
			TextSize = 9,
			Color = Color3.fromRGB(100, 180, 255),
			Align = Enum.TextXAlignment.Center,
			Font = Enum.Font.Gotham,
			Parent = bubble,
		})

		-- Cooldown label (bottom)
		local cdLabel = makeLabel({
			Name = "CooldownLabel",
			Text = "",
			Size = UDim2.new(1, 0, 0, 10),
			Position = UDim2.new(0, 0, 0, 62),
			TextSize = 8,
			Color = Color3.fromRGB(180, 150, 100),
			Align = Enum.TextXAlignment.Center,
			Font = Enum.Font.Gotham,
			Parent = bubble,
		})

		-- Cooldown dark overlay (sweeps down from top when on cooldown)
		local cooldownOverlay = makeFrame({
			Name = "CooldownOverlay",
			Size = UDim2.fromScale(1, 0),
			Color = Color3.new(0, 0, 0),
			Transparency = 0.4,
			Corner = 42,
			Parent = bubble,
		})

		-- Tooltip on hover (shows description)
		local tooltip = makeFrame({
			Name = "Tooltip",
			Size = UDim2.new(0, 180, 0, 60),
			Position = UDim2.new(0.5, -90, 0, -68),
			Color = Color3.fromRGB(15, 15, 30),
			Transparency = 0.05,
			Corner = 8,
			Parent = bubble,
		})
		tooltip.Visible = false
		tooltip.ZIndex = 10

		local tooltipStroke = Instance.new("UIStroke")
		tooltipStroke.Color = SLOT_BORDER_COLORS[i]
		tooltipStroke.Thickness = 1
		tooltipStroke.Parent = tooltip

		local tooltipTitle = makeLabel({
			Name = "TooltipTitle",
			Text = "",
			Size = UDim2.new(1, -10, 0, 16),
			Position = UDim2.new(0, 5, 0, 3),
			TextSize = 12,
			Color = Color3.new(1, 1, 1),
			Font = Enum.Font.GothamBold,
			Parent = tooltip,
		})
		tooltipTitle.ZIndex = 11

		local tooltipDesc = makeLabel({
			Name = "TooltipDesc",
			Text = "",
			Size = UDim2.new(1, -10, 0, 36),
			Position = UDim2.new(0, 5, 0, 20),
			TextSize = 10,
			Color = Color3.fromRGB(180, 180, 200),
			Font = Enum.Font.Gotham,
			Parent = tooltip,
		})
		tooltipDesc.TextWrapped = true
		tooltipDesc.ZIndex = 11

		-- Hover detection via invisible button
		local hoverBtn = Instance.new("TextButton")
		hoverBtn.Size = UDim2.fromScale(1, 1)
		hoverBtn.BackgroundTransparency = 1
		hoverBtn.Text = ""
		hoverBtn.ZIndex = 6
		hoverBtn.Parent = bubble

		hoverBtn.MouseEnter:Connect(function()
			tooltip.Visible = true
			TweenService:Create(stroke, TweenInfo.new(0.15), { Color = Color3.new(1, 1, 1) }):Play()
			TweenService:Create(bubble, TweenInfo.new(0.15), { Size = UDim2.new(0, 90, 0, 90) }):Play()
		end)
		hoverBtn.MouseLeave:Connect(function()
			tooltip.Visible = false
			TweenService:Create(stroke, TweenInfo.new(0.15), { Color = SLOT_BORDER_COLORS[i] }):Play()
			TweenService:Create(bubble, TweenInfo.new(0.15), { Size = UDim2.new(0, 85, 0, 85) }):Play()
		end)

		hudElements.skillSlots[i] = {
			slot = bubble,
			moveName = moveName,
			kiCost = kiCost,
			cdLabel = cdLabel,
			cooldown = cooldownOverlay,
			tooltipTitle = tooltipTitle,
			tooltipDesc = tooltipDesc,
			stroke = stroke,
		}
	end

	-- ========== COMBO COUNTER (Right side) ==========
	local comboFrame = makeFrame({
		Name = "ComboFrame",
		Size = UDim2.new(0, 150, 0, 70),
		Position = UDim2.new(1, -170, 0.4, 0),
		Transparency = 1,
		Parent = gui,
	})
	comboFrame.Visible = false
	hudElements.comboFrame = comboFrame

	local comboCount = makeLabel({
		Name = "ComboCount",
		Text = "0",
		Size = UDim2.new(1, 0, 0, 45),
		TextSize = 42,
		Color = Color3.fromRGB(255, 200, 50),
		Align = Enum.TextXAlignment.Right,
		Stroke = 0,
		Parent = comboFrame,
	})
	hudElements.comboCount = comboCount

	local comboLabel = makeLabel({
		Name = "ComboLabel",
		Text = "HITS",
		Size = UDim2.new(1, 0, 0, 18),
		Position = UDim2.new(0, 0, 0, 42),
		TextSize = 14,
		Color = Color3.fromRGB(255, 180, 50),
		Align = Enum.TextXAlignment.Right,
		Stroke = 0,
		Parent = comboFrame,
	})

	local comboDamage = makeLabel({
		Name = "ComboDamage",
		Text = "0 DMG",
		Size = UDim2.new(1, 0, 0, 14),
		Position = UDim2.new(0, 0, 0, 58),
		TextSize = 11,
		Color = Color3.fromRGB(200, 150, 50),
		Align = Enum.TextXAlignment.Right,
		Parent = comboFrame,
	})
	hudElements.comboDamage = comboDamage

	-- ========== POWER LEVEL (Below player frame) ==========
	local powerLabel = makeLabel({
		Name = "PowerLevel",
		Text = "Power Level: 0",
		Size = UDim2.new(0, 200, 0, 16),
		Position = UDim2.new(0, 15, 0, 115),
		TextSize = 12,
		Color = Color3.fromRGB(200, 200, 220),
		Parent = gui,
	})
	hudElements.powerLabel = powerLabel

	-- ========== XP BAR (Bottom) ==========
	local xpBg = makeFrame({
		Name = "XpBg",
		Size = UDim2.new(0.5, 0, 0, 6),
		Position = UDim2.new(0.25, 0, 1, -8),
		Color = Color3.fromRGB(20, 20, 30),
		Corner = 3,
		Parent = gui,
	})

	local xpBar = makeFrame({
		Name = "XpBar",
		Size = UDim2.fromScale(0, 1),
		Color = Color3.fromRGB(255, 255, 80),
		Corner = 3,
		Parent = xpBg,
	})
	local xpGrad = Instance.new("UIGradient")
	xpGrad.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 255, 120)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(200, 200, 50)),
	})
	xpGrad.Parent = xpBar
	hudElements.xpBar = xpBar

	local xpText = makeLabel({
		Name = "XpText",
		Text = "Lv.1 — 0 / 100 XP",
		Size = UDim2.new(1, 0, 0, 18),
		Position = UDim2.new(0, 0, 0, -16),
		TextSize = 10,
		Color = Color3.fromRGB(200, 200, 180),
		Align = Enum.TextXAlignment.Center,
		Parent = xpBg,
	})
	hudElements.xpText = xpText

	-- ========== ZENI (Top-right corner) ==========
	local zeniLabel = makeLabel({
		Name = "Zeni",
		Text = "0 Zeni",
		Size = UDim2.new(0, 120, 0, 20),
		Position = UDim2.new(1, -135, 0, 85),
		TextSize = 14,
		Color = Color3.fromRGB(255, 215, 0),
		Align = Enum.TextXAlignment.Right,
		Stroke = 0,
		Parent = gui,
	})
	hudElements.zeniLabel = zeniLabel

	-- ========== LEVEL UP POPUP ==========
	local levelUpPopup = makeLabel({
		Name = "LevelUp",
		Text = "LEVEL UP!",
		Size = UDim2.new(0, 300, 0, 50),
		Position = UDim2.new(0.5, -150, 0.3, 0),
		TextSize = 36,
		Color = Color3.fromRGB(255, 255, 50),
		Align = Enum.TextXAlignment.Center,
		Stroke = 0,
		Parent = gui,
	})
	levelUpPopup.TextTransparency = 1
	hudElements.levelUpPopup = levelUpPopup

	-- ========== TARGET FRAME UPDATE LOOP ==========
	RunService.RenderStepped:Connect(function()
		self:_updateTargetFrame()
	end)
end

-- ==========================================
-- HUD UPDATES
-- ==========================================
function UIController:_refreshHUD(data: any)
	if not hudElements.gui then return end

	-- Name + Level
	if hudElements.nameLabel then
		hudElements.nameLabel.Text = "Lv." .. data.Level .. " " .. localPlayer.Name
	end

	-- Race icon
	if hudElements.raceIcon then
		local raceLetters = { Saiyan = "S", Namekian = "N", FriezaRace = "F", Majin = "M", Android = "A", Human = "H" }
		hudElements.raceIcon.Text = raceLetters[data.Race] or "?"
	end

	-- Transformation label
	if hudElements.formLabel then
		local formText = data.CurrentTransformation == "Base" and "Base Form" or data.CurrentTransformation
		hudElements.formLabel.Text = formText

		-- Update portrait border color to match transformation aura
		local Transformations = require(ReplicatedStorage.Shared.Constants.Transformations)
		local form = Transformations:Get(data.CurrentTransformation)
		local portrait = hudElements.gui:FindFirstChild("PlayerStatus") and hudElements.gui.PlayerStatus:FindFirstChild("Portrait")
		if portrait then
			local stroke = portrait:FindFirstChildOfClass("UIStroke")
			if stroke then
				stroke.Color = form and form.auraColor or Color3.fromRGB(255, 215, 0)
			end
		end
	end

	-- Zeni
	if hudElements.zeniLabel then
		hudElements.zeniLabel.Text = tostring(data.Zeni) .. " Zeni"
	end

	-- XP
	local Stats = require(ReplicatedStorage.Shared.Constants.Stats)
	local required = Stats:GetRequiredXP(data.Level)
	local xpPercent = if required > 0 then data.XP / required else 0

	if hudElements.xpBar then
		TweenService:Create(hudElements.xpBar, TweenInfo.new(0.3), {
			Size = UDim2.fromScale(math.clamp(xpPercent, 0, 1), 1),
		}):Play()
	end
	if hudElements.xpText then
		hudElements.xpText.Text = "Lv." .. data.Level .. " — " .. data.XP .. " / " .. required .. " XP"
	end

	-- Power Level (sum of all effective stats)
	if hudElements.powerLabel then
		local power = 0
		for _, v in pairs(data.Stats) do
			power = power + v
		end
		power = power * data.Level
		hudElements.powerLabel.Text = "Power Level: " .. tostring(math.floor(power))
	end

	-- Skill loadout bubbles — populate with full move info
	if hudElements.skillSlots and data.MoveLoadout then
		local Moves = require(ReplicatedStorage.Shared.Constants.Moves)
		for i = 1, 6 do
			local slot = hudElements.skillSlots[i]
			if not slot then continue end

			local moveId = data.MoveLoadout[i]
			if moveId then
				local move = Moves:Get(moveId)
				if move then
					slot.moveName.Text = move.displayName
					-- Ki cost
					local ki = move.kiCost or move.kiCostBase or 0
					slot.kiCost.Text = ki > 0 and (ki .. " Ki") or "Free"
					-- Cooldown
					local cd = move.cooldown or 0
					slot.cdLabel.Text = cd > 0 and (cd .. "s CD") or ""
					-- Tooltip
					slot.tooltipTitle.Text = move.displayName .. " [" .. move.category .. "]"
					slot.tooltipDesc.Text = move.description or ""
					-- Style active
					slot.moveName.TextColor3 = Color3.new(1, 1, 1)
				else
					slot.moveName.Text = moveId
					slot.kiCost.Text = ""
					slot.cdLabel.Text = ""
					slot.tooltipTitle.Text = moveId
					slot.tooltipDesc.Text = ""
				end
			else
				slot.moveName.Text = "Empty"
				slot.moveName.TextColor3 = Color3.fromRGB(80, 80, 100)
				slot.kiCost.Text = ""
				slot.cdLabel.Text = ""
				slot.tooltipTitle.Text = "No move equipped"
				slot.tooltipDesc.Text = "Open Move Loadout (K) to equip a move to this slot."
			end
		end
	end
end

function UIController:UpdateHealthBar(current: number, max: number)
	if not hudElements.hpBar then return end

	local percent = if max > 0 then current / max else 0
	percent = math.clamp(percent, 0, 1)

	-- Damage flash: show red bar at old position, then tween down
	if percent < lastHpPercent and hudElements.hpDamageBar then
		hudElements.hpDamageBar.Size = UDim2.fromScale(lastHpPercent, 1)
		TweenService:Create(hudElements.hpDamageBar, TweenInfo.new(0.6, Enum.EasingStyle.Quad, Enum.EasingDirection.Out, 0, false, 0.2), {
			Size = UDim2.fromScale(percent, 1),
		}):Play()
	end

	-- Smooth HP bar tween
	TweenService:Create(hudElements.hpBar, TweenInfo.new(0.2), {
		Size = UDim2.fromScale(percent, 1),
	}):Play()

	-- Color change based on HP
	local color = getHpColor(percent)
	if hudElements.hpGradient then
		local bright = Color3.new(
			math.min(1, color.R * 1.3),
			math.min(1, color.G * 1.3),
			math.min(1, color.B * 1.3)
		)
		hudElements.hpGradient.Color = ColorSequence.new({ ColorSequenceKeypoint.new(0, bright), ColorSequenceKeypoint.new(1, color) })
	end

	-- HP text
	if hudElements.hpText then
		hudElements.hpText.Text = math.floor(current) .. " / " .. math.floor(max)
	end

	-- Low HP warning pulse
	if percent <= 0.2 and hudElements.hpBar then
		TweenService:Create(hudElements.hpBar, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, 0, true), {
			BackgroundTransparency = 0.4,
		}):Play()
	end

	lastHpPercent = percent
end

function UIController:UpdateKiBar(current: number, max: number)
	if not hudElements.kiBar then return end
	local percent = if max > 0 then current / max else 0
	TweenService:Create(hudElements.kiBar, TweenInfo.new(0.15), {
		Size = UDim2.fromScale(math.clamp(percent, 0, 1), 1),
	}):Play()
end

function UIController:UpdateStaminaBar(current: number, max: number)
	if not hudElements.staBar then return end
	local percent = if max > 0 then current / max else 0
	TweenService:Create(hudElements.staBar, TweenInfo.new(0.15), {
		Size = UDim2.fromScale(math.clamp(percent, 0, 1), 1),
	}):Play()
end

-- ==========================================
-- TARGET FRAME (updates every frame)
-- ==========================================
function UIController:_updateTargetFrame()
	local TargetController = Knit.GetController("TargetController")
	if not TargetController then return end

	local targetModel = TargetController:GetTarget()

	if not targetModel then
		if hudElements.targetFrame then
			hudElements.targetFrame.Visible = false
		end
		return
	end

	if hudElements.targetFrame then
		hudElements.targetFrame.Visible = true
	end

	-- Name
	if hudElements.targetName then
		hudElements.targetName.Text = targetModel.Name
	end

	-- HP
	local humanoid = targetModel:FindFirstChildOfClass("Humanoid")
	if humanoid and hudElements.targetHpBar then
		local percent = if humanoid.MaxHealth > 0 then humanoid.Health / humanoid.MaxHealth else 0
		hudElements.targetHpBar.Size = UDim2.fromScale(math.clamp(percent, 0, 1), 1)
		hudElements.targetHpBar.BackgroundColor3 = getHpColor(percent)

		if hudElements.targetHpText then
			hudElements.targetHpText.Text = math.floor(humanoid.Health) .. " / " .. math.floor(humanoid.MaxHealth)
		end
	end
end

-- ==========================================
-- COMBO COUNTER
-- ==========================================
function UIController:ShowComboCounter(hits: number, totalDamage: number)
	if not hudElements.comboFrame then return end

	hudElements.comboFrame.Visible = true

	if hudElements.comboCount then
		hudElements.comboCount.Text = tostring(hits)
		-- Pop animation
		hudElements.comboCount.TextSize = 25
		TweenService:Create(hudElements.comboCount, TweenInfo.new(0.15, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
			TextSize = 42,
		}):Play()
	end

	if hudElements.comboDamage then
		hudElements.comboDamage.Text = tostring(math.floor(totalDamage)) .. " DMG"
	end

	-- Auto-hide after 2 seconds of no updates
	if comboCounterVisible then return end
	comboCounterVisible = true

	task.spawn(function()
		local lastHits = hits
		while true do
			task.wait(2)
			if hudElements.comboCount and tonumber(hudElements.comboCount.Text) == lastHits then
				-- No new hits — fade out
				if hudElements.comboFrame then
					hudElements.comboFrame.Visible = false
				end
				comboCounterVisible = false
				return
			end
			lastHits = tonumber(hudElements.comboCount.Text) or 0
		end
	end)
end

-- ==========================================
-- LEVEL UP POPUP
-- ==========================================
function UIController:ShowLevelUp(newLevel: number)
	local popup = hudElements.levelUpPopup
	if not popup then return end

	popup.Text = "LEVEL UP! Lv." .. newLevel
	popup.TextTransparency = 0
	popup.TextSize = 20

	TweenService:Create(popup, TweenInfo.new(0.3, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
		TextSize = 40,
	}):Play()

	task.delay(2, function()
		TweenService:Create(popup, TweenInfo.new(0.5), {
			TextTransparency = 1,
		}):Play()
	end)
end

-- ==========================================
-- MENUS
-- ==========================================
function UIController:_showCharacterCreation()
	local CharacterCreation = require(script.Parent.Parent.UI.Menus.CharacterCreation)
	CharacterCreation.Show()
end

function UIController:SetMenuVisible(menuName: string, visible: boolean)
	menuVisibility[menuName] = visible

	if menuName == "TransformationWheel" then
		local TransformationWheel = require(script.Parent.Parent.UI.Menus.TransformationWheel)
		if visible then
			local DataService = Knit.GetService("DataService")
			DataService:RequestPlayerData():andThen(function(data)
				if data then
					TransformationWheel.Show(data.UnlockedTransformations, data.CurrentTransformation, data.Race)
				end
			end)
		else
			TransformationWheel.Hide()
		end
	elseif menuName == "QuestLog" then
		local QuestLog = require(script.Parent.Parent.UI.Menus.QuestLog)
		if visible then
			local DataService = Knit.GetService("DataService")
			DataService:RequestPlayerData():andThen(function(data) QuestLog.Show(data) end)
		else
			QuestLog.Hide()
		end
	elseif menuName == "Shop" then
		local Shop = require(script.Parent.Parent.UI.Menus.Shop)
		if visible then
			local DataService = Knit.GetService("DataService")
			DataService:RequestPlayerData():andThen(function(data) Shop.Show(data) end)
		else
			Shop.Hide()
		end
	elseif menuName == "MoveLoadout" then
		local MoveLoadout = require(script.Parent.Parent.UI.Menus.MoveLoadout)
		if visible then
			local DataService = Knit.GetService("DataService")
			DataService:RequestPlayerData():andThen(function(data) MoveLoadout.Show(data) end)
		else
			MoveLoadout.Hide()
		end
	elseif menuName == "Settings" then
		local SettingsMenu = require(script.Parent.Parent.UI.Menus.Settings)
		if visible then SettingsMenu.Show() else SettingsMenu.Hide() end
	end
end

return UIController
]]></string>
            </Properties>
          </Item>
          <Item class="ModuleScript" referent="103">
            <Properties>
              <string name="Name">VfxController</string>
              <string name="Source"><![CDATA[--[[
	VfxController.lua
	Complete VFX system: per-transformation auras with lightning, ki blast projectiles,
	beam weapons, impact effects, transformation cutscenes, knockback trails.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)

local VfxController = Knit.CreateController({ Name = "VfxController" })

local localPlayer = Players.LocalPlayer
local activeAuras: { [Player]: { parts: { Instance } } } = {}
local activeEffects: { Instance } = {}

-- ==========================================
-- AURA DEFINITIONS (per transformation)
-- ==========================================
local AURA_CONFIGS = {
	Base = nil,
	Kaioken = {
		innerColor = Color3.fromRGB(255, 40, 40),
		outerColor = Color3.fromRGB(200, 20, 20),
		lightColor = Color3.fromRGB(255, 50, 50),
		innerRate = 40, outerRate = 25,
		innerSize = 2, outerSize = 3,
		innerSpeed = 6, outerSpeed = 10,
		lightBrightness = 1.5, lightRange = 15,
		hasLightning = false,
		groundGlow = Color3.fromRGB(255, 50, 50),
	},
	SSJ = {
		innerColor = Color3.fromRGB(255, 255, 60),
		outerColor = Color3.fromRGB(255, 220, 30),
		lightColor = Color3.fromRGB(255, 255, 80),
		innerRate = 60, outerRate = 35,
		innerSize = 2.5, outerSize = 4,
		innerSpeed = 8, outerSpeed = 12,
		lightBrightness = 2, lightRange = 20,
		hasLightning = true,
		lightningColor = Color3.fromRGB(255, 255, 100),
		lightningRate = 3,
		groundGlow = Color3.fromRGB(255, 255, 50),
	},
	SSJ2 = {
		innerColor = Color3.fromRGB(255, 255, 80),
		outerColor = Color3.fromRGB(255, 230, 40),
		lightColor = Color3.fromRGB(255, 255, 100),
		innerRate = 70, outerRate = 40,
		innerSize = 3, outerSize = 4.5,
		innerSpeed = 10, outerSpeed = 14,
		lightBrightness = 2.5, lightRange = 25,
		hasLightning = true,
		lightningColor = Color3.fromRGB(100, 150, 255),
		lightningRate = 6,
		groundGlow = Color3.fromRGB(255, 255, 80),
	},
	SSJ3 = {
		innerColor = Color3.fromRGB(255, 255, 50),
		outerColor = Color3.fromRGB(255, 200, 20),
		lightColor = Color3.fromRGB(255, 255, 60),
		innerRate = 80, outerRate = 50,
		innerSize = 4, outerSize = 6,
		innerSpeed = 12, outerSpeed = 16,
		lightBrightness = 3, lightRange = 30,
		hasLightning = true,
		lightningColor = Color3.fromRGB(255, 255, 80),
		lightningRate = 8,
		groundGlow = Color3.fromRGB(255, 255, 40),
		hasDebris = true,
	},
	SSG = {
		innerColor = Color3.fromRGB(255, 60, 60),
		outerColor = Color3.fromRGB(200, 40, 60),
		lightColor = Color3.fromRGB(255, 80, 80),
		innerRate = 50, outerRate = 30,
		innerSize = 2, outerSize = 3,
		innerSpeed = 6, outerSpeed = 8,
		lightBrightness = 2, lightRange = 18,
		hasLightning = false,
		groundGlow = Color3.fromRGB(255, 60, 60),
	},
	SSB = {
		innerColor = Color3.fromRGB(50, 150, 255),
		outerColor = Color3.fromRGB(30, 100, 255),
		lightColor = Color3.fromRGB(80, 180, 255),
		innerRate = 65, outerRate = 40,
		innerSize = 2.5, outerSize = 4,
		innerSpeed = 8, outerSpeed = 12,
		lightBrightness = 2.5, lightRange = 22,
		hasLightning = true,
		lightningColor = Color3.fromRGB(150, 200, 255),
		lightningRate = 4,
		groundGlow = Color3.fromRGB(50, 150, 255),
		hasSparkles = true,
	},
	SSBE = {
		innerColor = Color3.fromRGB(30, 80, 255),
		outerColor = Color3.fromRGB(20, 50, 200),
		lightColor = Color3.fromRGB(50, 120, 255),
		innerRate = 75, outerRate = 45,
		innerSize = 3, outerSize = 5,
		innerSpeed = 10, outerSpeed = 14,
		lightBrightness = 3, lightRange = 25,
		hasLightning = true,
		lightningColor = Color3.fromRGB(100, 150, 255),
		lightningRate = 6,
		groundGlow = Color3.fromRGB(30, 100, 255),
	},
	UltraInstinct = {
		innerColor = Color3.fromRGB(200, 200, 240),
		outerColor = Color3.fromRGB(180, 180, 220),
		lightColor = Color3.fromRGB(220, 220, 255),
		innerRate = 50, outerRate = 30,
		innerSize = 2, outerSize = 3.5,
		innerSpeed = 5, outerSpeed = 8,
		lightBrightness = 2, lightRange = 20,
		hasLightning = false,
		groundGlow = Color3.fromRGB(200, 200, 255),
		hasSparkles = true,
		ethereal = true,
	},
	GoldenForm = {
		innerColor = Color3.fromRGB(255, 215, 0),
		outerColor = Color3.fromRGB(255, 180, 0),
		lightColor = Color3.fromRGB(255, 215, 50),
		innerRate = 60, outerRate = 35,
		innerSize = 2.5, outerSize = 4,
		innerSpeed = 8, outerSpeed = 12,
		lightBrightness = 2.5, lightRange = 22,
		hasLightning = true,
		lightningColor = Color3.fromRGB(255, 255, 100),
		lightningRate = 3,
		groundGlow = Color3.fromRGB(255, 200, 0),
	},
}

function VfxController:KnitInit() end
function VfxController:KnitStart() end

-- ==========================================
-- AURA SYSTEM
-- ==========================================
function VfxController:SetAura(player: Player, auraColor: Color3?, auraId: string?)
	self:RemoveAura(player)
	if not auraColor then return end

	local character = player.Character
	if not character then return end
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	-- Find matching config or create generic one
	local config = AURA_CONFIGS[auraId and auraId:gsub("_Aura", "") or ""] or {
		innerColor = auraColor,
		outerColor = auraColor,
		lightColor = auraColor,
		innerRate = 50, outerRate = 30,
		innerSize = 2.5, outerSize = 3.5,
		innerSpeed = 7, outerSpeed = 10,
		lightBrightness = 2, lightRange = 18,
		groundGlow = auraColor,
	}

	local parts = {}

	-- Inner glow particles
	local inner = Instance.new("ParticleEmitter")
	inner.Name = "AuraInner"
	inner.Color = ColorSequence.new(config.innerColor)
	inner.Size = NumberSequence.new({
		NumberSequenceKeypoint.new(0, config.innerSize),
		NumberSequenceKeypoint.new(0.5, config.innerSize * 0.7),
		NumberSequenceKeypoint.new(1, 0),
	})
	inner.Lifetime = NumberRange.new(0.3, 0.6)
	inner.Rate = config.innerRate
	inner.Speed = NumberRange.new(config.innerSpeed, config.innerSpeed * 1.5)
	inner.SpreadAngle = Vector2.new(20, 20)
	inner.LightEmission = 1
	inner.Transparency = NumberSequence.new({
		NumberSequenceKeypoint.new(0, 0.2),
		NumberSequenceKeypoint.new(0.5, 0.4),
		NumberSequenceKeypoint.new(1, 1),
	})
	inner.EmissionDirection = Enum.NormalId.Top
	inner.Parent = rootPart
	table.insert(parts, inner)

	-- Outer flame particles
	local outer = Instance.new("ParticleEmitter")
	outer.Name = "AuraOuter"
	outer.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, config.outerColor),
		ColorSequenceKeypoint.new(1, config.innerColor),
	})
	outer.Size = NumberSequence.new({
		NumberSequenceKeypoint.new(0, config.outerSize),
		NumberSequenceKeypoint.new(1, 0),
	})
	outer.Lifetime = NumberRange.new(0.5, 1.0)
	outer.Rate = config.outerRate
	outer.Speed = NumberRange.new(config.outerSpeed, config.outerSpeed * 1.5)
	outer.SpreadAngle = Vector2.new(30, 30)
	outer.LightEmission = 0.8
	outer.Transparency = NumberSequence.new({
		NumberSequenceKeypoint.new(0, 0.4),
		NumberSequenceKeypoint.new(1, 1),
	})
	outer.EmissionDirection = Enum.NormalId.Top
	outer.Parent = rootPart
	table.insert(parts, outer)

	-- Point light
	local light = Instance.new("PointLight")
	light.Name = "AuraLight"
	light.Color = config.lightColor
	light.Brightness = config.lightBrightness
	light.Range = config.lightRange
	light.Parent = rootPart
	table.insert(parts, light)

	-- Ground glow circle
	if config.groundGlow then
		local glow = Instance.new("Part")
		glow.Name = "GroundGlow"
		glow.Size = Vector3.new(8, 0.1, 8)
		glow.Shape = Enum.PartType.Cylinder
		glow.Color = config.groundGlow
		glow.Material = Enum.Material.Neon
		glow.Transparency = 0.6
		glow.Anchored = false
		glow.CanCollide = false
		glow.CFrame = rootPart.CFrame * CFrame.new(0, -3, 0) * CFrame.Angles(0, 0, math.rad(90))
		glow.Parent = character

		local weld = Instance.new("WeldConstraint")
		weld.Part0 = rootPart
		weld.Part1 = glow
		weld.Parent = glow

		table.insert(parts, glow)
	end

	-- Lightning bolts (SSJ2+)
	if config.hasLightning then
		task.spawn(function()
			while activeAuras[player] do
				self:_spawnLightningBolt(rootPart, config.lightningColor, character)
				task.wait(1 / (config.lightningRate or 3))
			end
		end)
	end

	-- Sparkle particles (SSB, UI)
	if config.hasSparkles then
		local sparkle = Instance.new("ParticleEmitter")
		sparkle.Name = "AuraSparkle"
		sparkle.Color = ColorSequence.new(Color3.new(1, 1, 1))
		sparkle.Size = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.3), NumberSequenceKeypoint.new(1, 0) })
		sparkle.Lifetime = NumberRange.new(0.3, 0.8)
		sparkle.Rate = 20
		sparkle.Speed = NumberRange.new(2, 5)
		sparkle.SpreadAngle = Vector2.new(180, 180)
		sparkle.LightEmission = 1
		sparkle.Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) })
		sparkle.Parent = rootPart
		table.insert(parts, sparkle)
	end

	-- Rising debris (SSJ3)
	if config.hasDebris then
		task.spawn(function()
			while activeAuras[player] do
				self:_spawnDebrisChunk(rootPart.Position)
				task.wait(0.5)
			end
		end)
	end

	activeAuras[player] = { parts = parts }
end

function VfxController:RemoveAura(player: Player)
	local aura = activeAuras[player]
	if aura then
		for _, part in ipairs(aura.parts) do
			if part and part.Parent then
				part:Destroy()
			end
		end
		activeAuras[player] = nil
	end
end

function VfxController:_spawnLightningBolt(rootPart: BasePart, color: Color3, parent: Instance)
	local angle = math.random() * math.pi * 2
	local dist = math.random(2, 5)
	local height = math.random(0, 6)

	local startPos = rootPart.Position + Vector3.new(math.cos(angle) * dist, height, math.sin(angle) * dist)
	local endPos = startPos + Vector3.new(math.random(-3, 3), math.random(-2, 4), math.random(-3, 3))

	local bolt = Instance.new("Part")
	bolt.Name = "Lightning"
	bolt.Size = Vector3.new(0.15, 0.15, (startPos - endPos).Magnitude)
	bolt.CFrame = CFrame.lookAt(startPos, endPos) * CFrame.new(0, 0, -(startPos - endPos).Magnitude / 2)
	bolt.Color = color
	bolt.Material = Enum.Material.Neon
	bolt.Anchored = true
	bolt.CanCollide = false
	bolt.Parent = workspace

	-- Quick flash and disappear
	Debris:AddItem(bolt, 0.1)
end

function VfxController:_spawnDebrisChunk(position: Vector3)
	local chunk = Instance.new("Part")
	chunk.Name = "Debris"
	chunk.Size = Vector3.new(math.random(1, 3), math.random(1, 2), math.random(1, 3))
	chunk.Position = position + Vector3.new(math.random(-4, 4), -2, math.random(-4, 4))
	chunk.Color = Color3.fromRGB(100 + math.random(0, 50), 90 + math.random(0, 40), 70 + math.random(0, 30))
	chunk.Material = Enum.Material.Slate
	chunk.Anchored = true
	chunk.CanCollide = false
	chunk.Parent = workspace

	-- Float upward then vanish
	TweenService:Create(chunk, TweenInfo.new(1.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
		Position = chunk.Position + Vector3.new(0, math.random(5, 10), 0),
		Transparency = 1,
	}):Play()

	Debris:AddItem(chunk, 1.6)
end

-- ==========================================
-- COMBAT EFFECTS
-- ==========================================
function VfxController:PlayMeleeEffect(moveId: string)
	local character = localPlayer.Character
	if not character then return end
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	local hitPos = rootPart.Position + rootPart.CFrame.LookVector * 3

	-- Impact flash
	local flash = Instance.new("Part")
	flash.Size = Vector3.new(1, 1, 1)
	flash.Shape = Enum.PartType.Ball
	flash.Material = Enum.Material.Neon
	flash.Color = Color3.new(1, 1, 1)
	flash.Anchored = true
	flash.CanCollide = false
	flash.Transparency = 0.3
	flash.Position = hitPos
	flash.Parent = workspace

	TweenService:Create(flash, TweenInfo.new(0.15, Enum.EasingStyle.Quad), {
		Size = Vector3.new(4, 4, 4),
		Transparency = 1,
	}):Play()
	Debris:AddItem(flash, 0.2)

	-- Shockwave ring
	local ring = Instance.new("Part")
	ring.Size = Vector3.new(0.5, 0.5, 0.5)
	ring.Shape = Enum.PartType.Cylinder
	ring.Material = Enum.Material.Neon
	ring.Color = Color3.fromRGB(255, 255, 200)
	ring.Anchored = true
	ring.CanCollide = false
	ring.Transparency = 0.4
	ring.CFrame = CFrame.new(hitPos) * CFrame.Angles(0, 0, math.rad(90))
	ring.Parent = workspace

	TweenService:Create(ring, TweenInfo.new(0.25), {
		Size = Vector3.new(0.2, 6, 6),
		Transparency = 1,
	}):Play()
	Debris:AddItem(ring, 0.3)

	-- Heavy attack = bigger effects
	if moveId == "HeavyAttack" then
		-- Ground crack
		local crack = Instance.new("Part")
		crack.Size = Vector3.new(8, 0.1, 8)
		crack.Color = Color3.fromRGB(60, 50, 40)
		crack.Material = Enum.Material.Slate
		crack.Anchored = true
		crack.CanCollide = false
		crack.Transparency = 0.3
		crack.Position = hitPos - Vector3.new(0, 2, 0)
		crack.Parent = workspace

		TweenService:Create(crack, TweenInfo.new(1.5), { Transparency = 1 }):Play()
		Debris:AddItem(crack, 2)
	end
end

function VfxController:SpawnKiBlastProjectile(attacker: Player, direction: Vector3, moveId: string, origin: Vector3)
	local isBeam = moveId == "Kamehameha" or moveId == "FinalFlash" or moveId == "GalickGun" or moveId == "Masenko" or moveId == "SpecialBeamCannon"

	if isBeam then
		self:_spawnBeam(origin, direction, moveId)
	else
		self:_spawnBlastProjectile(origin, direction, moveId)
	end
end

function VfxController:_spawnBlastProjectile(origin: Vector3, direction: Vector3, moveId: string)
	local color = Color3.fromRGB(255, 255, 100)
	local size = 2
	local speed = 100

	if moveId == "ChargedKiBlast" then size = 3; speed = 80 end
	if moveId == "DeathBeam" then color = Color3.fromRGB(200, 50, 255); size = 1; speed = 200 end
	if moveId == "VanishingBall" then color = Color3.fromRGB(200, 50, 200); size = 3; speed = 90 end

	local blast = Instance.new("Part")
	blast.Name = "KiBlast"
	blast.Size = Vector3.new(size, size, size)
	blast.Shape = Enum.PartType.Ball
	blast.Material = Enum.Material.Neon
	blast.Color = color
	blast.Anchored = true
	blast.CanCollide = false
	blast.Position = origin
	blast.Parent = workspace

	local trail = Instance.new("ParticleEmitter")
	trail.Color = ColorSequence.new(color)
	trail.Size = NumberSequence.new({ NumberSequenceKeypoint.new(0, size * 0.8), NumberSequenceKeypoint.new(1, 0) })
	trail.Lifetime = NumberRange.new(0.2, 0.4)
	trail.Rate = 80
	trail.Speed = NumberRange.new(0, 1)
	trail.LightEmission = 1
	trail.Parent = blast

	local light = Instance.new("PointLight")
	light.Color = color
	light.Brightness = 3
	light.Range = 15
	light.Parent = blast

	local traveled = 0
	task.spawn(function()
		while traveled < 200 and blast.Parent do
			local dt = task.wait()
			local move = direction * speed * dt
			blast.Position = blast.Position + move
			traveled = traveled + move.Magnitude
		end

		-- Explosion on despawn
		if blast.Parent then
			self:_spawnExplosion(blast.Position, color, size * 2)
		end
		blast:Destroy()
	end)
end

function VfxController:_spawnBeam(origin: Vector3, direction: Vector3, moveId: string)
	local color = Color3.fromRGB(100, 180, 255)
	local width = 3
	local length = 80

	if moveId == "Kamehameha" then color = Color3.fromRGB(100, 180, 255); width = 4; length = 100 end
	if moveId == "FinalFlash" then color = Color3.fromRGB(255, 255, 80); width = 5; length = 100 end
	if moveId == "GalickGun" then color = Color3.fromRGB(200, 80, 255); width = 4; length = 100 end
	if moveId == "Masenko" then color = Color3.fromRGB(255, 200, 80); width = 3; length = 80 end
	if moveId == "SpecialBeamCannon" then color = Color3.fromRGB(255, 255, 100); width = 2; length = 100 end

	-- Beam core
	local beam = Instance.new("Part")
	beam.Name = "BeamCore"
	beam.Size = Vector3.new(width * 0.5, width * 0.5, 0.1)
	beam.Material = Enum.Material.Neon
	beam.Color = Color3.new(1, 1, 1)
	beam.Anchored = true
	beam.CanCollide = false
	beam.CFrame = CFrame.lookAt(origin, origin + direction) * CFrame.new(0, 0, -0.05)
	beam.Parent = workspace

	-- Beam outer glow
	local beamOuter = Instance.new("Part")
	beamOuter.Name = "BeamOuter"
	beamOuter.Size = Vector3.new(width, width, 0.1)
	beamOuter.Material = Enum.Material.Neon
	beamOuter.Color = color
	beamOuter.Transparency = 0.3
	beamOuter.Anchored = true
	beamOuter.CanCollide = false
	beamOuter.CFrame = beam.CFrame
	beamOuter.Parent = workspace

	local light = Instance.new("PointLight")
	light.Color = color
	light.Brightness = 4
	light.Range = 30
	light.Parent = beam

	-- Extend beam over time
	task.spawn(function()
		local elapsed = 0
		local beamDuration = 2.0
		while elapsed < beamDuration and beam.Parent do
			local dt = task.wait()
			elapsed = elapsed + dt
			local currentLength = math.min(length, (elapsed / 0.3) * length)

			beam.Size = Vector3.new(width * 0.5, width * 0.5, currentLength)
			beam.CFrame = CFrame.lookAt(origin, origin + direction) * CFrame.new(0, 0, -currentLength / 2)

			beamOuter.Size = Vector3.new(width, width, currentLength)
			beamOuter.CFrame = beam.CFrame
		end

		-- End explosion
		local endPos = origin + direction * length
		self:_spawnExplosion(endPos, color, width * 3)

		-- Fade out
		TweenService:Create(beam, TweenInfo.new(0.3), { Transparency = 1 }):Play()
		TweenService:Create(beamOuter, TweenInfo.new(0.3), { Transparency = 1 }):Play()

		Debris:AddItem(beam, 0.4)
		Debris:AddItem(beamOuter, 0.4)
	end)
end

function VfxController:_spawnExplosion(position: Vector3, color: Color3, size: number)
	-- Sphere flash
	local sphere = Instance.new("Part")
	sphere.Size = Vector3.new(1, 1, 1)
	sphere.Shape = Enum.PartType.Ball
	sphere.Material = Enum.Material.Neon
	sphere.Color = color
	sphere.Anchored = true
	sphere.CanCollide = false
	sphere.Position = position
	sphere.Parent = workspace

	TweenService:Create(sphere, TweenInfo.new(0.3), {
		Size = Vector3.new(size, size, size),
		Transparency = 1,
	}):Play()
	Debris:AddItem(sphere, 0.4)

	-- Shockwave ring
	local ring = Instance.new("Part")
	ring.Size = Vector3.new(0.3, 1, 1)
	ring.Shape = Enum.PartType.Cylinder
	ring.Material = Enum.Material.Neon
	ring.Color = color
	ring.Anchored = true
	ring.CanCollide = false
	ring.Transparency = 0.3
	ring.CFrame = CFrame.new(position) * CFrame.Angles(0, 0, math.rad(90))
	ring.Parent = workspace

	TweenService:Create(ring, TweenInfo.new(0.4), {
		Size = Vector3.new(0.1, size * 3, size * 3),
		Transparency = 1,
	}):Play()
	Debris:AddItem(ring, 0.5)

	-- Light flash
	local light = Instance.new("PointLight")
	light.Color = color
	light.Brightness = 5
	light.Range = size * 5
	light.Parent = sphere
end

-- ==========================================
-- VANISH / DODGE EFFECTS
-- ==========================================
function VfxController:PlayVanishEffect(player: Player)
	local character = if typeof(player) == "Instance" and player:IsA("Player") then player.Character else (typeof(player) == "Instance" and player:IsA("Model") and player or nil)
	if not character then return end

	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	-- After-image (semi-transparent clone silhouette)
	local afterImage = Instance.new("Part")
	afterImage.Size = Vector3.new(2, 5, 1)
	afterImage.Material = Enum.Material.Neon
	afterImage.Color = Color3.fromRGB(150, 150, 255)
	afterImage.Anchored = true
	afterImage.CanCollide = false
	afterImage.Transparency = 0.3
	afterImage.CFrame = rootPart.CFrame
	afterImage.Parent = workspace

	-- Speed lines
	for i = 1, 5 do
		local line = Instance.new("Part")
		line.Size = Vector3.new(0.1, 0.1, math.random(3, 6))
		line.Material = Enum.Material.Neon
		line.Color = Color3.fromRGB(200, 200, 255)
		line.Anchored = true
		line.CanCollide = false
		line.Transparency = 0.3
		line.CFrame = rootPart.CFrame * CFrame.new(math.random(-2, 2), math.random(-2, 2), 0)
		line.Parent = workspace

		TweenService:Create(line, TweenInfo.new(0.3), { Transparency = 1, Size = Vector3.new(0.05, 0.05, 8) }):Play()
		Debris:AddItem(line, 0.4)
	end

	TweenService:Create(afterImage, TweenInfo.new(0.4), {
		Transparency = 1,
		Size = Vector3.new(3, 6, 2),
	}):Play()
	Debris:AddItem(afterImage, 0.5)
end

-- ==========================================
-- LAUNCH / KNOCKBACK EFFECTS
-- ==========================================
function VfxController:PlayLaunchEffect(target)
	-- Speed lines going upward
	local character = nil
	if typeof(target) == "Instance" then
		if target:IsA("Player") then
			character = target.Character
		elseif target:IsA("Model") then
			character = target
		end
	end
	if not character then return end

	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	for i = 1, 8 do
		local line = Instance.new("Part")
		line.Size = Vector3.new(0.08, math.random(4, 8), 0.08)
		line.Material = Enum.Material.Neon
		line.Color = Color3.new(1, 1, 1)
		line.Anchored = true
		line.CanCollide = false
		line.Transparency = 0.3
		line.Position = rootPart.Position + Vector3.new(math.random(-3, 3), -2, math.random(-3, 3))
		line.Parent = workspace

		TweenService:Create(line, TweenInfo.new(0.4), {
			Position = line.Position + Vector3.new(0, 15, 0),
			Transparency = 1,
		}):Play()
		Debris:AddItem(line, 0.5)
	end
end

-- ==========================================
-- BEAM STRUGGLE
-- ==========================================
function VfxController:PlayBeamStruggleResult(winner: number)
	-- Big explosion at center
	local cam = workspace.CurrentCamera
	if cam then
		local pos = cam.CFrame.Position + cam.CFrame.LookVector * 30
		self:_spawnExplosion(pos, Color3.fromRGB(255, 255, 200), 20)
	end
end

-- ==========================================
-- CHARGE EFFECTS
-- ==========================================
function VfxController:StartChargeEffect()
	local character = localPlayer.Character
	if not character then return end
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	local orb = Instance.new("Part")
	orb.Name = "ChargeOrb"
	orb.Size = Vector3.new(1, 1, 1)
	orb.Shape = Enum.PartType.Ball
	orb.Material = Enum.Material.Neon
	orb.Color = Color3.fromRGB(255, 255, 100)
	orb.Anchored = false
	orb.CanCollide = false
	orb.Transparency = 0.3
	orb.Parent = rootPart

	local weld = Instance.new("WeldConstraint")
	weld.Part0 = rootPart
	weld.Part1 = orb
	weld.Parent = orb

	TweenService:Create(orb, TweenInfo.new(3, Enum.EasingStyle.Linear), {
		Size = Vector3.new(4, 4, 4),
	}):Play()

	-- Swirling particles
	local swirl = Instance.new("ParticleEmitter")
	swirl.Name = "ChargeSwirl"
	swirl.Color = ColorSequence.new(Color3.fromRGB(255, 255, 150))
	swirl.Size = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.5), NumberSequenceKeypoint.new(1, 0) })
	swirl.Lifetime = NumberRange.new(0.3, 0.5)
	swirl.Rate = 40
	swirl.Speed = NumberRange.new(8, 12)
	swirl.SpreadAngle = Vector2.new(180, 180)
	swirl.LightEmission = 1
	swirl.Parent = orb
end

function VfxController:StopChargeEffect()
	local character = localPlayer.Character
	if not character then return end
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	local orb = rootPart:FindFirstChild("ChargeOrb")
	if orb then orb:Destroy() end
end

function VfxController:StartKiChargeEffect()
	local character = localPlayer.Character
	if not character then return end
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	local aura = Instance.new("ParticleEmitter")
	aura.Name = "KiChargeAura"
	aura.Color = ColorSequence.new(Color3.fromRGB(100, 150, 255))
	aura.Size = NumberSequence.new({ NumberSequenceKeypoint.new(0, 4), NumberSequenceKeypoint.new(1, 0) })
	aura.Lifetime = NumberRange.new(0.5, 1.0)
	aura.Rate = 60
	aura.Speed = NumberRange.new(5, 10)
	aura.SpreadAngle = Vector2.new(30, 30)
	aura.LightEmission = 1
	aura.EmissionDirection = Enum.NormalId.Top
	aura.Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.3), NumberSequenceKeypoint.new(1, 1) })
	aura.Parent = rootPart

	-- Wind effect around player
	local wind = Instance.new("ParticleEmitter")
	wind.Name = "KiChargeWind"
	wind.Color = ColorSequence.new(Color3.fromRGB(200, 220, 255))
	wind.Size = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.3), NumberSequenceKeypoint.new(1, 0) })
	wind.Lifetime = NumberRange.new(0.3, 0.5)
	wind.Rate = 30
	wind.Speed = NumberRange.new(10, 20)
	wind.SpreadAngle = Vector2.new(180, 180)
	wind.LightEmission = 0.5
	wind.Parent = rootPart
end

function VfxController:StopKiChargeEffect()
	local character = localPlayer.Character
	if not character then return end
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	if not rootPart then return end

	local aura = rootPart:FindFirstChild("KiChargeAura")
	if aura then aura:Destroy() end
	local wind = rootPart:FindFirstChild("KiChargeWind")
	if wind then wind:Destroy() end
end

return VfxController
]]></string>
            </Properties>
          </Item>
        </Item>
        <Item class="Folder" referent="104">
          <Properties>
            <string name="Name">UI</string>
          </Properties>
          <Item class="Folder" referent="105">
            <Properties>
              <string name="Name">Menus</string>
            </Properties>
            <Item class="ModuleScript" referent="106">
              <Properties>
                <string name="Name">CharacterCreation</string>
                <string name="Source"><![CDATA[--[[
	CharacterCreation.lua
	Full-screen character creation UI.
	Race selection, appearance customization, and confirmation.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Races = require(ReplicatedStorage.Shared.Constants.Races)
local CharacterPreview = require(script.Parent.CharacterPreview)

local CharacterCreation = {}

local localPlayer = Players.LocalPlayer
local playerGui = localPlayer:WaitForChild("PlayerGui")

local selectedRace = nil
local selectedHairStyle = "Spiky"
local selectedHairColor = { R = 30, G = 30, B = 30 }
local selectedSkinColor = { R = 255, G = 200, B = 150 }
local selectedOutfitColor = { R = 100, G = 100, B = 200 }

local HAIR_STYLES = { "Spiky", "Long", "Mohawk", "Bald", "Default" }

local SKIN_PRESETS = {
	{ name = "Light", color = { R = 255, G = 220, B = 185 } },
	{ name = "Medium", color = { R = 230, G = 180, B = 140 } },
	{ name = "Tan", color = { R = 200, G = 150, B = 110 } },
	{ name = "Dark", color = { R = 140, G = 100, B = 70 } },
	{ name = "Green", color = { R = 80, G = 180, B = 80 } },
	{ name = "Purple", color = { R = 180, G = 130, B = 200 } },
	{ name = "Pink", color = { R = 255, G = 150, B = 180 } },
	{ name = "Blue", color = { R = 150, G = 180, B = 240 } },
}

local HAIR_COLOR_PRESETS = {
	{ name = "Black", color = { R = 20, G = 20, B = 20 } },
	{ name = "Brown", color = { R = 100, G = 60, B = 30 } },
	{ name = "Blonde", color = { R = 230, G = 200, B = 100 } },
	{ name = "Red", color = { R = 200, G = 50, B = 30 } },
	{ name = "Blue", color = { R = 50, G = 80, B = 200 } },
	{ name = "White", color = { R = 230, G = 230, B = 230 } },
	{ name = "Purple", color = { R = 150, G = 50, B = 200 } },
	{ name = "Green", color = { R = 50, G = 150, B = 50 } },
}

local OUTFIT_COLOR_PRESETS = {
	{ name = "Blue Gi", color = { R = 50, G = 80, B = 200 } },
	{ name = "Orange Gi", color = { R = 255, G = 150, B = 50 } },
	{ name = "Purple", color = { R = 150, G = 50, B = 200 } },
	{ name = "Red", color = { R = 200, G = 40, B = 40 } },
	{ name = "Green", color = { R = 40, G = 150, B = 50 } },
	{ name = "Black", color = { R = 30, G = 30, B = 30 } },
	{ name = "White", color = { R = 230, G = 230, B = 230 } },
	{ name = "Gold", color = { R = 200, G = 170, B = 50 } },
}

-- Helper to make consistent UI elements
local function makeFrame(props)
	local frame = Instance.new("Frame")
	frame.Size = props.Size or UDim2.fromScale(1, 1)
	frame.Position = props.Position or UDim2.new()
	frame.BackgroundColor3 = props.BackgroundColor3 or Color3.fromRGB(20, 20, 30)
	frame.BackgroundTransparency = props.BackgroundTransparency or 0
	frame.BorderSizePixel = 0
	frame.Name = props.Name or "Frame"
	if props.Parent then frame.Parent = props.Parent end
	if props.CornerRadius then
		Instance.new("UICorner", frame).CornerRadius = props.CornerRadius
	end
	return frame
end

local function makeButton(props)
	local btn = Instance.new("TextButton")
	btn.Size = props.Size or UDim2.new(0, 150, 0, 40)
	btn.Position = props.Position or UDim2.new()
	btn.BackgroundColor3 = props.BackgroundColor3 or Color3.fromRGB(60, 60, 100)
	btn.Text = props.Text or "Button"
	btn.TextColor3 = props.TextColor3 or Color3.new(1, 1, 1)
	btn.TextSize = props.TextSize or 16
	btn.Font = Enum.Font.GothamBold
	btn.BorderSizePixel = 0
	btn.Name = props.Name or "Button"
	btn.AutoButtonColor = true
	if props.Parent then btn.Parent = props.Parent end
	Instance.new("UICorner", btn).CornerRadius = UDim.new(0, 6)
	return btn
end

local function makeLabel(props)
	local label = Instance.new("TextLabel")
	label.Size = props.Size or UDim2.new(1, 0, 0, 30)
	label.Position = props.Position or UDim2.new()
	label.BackgroundTransparency = 1
	label.Text = props.Text or ""
	label.TextColor3 = props.TextColor3 or Color3.new(1, 1, 1)
	label.TextSize = props.TextSize or 18
	label.Font = props.Font or Enum.Font.GothamBold
	label.TextXAlignment = props.TextXAlignment or Enum.TextXAlignment.Center
	label.Name = props.Name or "Label"
	if props.Parent then label.Parent = props.Parent end
	return label
end

function CharacterCreation.Show()
	-- Main screen gui
	local gui = Instance.new("ScreenGui")
	gui.Name = "CharacterCreation"
	gui.ResetOnSpawn = false
	gui.IgnoreGuiInset = true
	gui.DisplayOrder = 100
	gui.Parent = playerGui

	-- Background
	local bg = makeFrame({
		Name = "Background",
		Size = UDim2.fromScale(1, 1),
		BackgroundColor3 = Color3.fromRGB(10, 10, 20),
		Parent = gui,
	})

	-- Gradient
	local gradient = Instance.new("UIGradient")
	gradient.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(10, 10, 30)),
		ColorSequenceKeypoint.new(0.5, Color3.fromRGB(20, 15, 40)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(10, 10, 30)),
	})
	gradient.Rotation = 45
	gradient.Parent = bg

	-- Title
	makeLabel({
		Name = "Title",
		Text = "CREATE YOUR WARRIOR",
		TextSize = 36,
		Size = UDim2.new(1, 0, 0, 60),
		Position = UDim2.new(0, 0, 0.02, 0),
		TextColor3 = Color3.fromRGB(255, 215, 0),
		Parent = bg,
	})

	-- Subtitle
	makeLabel({
		Name = "Subtitle",
		Text = "Choose your race and customize your appearance",
		TextSize = 16,
		Size = UDim2.new(1, 0, 0, 25),
		Position = UDim2.new(0, 0, 0.08, 0),
		TextColor3 = Color3.fromRGB(180, 180, 200),
		Font = Enum.Font.Gotham,
		Parent = bg,
	})

	-- ==================
	-- Left panel: Race selection
	-- ==================
	local racePanel = makeFrame({
		Name = "RacePanel",
		Size = UDim2.new(0.3, 0, 0.7, 0),
		Position = UDim2.new(0.02, 0, 0.13, 0),
		BackgroundColor3 = Color3.fromRGB(25, 25, 40),
		CornerRadius = UDim.new(0, 10),
		Parent = bg,
	})

	makeLabel({
		Text = "SELECT RACE",
		TextSize = 20,
		Size = UDim2.new(1, 0, 0, 35),
		Position = UDim2.new(0, 0, 0, 5),
		TextColor3 = Color3.fromRGB(255, 215, 0),
		Parent = racePanel,
	})

	local raceList = Instance.new("ScrollingFrame")
	raceList.Name = "RaceList"
	raceList.Size = UDim2.new(0.9, 0, 0.85, 0)
	raceList.Position = UDim2.new(0.05, 0, 0.1, 0)
	raceList.BackgroundTransparency = 1
	raceList.ScrollBarThickness = 4
	raceList.CanvasSize = UDim2.new(0, 0, 0, #Races.Names * 120)
	raceList.Parent = racePanel

	local raceLayout = Instance.new("UIListLayout")
	raceLayout.Padding = UDim.new(0, 8)
	raceLayout.Parent = raceList

	local raceButtons = {}
	local raceInfoLabel = nil

	for _, raceName in ipairs(Races.Names) do
		local raceData = Races:Get(raceName)

		local raceBtn = makeFrame({
			Name = raceName,
			Size = UDim2.new(1, 0, 0, 100),
			BackgroundColor3 = Color3.fromRGB(40, 40, 60),
			CornerRadius = UDim.new(0, 8),
			Parent = raceList,
		})

		local raceTitleLabel = makeLabel({
			Text = raceData.displayName,
			TextSize = 18,
			Size = UDim2.new(1, -10, 0, 25),
			Position = UDim2.new(0, 10, 0, 5),
			TextXAlignment = Enum.TextXAlignment.Left,
			Parent = raceBtn,
		})

		local raceDesc = makeLabel({
			Text = raceData.description,
			TextSize = 12,
			Size = UDim2.new(1, -20, 0, 35),
			Position = UDim2.new(0, 10, 0, 28),
			TextXAlignment = Enum.TextXAlignment.Left,
			TextColor3 = Color3.fromRGB(180, 180, 200),
			Font = Enum.Font.Gotham,
			Parent = raceBtn,
		})
		raceDesc.TextWrapped = true

		local passiveLabel = makeLabel({
			Text = "Passive: " .. raceData.passive.name .. " - " .. raceData.passive.description,
			TextSize = 11,
			Size = UDim2.new(1, -20, 0, 30),
			Position = UDim2.new(0, 10, 0, 65),
			TextXAlignment = Enum.TextXAlignment.Left,
			TextColor3 = Color3.fromRGB(100, 200, 100),
			Font = Enum.Font.Gotham,
			Parent = raceBtn,
		})
		passiveLabel.TextWrapped = true

		-- Click handler
		local clickBtn = Instance.new("TextButton")
		clickBtn.Size = UDim2.fromScale(1, 1)
		clickBtn.BackgroundTransparency = 1
		clickBtn.Text = ""
		clickBtn.Parent = raceBtn

		table.insert(raceButtons, { button = raceBtn, name = raceName })

		clickBtn.MouseButton1Click:Connect(function()
			selectedRace = raceName
			-- Highlight selected
			for _, rb in ipairs(raceButtons) do
				if rb.name == raceName then
					rb.button.BackgroundColor3 = Color3.fromRGB(80, 60, 150)
				else
					rb.button.BackgroundColor3 = Color3.fromRGB(40, 40, 60)
				end
			end

			-- Auto-set skin color based on race
			local raceConfig = Races:Get(raceName)
			if raceName == "Namekian" then
				selectedSkinColor = { R = 80, G = 180, B = 80 }
			elseif raceName == "FriezaRace" then
				selectedSkinColor = { R = 220, G = 200, B = 230 }
			elseif raceName == "Majin" then
				selectedSkinColor = { R = 255, G = 150, B = 180 }
			elseif raceName == "Android" then
				selectedSkinColor = { R = 240, G = 220, B = 200 }
			else
				selectedSkinColor = { R = 255, G = 200, B = 150 }
			end
			refreshPreview()
		end)
	end

	-- ==================
	-- Middle panel: Appearance
	-- ==================
	local appearPanel = makeFrame({
		Name = "AppearancePanel",
		Size = UDim2.new(0.33, 0, 0.7, 0),
		Position = UDim2.new(0.335, 0, 0.13, 0),
		BackgroundColor3 = Color3.fromRGB(25, 25, 40),
		CornerRadius = UDim.new(0, 10),
		Parent = bg,
	})

	makeLabel({
		Text = "CUSTOMIZE",
		TextSize = 20,
		Size = UDim2.new(1, 0, 0, 35),
		Position = UDim2.new(0, 0, 0, 5),
		TextColor3 = Color3.fromRGB(255, 215, 0),
		Parent = appearPanel,
	})

	local customScroll = Instance.new("ScrollingFrame")
	customScroll.Size = UDim2.new(0.9, 0, 0.85, 0)
	customScroll.Position = UDim2.new(0.05, 0, 0.1, 0)
	customScroll.BackgroundTransparency = 1
	customScroll.ScrollBarThickness = 4
	customScroll.CanvasSize = UDim2.new(0, 0, 0, 600)
	customScroll.Parent = appearPanel

	local customLayout = Instance.new("UIListLayout")
	customLayout.Padding = UDim.new(0, 10)
	customLayout.Parent = customScroll

	-- Hair Style
	local hairSection = makeFrame({
		Name = "HairSection",
		Size = UDim2.new(1, 0, 0, 80),
		BackgroundTransparency = 1,
		Parent = customScroll,
	})
	makeLabel({ Text = "Hair Style", TextSize = 14, Size = UDim2.new(1, 0, 0, 20), Parent = hairSection, TextXAlignment = Enum.TextXAlignment.Left })

	local hairFlow = Instance.new("Frame")
	hairFlow.Size = UDim2.new(1, 0, 0, 50)
	hairFlow.Position = UDim2.new(0, 0, 0, 25)
	hairFlow.BackgroundTransparency = 1
	hairFlow.Parent = hairSection

	local hairFlowLayout = Instance.new("UIListLayout")
	hairFlowLayout.FillDirection = Enum.FillDirection.Horizontal
	hairFlowLayout.Padding = UDim.new(0, 5)
	hairFlowLayout.Parent = hairFlow

	local hairButtons = {}
	for _, style in ipairs(HAIR_STYLES) do
		local btn = makeButton({
			Text = style,
			Size = UDim2.new(0, 70, 0, 35),
			BackgroundColor3 = Color3.fromRGB(50, 50, 80),
			TextSize = 12,
			Parent = hairFlow,
		})
		table.insert(hairButtons, { button = btn, style = style })
		btn.MouseButton1Click:Connect(function()
			selectedHairStyle = style
			for _, hb in ipairs(hairButtons) do
				hb.button.BackgroundColor3 = hb.style == style and Color3.fromRGB(80, 60, 150) or Color3.fromRGB(50, 50, 80)
			end
			refreshPreview()
		end)
	end

	-- Hair Color
	local hairColorSection = makeFrame({
		Name = "HairColorSection",
		Size = UDim2.new(1, 0, 0, 80),
		BackgroundTransparency = 1,
		Parent = customScroll,
	})
	makeLabel({ Text = "Hair Color", TextSize = 14, Size = UDim2.new(1, 0, 0, 20), Parent = hairColorSection, TextXAlignment = Enum.TextXAlignment.Left })

	local hcFlow = Instance.new("Frame")
	hcFlow.Size = UDim2.new(1, 0, 0, 50)
	hcFlow.Position = UDim2.new(0, 0, 0, 25)
	hcFlow.BackgroundTransparency = 1
	hcFlow.Parent = hairColorSection

	local hcLayout = Instance.new("UIListLayout")
	hcLayout.FillDirection = Enum.FillDirection.Horizontal
	hcLayout.Padding = UDim.new(0, 5)
	hcLayout.Parent = hcFlow

	for _, preset in ipairs(HAIR_COLOR_PRESETS) do
		local swatch = Instance.new("TextButton")
		swatch.Size = UDim2.new(0, 35, 0, 35)
		swatch.BackgroundColor3 = Color3.fromRGB(preset.color.R, preset.color.G, preset.color.B)
		swatch.Text = ""
		swatch.BorderSizePixel = 0
		swatch.Parent = hcFlow
		Instance.new("UICorner", swatch).CornerRadius = UDim.new(0.5, 0)

		swatch.MouseButton1Click:Connect(function()
			selectedHairColor = preset.color
			-- Selection ring
			for _, s in ipairs(hcFlow:GetChildren()) do
				if s:IsA("TextButton") then
					local ring = s:FindFirstChild("SelectRing")
					if ring then ring:Destroy() end
				end
			end
			local ring = Instance.new("UIStroke")
			ring.Name = "SelectRing"
			ring.Color = Color3.new(1, 1, 1)
			ring.Thickness = 3
			ring.Parent = swatch
			refreshPreview()
		end)
	end

	-- Skin Color
	local skinSection = makeFrame({
		Name = "SkinSection",
		Size = UDim2.new(1, 0, 0, 80),
		BackgroundTransparency = 1,
		Parent = customScroll,
	})
	makeLabel({ Text = "Skin Color", TextSize = 14, Size = UDim2.new(1, 0, 0, 20), Parent = skinSection, TextXAlignment = Enum.TextXAlignment.Left })

	local scFlow = Instance.new("Frame")
	scFlow.Size = UDim2.new(1, 0, 0, 50)
	scFlow.Position = UDim2.new(0, 0, 0, 25)
	scFlow.BackgroundTransparency = 1
	scFlow.Parent = skinSection

	local scLayout = Instance.new("UIListLayout")
	scLayout.FillDirection = Enum.FillDirection.Horizontal
	scLayout.Padding = UDim.new(0, 5)
	scLayout.Parent = scFlow

	for _, preset in ipairs(SKIN_PRESETS) do
		local swatch = Instance.new("TextButton")
		swatch.Size = UDim2.new(0, 35, 0, 35)
		swatch.BackgroundColor3 = Color3.fromRGB(preset.color.R, preset.color.G, preset.color.B)
		swatch.Text = ""
		swatch.BorderSizePixel = 0
		swatch.Parent = scFlow
		Instance.new("UICorner", swatch).CornerRadius = UDim.new(0.5, 0)

		swatch.MouseButton1Click:Connect(function()
			selectedSkinColor = preset.color
			for _, s in ipairs(scFlow:GetChildren()) do
				if s:IsA("TextButton") then
					local ring = s:FindFirstChild("SelectRing")
					if ring then ring:Destroy() end
				end
			end
			local ring = Instance.new("UIStroke")
			ring.Name = "SelectRing"
			ring.Color = Color3.new(1, 1, 1)
			ring.Thickness = 3
			ring.Parent = swatch
			refreshPreview()
		end)
	end

	-- Outfit Color
	local outfitSection = makeFrame({
		Name = "OutfitSection",
		Size = UDim2.new(1, 0, 0, 80),
		BackgroundTransparency = 1,
		Parent = customScroll,
	})
	makeLabel({ Text = "Outfit Color", TextSize = 14, Size = UDim2.new(1, 0, 0, 20), Parent = outfitSection, TextXAlignment = Enum.TextXAlignment.Left })

	local ocFlow = Instance.new("Frame")
	ocFlow.Size = UDim2.new(1, 0, 0, 50)
	ocFlow.Position = UDim2.new(0, 0, 0, 25)
	ocFlow.BackgroundTransparency = 1
	ocFlow.Parent = outfitSection

	local ocLayout = Instance.new("UIListLayout")
	ocLayout.FillDirection = Enum.FillDirection.Horizontal
	ocLayout.Padding = UDim.new(0, 5)
	ocLayout.Parent = ocFlow

	for _, preset in ipairs(OUTFIT_COLOR_PRESETS) do
		local swatch = Instance.new("TextButton")
		swatch.Size = UDim2.new(0, 35, 0, 35)
		swatch.BackgroundColor3 = Color3.fromRGB(preset.color.R, preset.color.G, preset.color.B)
		swatch.Text = ""
		swatch.BorderSizePixel = 0
		swatch.Parent = ocFlow
		Instance.new("UICorner", swatch).CornerRadius = UDim.new(0.5, 0)

		swatch.MouseButton1Click:Connect(function()
			selectedOutfitColor = preset.color
			for _, s in ipairs(ocFlow:GetChildren()) do
				if s:IsA("TextButton") then
					local ring = s:FindFirstChild("SelectRing")
					if ring then ring:Destroy() end
				end
			end
			local ring = Instance.new("UIStroke")
			ring.Name = "SelectRing"
			ring.Color = Color3.new(1, 1, 1)
			ring.Thickness = 3
			ring.Parent = swatch
			refreshPreview()
		end)
	end

	-- ==================
	-- Right panel: 3D Preview + Stats + Confirm
	-- ==================
	local previewPanel = makeFrame({
		Name = "PreviewPanel",
		Size = UDim2.new(0.3, 0, 0.7, 0),
		Position = UDim2.new(0.68, 0, 0.13, 0),
		BackgroundColor3 = Color3.fromRGB(25, 25, 40),
		CornerRadius = UDim.new(0, 10),
		Parent = bg,
	})

	makeLabel({
		Text = "PREVIEW",
		TextSize = 20,
		Size = UDim2.new(1, 0, 0, 35),
		Position = UDim2.new(0, 0, 0, 5),
		TextColor3 = Color3.fromRGB(255, 215, 0),
		Parent = previewPanel,
	})

	-- 3D Character preview viewport
	local previewContainer = makeFrame({
		Name = "PreviewContainer",
		Size = UDim2.new(0.9, 0, 0.42, 0),
		Position = UDim2.new(0.05, 0, 0.07, 0),
		BackgroundColor3 = Color3.fromRGB(15, 15, 25),
		CornerRadius = UDim.new(0, 8),
		Parent = previewPanel,
	})

	local previewViewport = CharacterPreview.Create(previewContainer)

	-- "Drag to rotate" hint
	makeLabel({
		Text = "Character rotates automatically",
		TextSize = 10,
		Size = UDim2.new(1, 0, 0, 15),
		Position = UDim2.new(0, 0, 1, -15),
		TextColor3 = Color3.fromRGB(120, 120, 150),
		Font = Enum.Font.Gotham,
		Parent = previewContainer,
	})

	-- Current selection labels
	local selectionInfo = makeLabel({
		Name = "SelectionInfo",
		Text = "Select a race to begin",
		TextSize = 12,
		Size = UDim2.new(0.9, 0, 0, 20),
		Position = UDim2.new(0.05, 0, 0.5, 0),
		TextColor3 = Color3.fromRGB(200, 200, 220),
		Font = Enum.Font.Gotham,
		Parent = previewPanel,
	})

	-- Compact stats
	local statsLabel = makeLabel({
		Name = "StatsPreview",
		Text = "",
		TextSize = 12,
		Size = UDim2.new(0.9, 0, 0.18, 0),
		Position = UDim2.new(0.05, 0, 0.53, 0),
		TextColor3 = Color3.fromRGB(200, 200, 220),
		Font = Enum.Font.Gotham,
		TextXAlignment = Enum.TextXAlignment.Left,
		TextYAlignment = Enum.TextYAlignment.Top,
		Parent = previewPanel,
	})
	statsLabel.TextWrapped = true

	-- Transformation list (compact)
	local transLabel = makeLabel({
		Name = "TransPreview",
		Text = "",
		TextSize = 11,
		Size = UDim2.new(0.9, 0, 0.13, 0),
		Position = UDim2.new(0.05, 0, 0.71, 0),
		TextColor3 = Color3.fromRGB(150, 200, 255),
		Font = Enum.Font.Gotham,
		TextXAlignment = Enum.TextXAlignment.Left,
		TextYAlignment = Enum.TextYAlignment.Top,
		Parent = previewPanel,
	})
	transLabel.TextWrapped = true

	-- Error label
	local errorLabel = makeLabel({
		Name = "ErrorLabel",
		Text = "",
		TextSize = 14,
		Size = UDim2.new(0.8, 0, 0, 25),
		Position = UDim2.new(0.1, 0, 0.84, 0),
		TextColor3 = Color3.fromRGB(255, 80, 80),
		Parent = previewPanel,
	})

	-- Confirm button
	local confirmBtn = makeButton({
		Name = "ConfirmButton",
		Text = "CREATE WARRIOR",
		Size = UDim2.new(0.8, 0, 0, 50),
		Position = UDim2.new(0.1, 0, 0.89, 0),
		BackgroundColor3 = Color3.fromRGB(50, 180, 50),
		TextSize = 20,
		Parent = previewPanel,
	})

	-- refreshPreview function — updates the 3D model + labels
	function refreshPreview()
		if not selectedRace then return end

		CharacterPreview.Update(selectedRace, selectedSkinColor, selectedHairStyle, selectedHairColor, selectedOutfitColor)

		local raceData = Races:Get(selectedRace)
		if raceData then
			selectionInfo.Text = raceData.displayName .. " | " .. selectedHairStyle .. " Hair"

			local statsText = ""
			for stat, value in pairs(raceData.baseStats) do
				statsText = statsText .. stat .. ": " .. value .. "  "
			end
			statsLabel.Text = statsText

			local forms = {}
			for _, formId in ipairs(raceData.transformationTree) do
				table.insert(forms, formId)
			end
			transLabel.Text = "Forms: " .. table.concat(forms, " > ")
		end
	end

	-- Update stats preview when race changes (polling fallback)
	task.spawn(function()
		while gui.Parent do
			if selectedRace then
				refreshPreview()
			end
			task.wait(0.5)
		end
		-- Cleanup preview on close
		CharacterPreview.Destroy()
	end)

	-- Confirm handler
	confirmBtn.MouseButton1Click:Connect(function()
		if not selectedRace then
			errorLabel.Text = "Please select a race!"
			return
		end

		errorLabel.Text = "Creating..."
		confirmBtn.BackgroundColor3 = Color3.fromRGB(100, 100, 100)

		local DataService = Knit.GetService("DataService")
		local success, message = DataService:SubmitCharacterCreation(selectedRace, {
			SkinColor = selectedSkinColor,
			HairStyle = selectedHairStyle,
			HairColor = selectedHairColor,
			Outfit = "Default",
			OutfitColor = selectedOutfitColor,
		})

		if success then
			-- Fade out and destroy
			TweenService:Create(bg, TweenInfo.new(0.5), {
				BackgroundTransparency = 1,
			}):Play()
			for _, desc in ipairs(gui:GetDescendants()) do
				if desc:IsA("GuiObject") then
					TweenService:Create(desc, TweenInfo.new(0.5), {
						BackgroundTransparency = 1,
					}):Play()
					if desc:IsA("TextLabel") or desc:IsA("TextButton") then
						TweenService:Create(desc, TweenInfo.new(0.5), {
							TextTransparency = 1,
						}):Play()
					end
				end
			end
			task.delay(0.6, function()
				gui:Destroy()
			end)
		else
			errorLabel.Text = message or "Failed to create character!"
			confirmBtn.BackgroundColor3 = Color3.fromRGB(50, 180, 50)
		end
	end)

	-- Animate in
	bg.BackgroundTransparency = 1
	TweenService:Create(bg, TweenInfo.new(0.5), { BackgroundTransparency = 0 }):Play()
end

function CharacterCreation.Hide()
	local gui = playerGui:FindFirstChild("CharacterCreation")
	if gui then
		gui:Destroy()
	end
end

return CharacterCreation
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="107">
              <Properties>
                <string name="Name">CharacterPreview</string>
                <string name="Source"><![CDATA[--[[
	CharacterPreview.lua
	Live 3D character preview for the character creation screen.
	Shows a rotating model that updates in real-time as the player changes options.
]]

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

local Races = require(ReplicatedStorage.Shared.Constants.Races)

local CharacterPreview = {}

local currentViewport = nil
local currentModel = nil
local currentCamera = nil
local rotationAngle = 0
local renderConn = nil

-- Race body configs (matches CharacterBuilder)
local RACE_CONFIGS = {
	Saiyan = { skinDefault = Color3.fromRGB(255, 200, 150), hasTail = true, tailColor = Color3.fromRGB(150, 100, 50) },
	Namekian = { skinDefault = Color3.fromRGB(80, 180, 80), hasAntennae = true },
	FriezaRace = { skinDefault = Color3.fromRGB(220, 200, 230), hasTail = true, hasHorns = true },
	Majin = { skinDefault = Color3.fromRGB(255, 150, 180), hasHeadTentacle = true },
	Android = { skinDefault = Color3.fromRGB(240, 220, 200) },
	Human = { skinDefault = Color3.fromRGB(255, 200, 150) },
}

local HAIR_DEFS = {
	Spiky = {
		{ size = Vector3.new(1.2, 0.5, 1.2), offset = CFrame.new(0, 0.8, 0) },
		{ size = Vector3.new(0.4, 0.8, 0.3), offset = CFrame.new(0.3, 1.1, 0) },
		{ size = Vector3.new(0.4, 0.9, 0.3), offset = CFrame.new(-0.3, 1.15, 0) },
		{ size = Vector3.new(0.4, 0.7, 0.3), offset = CFrame.new(0, 1.2, 0.2) },
	},
	Long = {
		{ size = Vector3.new(1.3, 0.5, 1.3), offset = CFrame.new(0, 0.8, 0) },
		{ size = Vector3.new(1.0, 1.5, 0.4), offset = CFrame.new(0, 0, -0.6) },
	},
	Mohawk = {
		{ size = Vector3.new(0.4, 1.0, 1.2), offset = CFrame.new(0, 1.0, 0) },
	},
	Bald = {},
	Default = {
		{ size = Vector3.new(1.2, 0.6, 1.2), offset = CFrame.new(0, 0.8, 0) },
	},
}

local function makePart(size, color, material, parent)
	local p = Instance.new("Part")
	p.Size = size
	p.Color = color
	p.Material = material or Enum.Material.SmoothPlastic
	p.Anchored = true
	p.CanCollide = false
	p.Parent = parent
	return p
end

function CharacterPreview.Create(parent: GuiObject): ViewportFrame
	CharacterPreview.Destroy()

	local viewport = Instance.new("ViewportFrame")
	viewport.Size = UDim2.fromScale(1, 1)
	viewport.BackgroundColor3 = Color3.fromRGB(15, 15, 25)
	viewport.BackgroundTransparency = 0
	viewport.BorderSizePixel = 0
	viewport.Parent = parent
	Instance.new("UICorner", viewport).CornerRadius = UDim.new(0, 10)

	local cam = Instance.new("Camera")
	cam.Parent = viewport
	viewport.CurrentCamera = cam

	-- Lighting
	local worldModel = Instance.new("WorldModel")
	worldModel.Parent = viewport

	currentViewport = viewport
	currentCamera = cam
	currentModel = nil
	rotationAngle = 0

	-- Spin the model
	renderConn = RunService.RenderStepped:Connect(function(dt)
		rotationAngle = rotationAngle + dt * 0.5
		if currentModel and currentCamera then
			local center = Vector3.new(0, 3, 0)
			local dist = 8
			currentCamera.CFrame = CFrame.new(
				center + Vector3.new(math.sin(rotationAngle) * dist, 1.5, math.cos(rotationAngle) * dist),
				center
			)
		end
	end)

	return viewport
end

function CharacterPreview.Update(raceName: string, skinColor: { R: number, G: number, B: number }, hairStyle: string, hairColor: { R: number, G: number, B: number }, outfitColor: { R: number, G: number, B: number })
	if not currentViewport then return end

	local worldModel = currentViewport:FindFirstChildOfClass("WorldModel")
	if not worldModel then return end

	-- Clear previous model
	for _, child in ipairs(worldModel:GetChildren()) do
		child:Destroy()
	end

	local raceConfig = RACE_CONFIGS[raceName]
	if not raceConfig then return end

	local skin = Color3.fromRGB(skinColor.R, skinColor.G, skinColor.B)
	local hair = Color3.fromRGB(hairColor.R, hairColor.G, hairColor.B)
	local outfit = Color3.fromRGB(outfitColor.R, outfitColor.G, outfitColor.B)

	local model = Instance.new("Model")
	model.Name = "Preview"
	model.Parent = worldModel

	-- Torso
	local torso = makePart(Vector3.new(2, 2, 1), outfit, Enum.Material.SmoothPlastic, model)
	torso.CFrame = CFrame.new(0, 3, 0)
	model.PrimaryPart = torso

	-- Gi overlay (slightly larger)
	local gi = makePart(Vector3.new(2.05, 2.05, 1.05), outfit, Enum.Material.Fabric, model)
	gi.CFrame = torso.CFrame

	-- Belt
	local belt = makePart(Vector3.new(2.1, 0.3, 1.1), Color3.fromRGB(40, 40, 40), Enum.Material.Fabric, model)
	belt.CFrame = torso.CFrame * CFrame.new(0, -0.85, 0)

	-- Head
	local head = makePart(Vector3.new(1.2, 1.2, 1.2), skin, Enum.Material.SmoothPlastic, model)
	head.Shape = Enum.PartType.Ball
	head.CFrame = torso.CFrame * CFrame.new(0, 1.5, 0)

	-- Eyes
	for _, xOff in ipairs({ -0.2, 0.2 }) do
		local eye = makePart(Vector3.new(0.15, 0.15, 0.05), Color3.new(1, 1, 1), Enum.Material.Neon, model)
		eye.Shape = Enum.PartType.Ball
		eye.CFrame = head.CFrame * CFrame.new(xOff, 0.1, 0.55)

		local pupil = makePart(Vector3.new(0.08, 0.08, 0.06), Color3.new(0, 0, 0), Enum.Material.SmoothPlastic, model)
		pupil.Shape = Enum.PartType.Ball
		pupil.CFrame = head.CFrame * CFrame.new(xOff, 0.1, 0.58)
	end

	-- Mouth
	local mouth = makePart(Vector3.new(0.25, 0.05, 0.05), Color3.fromRGB(50, 30, 30), Enum.Material.SmoothPlastic, model)
	mouth.CFrame = head.CFrame * CFrame.new(0, -0.2, 0.58)

	-- Arms
	for _, side in ipairs({ -1, 1 }) do
		local arm = makePart(Vector3.new(0.6, 2, 0.6), skin, Enum.Material.SmoothPlastic, model)
		arm.CFrame = torso.CFrame * CFrame.new(side * 1.3, 0, 0)

		-- Wristband
		local band = makePart(Vector3.new(0.65, 0.3, 0.65), Color3.fromRGB(40, 40, 80), Enum.Material.Fabric, model)
		band.CFrame = arm.CFrame * CFrame.new(0, -0.85, 0)
	end

	-- Legs
	for _, side in ipairs({ -1, 1 }) do
		local leg = makePart(Vector3.new(0.7, 2, 0.7), outfit, Enum.Material.SmoothPlastic, model)
		leg.CFrame = torso.CFrame * CFrame.new(side * 0.5, -2, 0)

		-- Boot
		local boot = makePart(Vector3.new(0.75, 0.6, 0.9), Color3.fromRGB(40, 40, 80), Enum.Material.Fabric, model)
		boot.CFrame = leg.CFrame * CFrame.new(0, -0.7, 0.05)
	end

	-- Hair
	local hairDef = HAIR_DEFS[hairStyle] or HAIR_DEFS["Spiky"]
	for _, partDef in ipairs(hairDef) do
		local hp = makePart(partDef.size, hair, Enum.Material.SmoothPlastic, model)
		hp.CFrame = head.CFrame * partDef.offset
	end

	-- Race features
	if raceConfig.hasTail then
		local tail = makePart(Vector3.new(0.25, 0.25, 1.5), raceConfig.tailColor or skin, Enum.Material.SmoothPlastic, model)
		tail.CFrame = torso.CFrame * CFrame.new(0, -0.5, -0.8)
	end

	if raceConfig.hasAntennae then
		for _, xOff in ipairs({ -0.2, 0.2 }) do
			local ant = makePart(Vector3.new(0.08, 0.7, 0.08), skin, Enum.Material.SmoothPlastic, model)
			ant.CFrame = head.CFrame * CFrame.new(xOff, 0.7, 0.15) * CFrame.Angles(math.rad(-25), 0, 0)
		end
	end

	if raceConfig.hasHorns then
		for _, xOff in ipairs({ -0.35, 0.35 }) do
			local horn = makePart(Vector3.new(0.12, 0.5, 0.12), Color3.fromRGB(220, 220, 240), Enum.Material.SmoothPlastic, model)
			horn.CFrame = head.CFrame * CFrame.new(xOff, 0.6, 0) * CFrame.Angles(0, 0, xOff > 0 and math.rad(15) or math.rad(-15))
		end
	end

	if raceConfig.hasHeadTentacle then
		local tent = makePart(Vector3.new(0.25, 0.25, 1.3), skin, Enum.Material.SmoothPlastic, model)
		tent.CFrame = head.CFrame * CFrame.new(0, 0.4, -0.4) * CFrame.Angles(math.rad(-55), 0, 0)
	end

	-- Floor disc
	local floor = makePart(Vector3.new(4, 0.1, 4), Color3.fromRGB(40, 40, 60), Enum.Material.SmoothPlastic, model)
	floor.Shape = Enum.PartType.Cylinder
	floor.CFrame = CFrame.new(0, 0.95, 0) * CFrame.Angles(0, 0, math.rad(90))

	currentModel = model
end

function CharacterPreview.Destroy()
	if renderConn then
		renderConn:Disconnect()
		renderConn = nil
	end
	if currentViewport then
		currentViewport:Destroy()
		currentViewport = nil
	end
	currentModel = nil
	currentCamera = nil
end

return CharacterPreview
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="108">
              <Properties>
                <string name="Name">MoveLoadout</string>
                <string name="Source"><![CDATA[--[[
	MoveLoadout.lua
	UI for configuring the player's 6-slot move loadout.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Moves = require(ReplicatedStorage.Shared.Constants.Moves)

local MoveLoadout = {}

local localPlayer = Players.LocalPlayer
local playerGui = localPlayer:WaitForChild("PlayerGui")
local currentGui = nil

local SLOT_LABELS = { "[1] Skill 1", "[2] Skill 2", "[3] Skill 3", "[4] Skill 4", "[5] Ultimate", "[6] Evasive" }

function MoveLoadout.Show(playerData: any)
	MoveLoadout.Hide()

	if not playerData then return end

	local gui = Instance.new("ScreenGui")
	gui.Name = "MoveLoadout"
	gui.ResetOnSpawn = false
	gui.DisplayOrder = 40
	gui.Parent = playerGui
	currentGui = gui

	local frame = Instance.new("Frame")
	frame.Size = UDim2.new(0.5, 0, 0.6, 0)
	frame.Position = UDim2.new(0.25, 0, 0.2, 0)
	frame.BackgroundColor3 = Color3.fromRGB(20, 20, 35)
	frame.BorderSizePixel = 0
	frame.Parent = gui
	Instance.new("UICorner", frame).CornerRadius = UDim.new(0, 12)

	local title = Instance.new("TextLabel")
	title.Size = UDim2.new(1, 0, 0, 50)
	title.BackgroundTransparency = 1
	title.Text = "MOVE LOADOUT"
	title.TextColor3 = Color3.fromRGB(255, 215, 0)
	title.TextSize = 28
	title.Font = Enum.Font.GothamBold
	title.Parent = frame

	local closeBtn = Instance.new("TextButton")
	closeBtn.Size = UDim2.new(0, 40, 0, 40)
	closeBtn.Position = UDim2.new(1, -45, 0, 5)
	closeBtn.BackgroundColor3 = Color3.fromRGB(200, 50, 50)
	closeBtn.Text = "X"
	closeBtn.TextColor3 = Color3.new(1, 1, 1)
	closeBtn.TextSize = 18
	closeBtn.Font = Enum.Font.GothamBold
	closeBtn.BorderSizePixel = 0
	closeBtn.Parent = frame
	Instance.new("UICorner", closeBtn).CornerRadius = UDim.new(0, 6)
	closeBtn.MouseButton1Click:Connect(function()
		MoveLoadout.Hide()
	end)

	-- Slots
	local slotsFrame = Instance.new("Frame")
	slotsFrame.Size = UDim2.new(0.95, 0, 0.35, 0)
	slotsFrame.Position = UDim2.new(0.025, 0, 0.12, 0)
	slotsFrame.BackgroundTransparency = 1
	slotsFrame.Parent = frame

	local slotLayout = Instance.new("UIGridLayout")
	slotLayout.CellSize = UDim2.new(0, 120, 0, 80)
	slotLayout.CellPadding = UDim2.new(0, 10, 0, 10)
	slotLayout.Parent = slotsFrame

	local loadout = playerData.MoveLoadout or {}

	for i = 1, 6 do
		local slotFrame = Instance.new("Frame")
		slotFrame.Name = "Slot_" .. i
		slotFrame.BackgroundColor3 = Color3.fromRGB(40, 40, 65)
		slotFrame.BorderSizePixel = 0
		slotFrame.Parent = slotsFrame
		Instance.new("UICorner", slotFrame).CornerRadius = UDim.new(0, 8)

		local slotLabel = Instance.new("TextLabel")
		slotLabel.Size = UDim2.new(1, 0, 0, 20)
		slotLabel.BackgroundTransparency = 1
		slotLabel.Text = SLOT_LABELS[i]
		slotLabel.TextColor3 = Color3.fromRGB(150, 150, 180)
		slotLabel.TextSize = 11
		slotLabel.Font = Enum.Font.Gotham
		slotLabel.Parent = slotFrame

		local moveId = loadout[i]
		local moveName = "Empty"
		if moveId then
			local move = Moves:Get(moveId)
			if move then
				moveName = move.displayName
			end
		end

		local moveLabel = Instance.new("TextLabel")
		moveLabel.Size = UDim2.new(1, 0, 0, 30)
		moveLabel.Position = UDim2.new(0, 0, 0, 22)
		moveLabel.BackgroundTransparency = 1
		moveLabel.Text = moveName
		moveLabel.TextColor3 = moveId and Color3.new(1, 1, 1) or Color3.fromRGB(100, 100, 100)
		moveLabel.TextSize = 13
		moveLabel.Font = Enum.Font.GothamBold
		moveLabel.Parent = slotFrame
	end

	-- Available moves list
	local availLabel = Instance.new("TextLabel")
	availLabel.Size = UDim2.new(1, 0, 0, 25)
	availLabel.Position = UDim2.new(0, 0, 0.5, 0)
	availLabel.BackgroundTransparency = 1
	availLabel.Text = "Unlocked Moves (drag to slots above)"
	availLabel.TextColor3 = Color3.fromRGB(200, 200, 220)
	availLabel.TextSize = 14
	availLabel.Font = Enum.Font.GothamBold
	availLabel.Parent = frame

	local moveScroll = Instance.new("ScrollingFrame")
	moveScroll.Size = UDim2.new(0.95, 0, 0.38, 0)
	moveScroll.Position = UDim2.new(0.025, 0, 0.57, 0)
	moveScroll.BackgroundColor3 = Color3.fromRGB(30, 30, 45)
	moveScroll.BorderSizePixel = 0
	moveScroll.ScrollBarThickness = 4
	moveScroll.Parent = frame
	Instance.new("UICorner", moveScroll).CornerRadius = UDim.new(0, 8)

	local moveLayout = Instance.new("UIGridLayout")
	moveLayout.CellSize = UDim2.new(0, 150, 0, 40)
	moveLayout.CellPadding = UDim2.new(0, 5, 0, 5)
	moveLayout.Parent = moveScroll

	if playerData.UnlockedMoves then
		for _, moveId in ipairs(playerData.UnlockedMoves) do
			local move = Moves:Get(moveId)
			if not move then continue end

			local moveBtn = Instance.new("TextButton")
			moveBtn.BackgroundColor3 = Color3.fromRGB(50, 50, 80)
			moveBtn.Text = move.displayName
			moveBtn.TextColor3 = Color3.new(1, 1, 1)
			moveBtn.TextSize = 12
			moveBtn.Font = Enum.Font.GothamBold
			moveBtn.BorderSizePixel = 0
			moveBtn.Parent = moveScroll
			Instance.new("UICorner", moveBtn).CornerRadius = UDim.new(0, 4)
		end

		local totalMoves = #playerData.UnlockedMoves
		local rows = math.ceil(totalMoves / 3)
		moveScroll.CanvasSize = UDim2.new(0, 0, 0, rows * 45)
	end
end

function MoveLoadout.Hide()
	if currentGui then
		currentGui:Destroy()
		currentGui = nil
	end
end

return MoveLoadout
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="109">
              <Properties>
                <string name="Name">QuestLog</string>
                <string name="Source"><![CDATA[--[[
	QuestLog.lua
	Quest log UI showing active quests, available sagas, and quest details.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Sagas = require(ReplicatedStorage.Shared.Constants.Sagas)

local QuestLog = {}

local localPlayer = Players.LocalPlayer
local playerGui = localPlayer:WaitForChild("PlayerGui")
local currentGui = nil

function QuestLog.Show(playerData: any)
	QuestLog.Hide()

	local gui = Instance.new("ScreenGui")
	gui.Name = "QuestLog"
	gui.ResetOnSpawn = false
	gui.DisplayOrder = 40
	gui.Parent = playerGui
	currentGui = gui

	-- Main frame
	local frame = Instance.new("Frame")
	frame.Size = UDim2.new(0.6, 0, 0.7, 0)
	frame.Position = UDim2.new(0.2, 0, 0.15, 0)
	frame.BackgroundColor3 = Color3.fromRGB(20, 20, 35)
	frame.BorderSizePixel = 0
	frame.Parent = gui
	Instance.new("UICorner", frame).CornerRadius = UDim.new(0, 12)

	-- Title
	local title = Instance.new("TextLabel")
	title.Size = UDim2.new(1, 0, 0, 50)
	title.BackgroundTransparency = 1
	title.Text = "QUEST LOG"
	title.TextColor3 = Color3.fromRGB(255, 215, 0)
	title.TextSize = 28
	title.Font = Enum.Font.GothamBold
	title.Parent = frame

	-- Close button
	local closeBtn = Instance.new("TextButton")
	closeBtn.Size = UDim2.new(0, 40, 0, 40)
	closeBtn.Position = UDim2.new(1, -45, 0, 5)
	closeBtn.BackgroundColor3 = Color3.fromRGB(200, 50, 50)
	closeBtn.Text = "X"
	closeBtn.TextColor3 = Color3.new(1, 1, 1)
	closeBtn.TextSize = 18
	closeBtn.Font = Enum.Font.GothamBold
	closeBtn.BorderSizePixel = 0
	closeBtn.Parent = frame
	Instance.new("UICorner", closeBtn).CornerRadius = UDim.new(0, 6)
	closeBtn.MouseButton1Click:Connect(function()
		QuestLog.Hide()
	end)

	-- Two panels: Active Quests | Available Sagas
	local leftPanel = Instance.new("ScrollingFrame")
	leftPanel.Size = UDim2.new(0.48, 0, 0.82, 0)
	leftPanel.Position = UDim2.new(0.01, 0, 0.12, 0)
	leftPanel.BackgroundColor3 = Color3.fromRGB(30, 30, 45)
	leftPanel.BorderSizePixel = 0
	leftPanel.ScrollBarThickness = 4
	leftPanel.Parent = frame
	Instance.new("UICorner", leftPanel).CornerRadius = UDim.new(0, 8)

	local leftTitle = Instance.new("TextLabel")
	leftTitle.Size = UDim2.new(1, 0, 0, 30)
	leftTitle.BackgroundTransparency = 1
	leftTitle.Text = "Active Quests"
	leftTitle.TextColor3 = Color3.fromRGB(100, 200, 255)
	leftTitle.TextSize = 16
	leftTitle.Font = Enum.Font.GothamBold
	leftTitle.Parent = leftPanel

	local leftLayout = Instance.new("UIListLayout")
	leftLayout.Padding = UDim.new(0, 5)
	leftLayout.Parent = leftPanel

	-- Spacer for title
	local spacer = Instance.new("Frame")
	spacer.Size = UDim2.new(1, 0, 0, 30)
	spacer.BackgroundTransparency = 1
	spacer.LayoutOrder = -1
	spacer.Parent = leftPanel

	-- Show active quests
	if playerData and playerData.QuestProgress then
		for questId, progress in pairs(playerData.QuestProgress) do
			local quest, saga = Sagas:GetQuestById(questId)
			if not quest then continue end

			local questFrame = Instance.new("Frame")
			questFrame.Size = UDim2.new(0.95, 0, 0, 80)
			questFrame.BackgroundColor3 = Color3.fromRGB(40, 40, 60)
			questFrame.BorderSizePixel = 0
			questFrame.Parent = leftPanel
			Instance.new("UICorner", questFrame).CornerRadius = UDim.new(0, 6)

			local qTitle = Instance.new("TextLabel")
			qTitle.Size = UDim2.new(1, -10, 0, 22)
			qTitle.Position = UDim2.new(0, 5, 0, 3)
			qTitle.BackgroundTransparency = 1
			qTitle.Text = quest.title
			qTitle.TextColor3 = Color3.new(1, 1, 1)
			qTitle.TextSize = 14
			qTitle.Font = Enum.Font.GothamBold
			qTitle.TextXAlignment = Enum.TextXAlignment.Left
			qTitle.Parent = questFrame

			-- Objectives
			local objY = 25
			for _, obj in ipairs(progress.objectives) do
				local objLabel = Instance.new("TextLabel")
				objLabel.Size = UDim2.new(1, -15, 0, 18)
				objLabel.Position = UDim2.new(0, 10, 0, objY)
				objLabel.BackgroundTransparency = 1
				objLabel.Text = (obj.completed and "✓ " or "○ ") .. obj.type .. " " .. (obj.npcId or "") .. ": " .. obj.current .. "/" .. obj.target
				objLabel.TextColor3 = obj.completed and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(200, 200, 200)
				objLabel.TextSize = 11
				objLabel.Font = Enum.Font.Gotham
				objLabel.TextXAlignment = Enum.TextXAlignment.Left
				objLabel.Parent = questFrame
				objY = objY + 18
			end
		end
	end

	-- Right panel: Available sagas
	local rightPanel = Instance.new("ScrollingFrame")
	rightPanel.Size = UDim2.new(0.48, 0, 0.82, 0)
	rightPanel.Position = UDim2.new(0.51, 0, 0.12, 0)
	rightPanel.BackgroundColor3 = Color3.fromRGB(30, 30, 45)
	rightPanel.BorderSizePixel = 0
	rightPanel.ScrollBarThickness = 4
	rightPanel.CanvasSize = UDim2.new(0, 0, 0, #Sagas.List * 110)
	rightPanel.Parent = frame
	Instance.new("UICorner", rightPanel).CornerRadius = UDim.new(0, 8)

	local rightTitle = Instance.new("TextLabel")
	rightTitle.Size = UDim2.new(1, 0, 0, 30)
	rightTitle.BackgroundTransparency = 1
	rightTitle.Text = "Story Sagas"
	rightTitle.TextColor3 = Color3.fromRGB(255, 200, 100)
	rightTitle.TextSize = 16
	rightTitle.Font = Enum.Font.GothamBold
	rightTitle.Parent = rightPanel

	local rightLayout = Instance.new("UIListLayout")
	rightLayout.Padding = UDim.new(0, 5)
	rightLayout.Parent = rightPanel

	local spacer2 = Instance.new("Frame")
	spacer2.Size = UDim2.new(1, 0, 0, 30)
	spacer2.BackgroundTransparency = 1
	spacer2.LayoutOrder = -1
	spacer2.Parent = rightPanel

	for _, saga in ipairs(Sagas.List) do
		local isAvailable = playerData and playerData.Level >= saga.levelRequirement
		local isCompleted = false
		if playerData and playerData.CompletedSagas then
			for _, id in ipairs(playerData.CompletedSagas) do
				if id == saga.id then
					isCompleted = true
					break
				end
			end
		end

		local sagaFrame = Instance.new("Frame")
		sagaFrame.Size = UDim2.new(0.95, 0, 0, 90)
		sagaFrame.BackgroundColor3 = isCompleted and Color3.fromRGB(30, 60, 30) or (isAvailable and Color3.fromRGB(40, 40, 60) or Color3.fromRGB(30, 30, 40))
		sagaFrame.BorderSizePixel = 0
		sagaFrame.Parent = rightPanel
		Instance.new("UICorner", sagaFrame).CornerRadius = UDim.new(0, 6)

		local sTitle = Instance.new("TextLabel")
		sTitle.Size = UDim2.new(1, -10, 0, 22)
		sTitle.Position = UDim2.new(0, 5, 0, 3)
		sTitle.BackgroundTransparency = 1
		sTitle.Text = saga.displayName .. (isCompleted and " [COMPLETE]" or "") .. " (Lv." .. saga.levelRequirement .. "+)"
		sTitle.TextColor3 = isCompleted and Color3.fromRGB(100, 255, 100) or (isAvailable and Color3.new(1, 1, 1) or Color3.fromRGB(120, 120, 120))
		sTitle.TextSize = 14
		sTitle.Font = Enum.Font.GothamBold
		sTitle.TextXAlignment = Enum.TextXAlignment.Left
		sTitle.Parent = sagaFrame

		local sDesc = Instance.new("TextLabel")
		sDesc.Size = UDim2.new(1, -15, 0, 35)
		sDesc.Position = UDim2.new(0, 10, 0, 25)
		sDesc.BackgroundTransparency = 1
		sDesc.Text = saga.description
		sDesc.TextColor3 = Color3.fromRGB(180, 180, 200)
		sDesc.TextSize = 11
		sDesc.Font = Enum.Font.Gotham
		sDesc.TextXAlignment = Enum.TextXAlignment.Left
		sDesc.TextWrapped = true
		sDesc.Parent = sagaFrame

		-- Accept first quest button
		if isAvailable and not isCompleted and #saga.quests > 0 then
			local acceptBtn = Instance.new("TextButton")
			acceptBtn.Size = UDim2.new(0, 80, 0, 25)
			acceptBtn.Position = UDim2.new(1, -90, 1, -30)
			acceptBtn.BackgroundColor3 = Color3.fromRGB(50, 150, 50)
			acceptBtn.Text = "START"
			acceptBtn.TextColor3 = Color3.new(1, 1, 1)
			acceptBtn.TextSize = 12
			acceptBtn.Font = Enum.Font.GothamBold
			acceptBtn.BorderSizePixel = 0
			acceptBtn.Parent = sagaFrame
			Instance.new("UICorner", acceptBtn).CornerRadius = UDim.new(0, 4)

			acceptBtn.MouseButton1Click:Connect(function()
				local QuestController = Knit.GetController("QuestController")
				QuestController:AcceptQuest(saga.quests[1].id)
				acceptBtn.Text = "Accepted!"
				acceptBtn.BackgroundColor3 = Color3.fromRGB(100, 100, 100)
			end)
		end
	end
end

function QuestLog.Hide()
	if currentGui then
		currentGui:Destroy()
		currentGui = nil
	end
end

return QuestLog
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="110">
              <Properties>
                <string name="Name">Settings</string>
                <string name="Source"><![CDATA[--[[
	Settings.lua
	In-game settings menu for audio, graphics, and controls.
]]

local Players = game:GetService("Players")

local Settings = {}

local localPlayer = Players.LocalPlayer
local playerGui = localPlayer:WaitForChild("PlayerGui")
local currentGui = nil

function Settings.Show()
	Settings.Hide()

	local gui = Instance.new("ScreenGui")
	gui.Name = "SettingsMenu"
	gui.ResetOnSpawn = false
	gui.DisplayOrder = 45
	gui.Parent = playerGui
	currentGui = gui

	local frame = Instance.new("Frame")
	frame.Size = UDim2.new(0.4, 0, 0.5, 0)
	frame.Position = UDim2.new(0.3, 0, 0.25, 0)
	frame.BackgroundColor3 = Color3.fromRGB(20, 20, 35)
	frame.BorderSizePixel = 0
	frame.Parent = gui
	Instance.new("UICorner", frame).CornerRadius = UDim.new(0, 12)

	local title = Instance.new("TextLabel")
	title.Size = UDim2.new(1, 0, 0, 50)
	title.BackgroundTransparency = 1
	title.Text = "SETTINGS"
	title.TextColor3 = Color3.fromRGB(255, 215, 0)
	title.TextSize = 28
	title.Font = Enum.Font.GothamBold
	title.Parent = frame

	local closeBtn = Instance.new("TextButton")
	closeBtn.Size = UDim2.new(0, 40, 0, 40)
	closeBtn.Position = UDim2.new(1, -45, 0, 5)
	closeBtn.BackgroundColor3 = Color3.fromRGB(200, 50, 50)
	closeBtn.Text = "X"
	closeBtn.TextColor3 = Color3.new(1, 1, 1)
	closeBtn.TextSize = 18
	closeBtn.Font = Enum.Font.GothamBold
	closeBtn.BorderSizePixel = 0
	closeBtn.Parent = frame
	Instance.new("UICorner", closeBtn).CornerRadius = UDim.new(0, 6)
	closeBtn.MouseButton1Click:Connect(function()
		Settings.Hide()
	end)

	local layout = Instance.new("UIListLayout")
	layout.Padding = UDim.new(0, 8)
	layout.Parent = frame

	-- Spacer for title
	local spacer = Instance.new("Frame")
	spacer.Size = UDim2.new(1, 0, 0, 55)
	spacer.BackgroundTransparency = 1
	spacer.Parent = frame

	-- Music volume
	local musicFrame = Instance.new("Frame")
	musicFrame.Size = UDim2.new(0.9, 0, 0, 40)
	musicFrame.BackgroundTransparency = 1
	musicFrame.Parent = frame

	local musicLabel = Instance.new("TextLabel")
	musicLabel.Size = UDim2.new(0.4, 0, 1, 0)
	musicLabel.BackgroundTransparency = 1
	musicLabel.Text = "Music Volume"
	musicLabel.TextColor3 = Color3.new(1, 1, 1)
	musicLabel.TextSize = 14
	musicLabel.Font = Enum.Font.Gotham
	musicLabel.TextXAlignment = Enum.TextXAlignment.Left
	musicLabel.Parent = musicFrame

	-- SFX volume
	local sfxFrame = musicFrame:Clone()
	sfxFrame.Parent = frame
	sfxFrame:FindFirstChildOfClass("TextLabel").Text = "SFX Volume"

	-- Screen Shake toggle
	local shakeFrame = Instance.new("Frame")
	shakeFrame.Size = UDim2.new(0.9, 0, 0, 40)
	shakeFrame.BackgroundTransparency = 1
	shakeFrame.Parent = frame

	local shakeLabel = Instance.new("TextLabel")
	shakeLabel.Size = UDim2.new(0.6, 0, 1, 0)
	shakeLabel.BackgroundTransparency = 1
	shakeLabel.Text = "Screen Shake"
	shakeLabel.TextColor3 = Color3.new(1, 1, 1)
	shakeLabel.TextSize = 14
	shakeLabel.Font = Enum.Font.Gotham
	shakeLabel.TextXAlignment = Enum.TextXAlignment.Left
	shakeLabel.Parent = shakeFrame

	local shakeToggle = Instance.new("TextButton")
	shakeToggle.Size = UDim2.new(0, 60, 0, 30)
	shakeToggle.Position = UDim2.new(0.7, 0, 0.1, 0)
	shakeToggle.BackgroundColor3 = Color3.fromRGB(50, 200, 50)
	shakeToggle.Text = "ON"
	shakeToggle.TextColor3 = Color3.new(1, 1, 1)
	shakeToggle.TextSize = 14
	shakeToggle.Font = Enum.Font.GothamBold
	shakeToggle.BorderSizePixel = 0
	shakeToggle.Parent = shakeFrame
	Instance.new("UICorner", shakeToggle).CornerRadius = UDim.new(0, 6)

	local shakeEnabled = true
	shakeToggle.MouseButton1Click:Connect(function()
		shakeEnabled = not shakeEnabled
		shakeToggle.Text = shakeEnabled and "ON" or "OFF"
		shakeToggle.BackgroundColor3 = shakeEnabled and Color3.fromRGB(50, 200, 50) or Color3.fromRGB(200, 50, 50)
	end)

	-- Controls reference
	local controlsLabel = Instance.new("TextLabel")
	controlsLabel.Size = UDim2.new(0.9, 0, 0, 120)
	controlsLabel.BackgroundTransparency = 1
	controlsLabel.Text = "Controls:\nM1 - Light Attack | M2 - Heavy Attack\nE - Ki Blast | R - Charged Blast\nF - Block | Q - Vanish | C - Charge Ki\n1-4 - Skills | T - Transform\nTab - Lock On"
	controlsLabel.TextColor3 = Color3.fromRGB(180, 180, 200)
	controlsLabel.TextSize = 12
	controlsLabel.Font = Enum.Font.Gotham
	controlsLabel.TextXAlignment = Enum.TextXAlignment.Left
	controlsLabel.TextWrapped = true
	controlsLabel.Parent = frame
end

function Settings.Hide()
	if currentGui then
		currentGui:Destroy()
		currentGui = nil
	end
end

return Settings
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="111">
              <Properties>
                <string name="Name">Shop</string>
                <string name="Source"><![CDATA[--[[
	Shop.lua
	In-game shop for purchasing moves and items with Zeni.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Moves = require(ReplicatedStorage.Shared.Constants.Moves)

local Shop = {}

local localPlayer = Players.LocalPlayer
local playerGui = localPlayer:WaitForChild("PlayerGui")
local currentGui = nil

function Shop.Show(playerData: any)
	Shop.Hide()

	local gui = Instance.new("ScreenGui")
	gui.Name = "Shop"
	gui.ResetOnSpawn = false
	gui.DisplayOrder = 40
	gui.Parent = playerGui
	currentGui = gui

	local frame = Instance.new("Frame")
	frame.Size = UDim2.new(0.6, 0, 0.7, 0)
	frame.Position = UDim2.new(0.2, 0, 0.15, 0)
	frame.BackgroundColor3 = Color3.fromRGB(20, 20, 35)
	frame.BorderSizePixel = 0
	frame.Parent = gui
	Instance.new("UICorner", frame).CornerRadius = UDim.new(0, 12)

	-- Title
	local title = Instance.new("TextLabel")
	title.Size = UDim2.new(0.7, 0, 0, 50)
	title.BackgroundTransparency = 1
	title.Text = "MOVE SHOP"
	title.TextColor3 = Color3.fromRGB(255, 215, 0)
	title.TextSize = 28
	title.Font = Enum.Font.GothamBold
	title.Parent = frame

	-- Zeni display
	local zeniLabel = Instance.new("TextLabel")
	zeniLabel.Size = UDim2.new(0.3, 0, 0, 50)
	zeniLabel.Position = UDim2.new(0.68, 0, 0, 0)
	zeniLabel.BackgroundTransparency = 1
	zeniLabel.Text = (playerData and tostring(playerData.Zeni) or "0") .. " Zeni"
	zeniLabel.TextColor3 = Color3.fromRGB(255, 215, 0)
	zeniLabel.TextSize = 18
	zeniLabel.Font = Enum.Font.GothamBold
	zeniLabel.TextXAlignment = Enum.TextXAlignment.Right
	zeniLabel.Parent = frame

	-- Close button
	local closeBtn = Instance.new("TextButton")
	closeBtn.Size = UDim2.new(0, 40, 0, 40)
	closeBtn.Position = UDim2.new(1, -45, 0, 5)
	closeBtn.BackgroundColor3 = Color3.fromRGB(200, 50, 50)
	closeBtn.Text = "X"
	closeBtn.TextColor3 = Color3.new(1, 1, 1)
	closeBtn.TextSize = 18
	closeBtn.Font = Enum.Font.GothamBold
	closeBtn.BorderSizePixel = 0
	closeBtn.Parent = frame
	Instance.new("UICorner", closeBtn).CornerRadius = UDim.new(0, 6)
	closeBtn.MouseButton1Click:Connect(function()
		Shop.Hide()
	end)

	-- Move list
	local scroll = Instance.new("ScrollingFrame")
	scroll.Size = UDim2.new(0.96, 0, 0.82, 0)
	scroll.Position = UDim2.new(0.02, 0, 0.12, 0)
	scroll.BackgroundTransparency = 1
	scroll.ScrollBarThickness = 4
	scroll.Parent = frame

	local layout = Instance.new("UIGridLayout")
	layout.CellSize = UDim2.new(0, 250, 0, 130)
	layout.CellPadding = UDim2.new(0, 10, 0, 10)
	layout.Parent = scroll

	-- Get available moves for race
	local raceMoves = {}
	if playerData then
		raceMoves = Moves:GetMovesForRace(playerData.Race)
	end

	local ownedMoves = {}
	if playerData and playerData.UnlockedMoves then
		for _, id in ipairs(playerData.UnlockedMoves) do
			ownedMoves[id] = true
		end
	end

	for _, moveId in ipairs(raceMoves) do
		local move = Moves:Get(moveId)
		if not move or move.unlockDefault then continue end
		if not move.zeniCost then continue end

		local owned = ownedMoves[moveId]
		local canAfford = playerData and playerData.Zeni >= move.zeniCost
		local meetsLevel = playerData and playerData.Level >= (move.levelRequirement or 0)

		local card = Instance.new("Frame")
		card.BackgroundColor3 = owned and Color3.fromRGB(30, 60, 30) or Color3.fromRGB(35, 35, 55)
		card.BorderSizePixel = 0
		card.Parent = scroll
		Instance.new("UICorner", card).CornerRadius = UDim.new(0, 8)

		-- Move name
		local nameLabel = Instance.new("TextLabel")
		nameLabel.Size = UDim2.new(1, -10, 0, 22)
		nameLabel.Position = UDim2.new(0, 5, 0, 5)
		nameLabel.BackgroundTransparency = 1
		nameLabel.Text = move.displayName .. " [" .. move.category .. "]"
		nameLabel.TextColor3 = owned and Color3.fromRGB(100, 255, 100) or Color3.new(1, 1, 1)
		nameLabel.TextSize = 14
		nameLabel.Font = Enum.Font.GothamBold
		nameLabel.TextXAlignment = Enum.TextXAlignment.Left
		nameLabel.Parent = card

		-- Description
		local descLabel = Instance.new("TextLabel")
		descLabel.Size = UDim2.new(1, -15, 0, 35)
		descLabel.Position = UDim2.new(0, 10, 0, 28)
		descLabel.BackgroundTransparency = 1
		descLabel.Text = move.description
		descLabel.TextColor3 = Color3.fromRGB(180, 180, 200)
		descLabel.TextSize = 11
		descLabel.Font = Enum.Font.Gotham
		descLabel.TextXAlignment = Enum.TextXAlignment.Left
		descLabel.TextWrapped = true
		descLabel.Parent = card

		-- Stats line
		local statsText = "Ki: " .. (move.kiCost or 0) .. " | CD: " .. (move.cooldown or 0) .. "s | Lv." .. (move.levelRequirement or 0)
		local statsLabel = Instance.new("TextLabel")
		statsLabel.Size = UDim2.new(1, -10, 0, 18)
		statsLabel.Position = UDim2.new(0, 5, 0, 68)
		statsLabel.BackgroundTransparency = 1
		statsLabel.Text = statsText
		statsLabel.TextColor3 = Color3.fromRGB(150, 180, 255)
		statsLabel.TextSize = 11
		statsLabel.Font = Enum.Font.Gotham
		statsLabel.TextXAlignment = Enum.TextXAlignment.Left
		statsLabel.Parent = card

		-- Buy/Owned button
		if owned then
			local ownedLabel = Instance.new("TextLabel")
			ownedLabel.Size = UDim2.new(1, -10, 0, 25)
			ownedLabel.Position = UDim2.new(0, 5, 1, -30)
			ownedLabel.BackgroundTransparency = 1
			ownedLabel.Text = "OWNED"
			ownedLabel.TextColor3 = Color3.fromRGB(100, 255, 100)
			ownedLabel.TextSize = 14
			ownedLabel.Font = Enum.Font.GothamBold
			ownedLabel.Parent = card
		else
			local buyBtn = Instance.new("TextButton")
			buyBtn.Size = UDim2.new(0.5, 0, 0, 25)
			buyBtn.Position = UDim2.new(0.25, 0, 1, -30)
			buyBtn.Text = move.zeniCost .. " Zeni"
			buyBtn.TextSize = 12
			buyBtn.Font = Enum.Font.GothamBold
			buyBtn.TextColor3 = Color3.new(1, 1, 1)
			buyBtn.BorderSizePixel = 0
			buyBtn.Parent = card
			Instance.new("UICorner", buyBtn).CornerRadius = UDim.new(0, 4)

			if canAfford and meetsLevel then
				buyBtn.BackgroundColor3 = Color3.fromRGB(50, 150, 50)
				buyBtn.MouseButton1Click:Connect(function()
					-- TODO: Call PurchaseItem on server
					buyBtn.Text = "Purchased!"
					buyBtn.BackgroundColor3 = Color3.fromRGB(100, 100, 100)
				end)
			else
				buyBtn.BackgroundColor3 = Color3.fromRGB(100, 50, 50)
				if not meetsLevel then
					buyBtn.Text = "Lv." .. (move.levelRequirement or 0) .. " Required"
				end
			end
		end
	end

	-- Auto-size canvas
	local totalItems = 0
	for _ in pairs(scroll:GetChildren()) do totalItems = totalItems + 1 end
	local rows = math.ceil((totalItems - 1) / 3) -- subtract UIGridLayout
	scroll.CanvasSize = UDim2.new(0, 0, 0, rows * 140)
end

function Shop.Hide()
	if currentGui then
		currentGui:Destroy()
		currentGui = nil
	end
end

return Shop
]]></string>
              </Properties>
            </Item>
            <Item class="ModuleScript" referent="112">
              <Properties>
                <string name="Name">TransformationWheel</string>
                <string name="Source"><![CDATA[--[[
	TransformationWheel.lua
	Radial UI for selecting transformations.
]]

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

local Knit = require(ReplicatedStorage.Packages.Knit)
local Races = require(ReplicatedStorage.Shared.Constants.Races)
local Transformations = require(ReplicatedStorage.Shared.Constants.Transformations)

local TransformationWheel = {}

local localPlayer = Players.LocalPlayer
local playerGui = localPlayer:WaitForChild("PlayerGui")
local currentGui = nil

function TransformationWheel.Show(unlockedForms: { string }, currentForm: string, raceName: string)
	TransformationWheel.Hide()

	local gui = Instance.new("ScreenGui")
	gui.Name = "TransformationWheel"
	gui.ResetOnSpawn = false
	gui.DisplayOrder = 50
	gui.Parent = playerGui
	currentGui = gui

	-- Semi-transparent background
	local bg = Instance.new("Frame")
	bg.Size = UDim2.fromScale(1, 1)
	bg.BackgroundColor3 = Color3.new(0, 0, 0)
	bg.BackgroundTransparency = 0.7
	bg.BorderSizePixel = 0
	bg.Parent = gui

	-- Click background to close
	local closeTrigger = Instance.new("TextButton")
	closeTrigger.Size = UDim2.fromScale(1, 1)
	closeTrigger.BackgroundTransparency = 1
	closeTrigger.Text = ""
	closeTrigger.Parent = bg
	closeTrigger.MouseButton1Click:Connect(function()
		TransformationWheel.Hide()
	end)

	-- Center label
	local centerLabel = Instance.new("TextLabel")
	centerLabel.Size = UDim2.new(0, 150, 0, 40)
	centerLabel.Position = UDim2.new(0.5, -75, 0.5, -20)
	centerLabel.BackgroundTransparency = 1
	centerLabel.Text = currentForm == "Base" and "BASE FORM" or currentForm
	centerLabel.TextColor3 = Color3.fromRGB(255, 215, 0)
	centerLabel.TextSize = 20
	centerLabel.Font = Enum.Font.GothamBold
	centerLabel.Parent = gui

	-- Revert button in center
	local revertBtn = Instance.new("TextButton")
	revertBtn.Size = UDim2.new(0, 100, 0, 30)
	revertBtn.Position = UDim2.new(0.5, -50, 0.5, 25)
	revertBtn.BackgroundColor3 = Color3.fromRGB(180, 50, 50)
	revertBtn.Text = "REVERT"
	revertBtn.TextColor3 = Color3.new(1, 1, 1)
	revertBtn.TextSize = 14
	revertBtn.Font = Enum.Font.GothamBold
	revertBtn.BorderSizePixel = 0
	revertBtn.Parent = gui
	Instance.new("UICorner", revertBtn).CornerRadius = UDim.new(0, 6)

	revertBtn.MouseButton1Click:Connect(function()
		local TransformationController = Knit.GetController("TransformationController")
		TransformationController:RequestRevert()
		TransformationWheel.Hide()
	end)

	-- Get race transformation tree
	local race = Races:Get(raceName)
	if not race then return end

	local tree = race.transformationTree
	local formCount = #tree
	local radius = 150

	-- Create form buttons in a circle
	for i, formId in ipairs(tree) do
		local angle = (i - 1) * (2 * math.pi / formCount) - math.pi / 2
		local x = math.cos(angle) * radius
		local y = math.sin(angle) * radius

		local form = Transformations:Get(formId)
		if not form then continue end

		local isUnlocked = false
		for _, unlocked in ipairs(unlockedForms) do
			if unlocked == formId then
				isUnlocked = true
				break
			end
		end

		local isCurrent = currentForm == formId

		local btn = Instance.new("TextButton")
		btn.Size = UDim2.new(0, 100, 0, 50)
		btn.Position = UDim2.new(0.5, x - 50, 0.5, y - 25)
		btn.Text = form.displayName
		btn.TextSize = 12
		btn.Font = Enum.Font.GothamBold
		btn.TextColor3 = Color3.new(1, 1, 1)
		btn.BorderSizePixel = 0
		btn.Parent = gui
		Instance.new("UICorner", btn).CornerRadius = UDim.new(0, 8)

		if isCurrent then
			btn.BackgroundColor3 = Color3.fromRGB(50, 200, 50)
		elseif isUnlocked then
			btn.BackgroundColor3 = Color3.fromRGB(60, 60, 150)
		else
			btn.BackgroundColor3 = Color3.fromRGB(60, 60, 60)
			btn.TextColor3 = Color3.fromRGB(120, 120, 120)
			btn.Text = form.displayName .. "\n(Locked)"
			btn.TextSize = 10
		end

		-- Ki drain label
		local kiLabel = Instance.new("TextLabel")
		kiLabel.Size = UDim2.new(1, 0, 0, 15)
		kiLabel.Position = UDim2.new(0, 0, 1, 2)
		kiLabel.BackgroundTransparency = 1
		kiLabel.Text = "Ki: " .. form.kiDrainPerSecond .. "/s"
		kiLabel.TextColor3 = Color3.fromRGB(100, 180, 255)
		kiLabel.TextSize = 10
		kiLabel.Font = Enum.Font.Gotham
		kiLabel.Parent = btn

		if isUnlocked and not isCurrent then
			btn.MouseButton1Click:Connect(function()
				local TransformationController = Knit.GetController("TransformationController")
				TransformationController:RequestTransform(formId)
				TransformationWheel.Hide()
			end)
		end

		-- Animate in
		btn.Size = UDim2.new(0, 0, 0, 0)
		btn.Position = UDim2.new(0.5, 0, 0.5, 0)
		TweenService:Create(btn, TweenInfo.new(0.3, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, i * 0.05), {
			Size = UDim2.new(0, 100, 0, 50),
			Position = UDim2.new(0.5, x - 50, 0.5, y - 25),
		}):Play()
	end

	-- Instructions
	local instrLabel = Instance.new("TextLabel")
	instrLabel.Size = UDim2.new(0, 300, 0, 25)
	instrLabel.Position = UDim2.new(0.5, -150, 0.85, 0)
	instrLabel.BackgroundTransparency = 1
	instrLabel.Text = "Press T or click outside to close"
	instrLabel.TextColor3 = Color3.fromRGB(150, 150, 170)
	instrLabel.TextSize = 14
	instrLabel.Font = Enum.Font.Gotham
	instrLabel.Parent = gui
end

function TransformationWheel.Hide()
	if currentGui then
		currentGui:Destroy()
		currentGui = nil
	end
end

function TransformationWheel.IsOpen(): boolean
	return currentGui ~= nil
end

return TransformationWheel
]]></string>
              </Properties>
            </Item>
          </Item>
        </Item>
      </Item>
    </Item>
  </Item>
</roblox>