import { setInterval, setTimeout } from "@rbxts/set-timeout";
import { store } from "server/store";
import { CANDY_LIMITS } from "server/world/constants";
import { getCandy, getRandomPointNearWorldOrigin, getSnake } from "server/world/utils";
import { getRandomAccent } from "shared/constants/palette";
import { getSnakeSkinForTracer } from "shared/constants/skins";
import { CandyEntity, CandyType, selectCandyById, selectCandyCount } from "shared/store/candy";
import { describeSnakeFromScore, selectSnakeIsBoosting } from "shared/store/snakes";
import { Grid } from "shared/utils/grid";
import { fillArray } from "shared/utils/object-utils";

const random = new Random();

export const candyGrid = new Grid<{ id: string }>(5);

let nextCandyId = 0;

export function createCandy(patch?: Partial<CandyEntity>): CandyEntity {
	const random = new Random();

	const candy: CandyEntity = {
		id: `${nextCandyId++}`,
		type: CandyType.Default,
		size: math.min(random.NextInteger(1, 4), random.NextInteger(1, 5)),
		position: getRandomPointNearWorldOrigin(0.98),
		color: getRandomAccent(),
		...patch,
	};

	candyGrid.insert(candy.position, { id: candy.id });

	return candy;
}

export function removeCandy(id: string, eatenAt?: Vector2) {
	const candy = store.getState(selectCandyById(id));

	if (!candy) {
		return;
	}

	store.setCandyEatenAt(id, eatenAt ?? candy.position);
	candyGrid.remove(candy.position);

	setTimeout(() => {
		store.removeCandy(id);
	}, 5);
}

export function eatCandy(candyId: string, snakeId: string) {
	const candy = getCandy(candyId);
	const snake = getSnake(snakeId);

	if (snake && candy && !candy.eatenAt) {
		removeCandy(candy.id, snake.head);
		store.incrementSnakeScore(snake.id, candy.size);
	}
}

export function populateCandy(amount: number) {
	store.populateCandy(fillArray(amount, () => createCandy()));
}

export function removeCandyIfAtLimit(candyType: CandyType) {
	const max = CANDY_LIMITS[candyType];
	const count = store.getState(selectCandyCount(candyType));

	if (count > max) {
		store.bulkRemoveStaleCandy(candyType, count - max);
	}
}

export function dropCandyWhileBoosting(id: string) {
	return store.observeWhile(selectSnakeIsBoosting(id), () => {
		let previousTail = Vector2.zero;

		const dropCandy = () => {
			const snake = getSnake(id);

			if (snake) {
				const description = describeSnakeFromScore(snake.score);
				const tail: Vector2 | undefined = snake.tracers[snake.tracers.size() - 1];

				if (tail && tail.sub(previousTail).Magnitude > description.radius * 2) {
					previousTail = tail;
					store.addCandy(createCandy({ position: tail, type: CandyType.Dropping }));
				}
			}
		};

		const decrementScore = () => {
			const snake = getSnake(id);

			if (snake) {
				const maxDecrease = math.clamp(math.round(3 + 0.001 * snake.score), 2, 10);
				store.incrementSnakeScore(id, random.NextInteger(-maxDecrease, -1));
			}
		};

		decrementScore();

		return setInterval(() => {
			decrementScore();
			dropCandy();
		}, 0.15);
	});
}

export function dropCandyOnDeath(id: string): void {
	const snake = getSnake(id);

	if (!snake) {
		return;
	}

	const tracers = [...snake.tracers, snake.head];
	const tracerRadius = describeSnakeFromScore(snake.score).radius;
	const candyPositions: Vector2[] = [];
	let lastTracer: Vector2 | undefined;

	for (const tracer of tracers) {
		// to prevent candy bunching up in certain areas, don't insert the
		// position of the tracer if it's too close to the last spawned candy
		if (lastTracer && tracer.sub(lastTracer).Magnitude < 0.25 * tracerRadius) {
			continue;
		}

		lastTracer = tracer;

		// create multiple candies on this tracer based on radius
		const amount = math.round(random.NextNumber(1, math.max(tracerRadius, 1)));

		for (const _ of $range(1, amount)) {
			const x = random.NextNumber(-1, 1) * tracerRadius;
			const y = random.NextNumber(-1, 1) * tracerRadius;

			candyPositions.push(tracer.add(new Vector2(x, y)));
		}
	}

	// the total worth of the loot should scale logarithmically with the
	// snake's score, but not exceed the score itself
	const sum = math.min(8000 * math.log10(snake.score / 3000 + 1), snake.score);
	const total = candyPositions.size();

	const candies = candyPositions.mapFiltered((position, index) => {
		const skin = getSnakeSkinForTracer(snake.skin, index);

		return createCandy({
			position,
			type: CandyType.Loot,
			size: math.ceil(sum / total),
			color: skin.boostTint || skin.tint,
		});
	});

	store.populateCandy(candies);
}


import { Players } from "@rbxts/services";
import { store } from "server/store";
import { SNAKE_TICK_PHASE } from "server/world/constants";
import { getSafePointInWorld, killSnake, playerIsSpawned } from "server/world/utils";
import { WORLD_TICK } from "shared/constants/core";
import { remotes } from "shared/remotes";
import { defaultPlayerSave, RANDOM_SKIN, selectPlayerSave } from "shared/store/saves";
import { createScheduler } from "shared/utils/scheduler";

import { deleteSnakeInput, onSnakeTick, registerSnakeInput } from "./snake-tick";

export async function initSnakeService() {
	createScheduler({
		name: "snake",
		tick: WORLD_TICK,
		phase: SNAKE_TICK_PHASE,
		onTick: onSnakeTick,
	});

	remotes.snake.spawn.connect((player) => {
		if (playerIsSpawned(player)) {
			return;
		}

		const save = store.getState(selectPlayerSave(player.Name)) || defaultPlayerSave;

		// random skin starts at one because zero is reserved
		const randomSkin = save.skins[math.random(1, save.skins.size() - 1)];
		const currentSkin = save.skin;

		store.addSnake(player.Name, {
			name: player.DisplayName,
			head: getSafePointInWorld(),
			skin: currentSkin !== RANDOM_SKIN ? currentSkin : randomSkin,
			score: 10,
		});
	});

	remotes.snake.move.connect((player, angle) => {
		registerSnakeInput(player.Name, angle);
	});

	remotes.snake.boost.connect((player, boost) => {
		store.boostSnake(player.Name, boost);
	});

	remotes.snake.kill.connect((player) => {
		killSnake(player.Name);
	});

	Players.PlayerRemoving.Connect((player) => {
		deleteSnakeInput(player.Name);
		killSnake(player.Name);
	});
}


import { setTimeout } from "@rbxts/set-timeout";
import { store } from "server/store";
import { WORLD_BOUNDS } from "shared/constants/core";
import { selectCandyById } from "shared/store/candy";
import { selectSnakeById } from "shared/store/snakes";

import { snakeGrid } from "./services/snakes/snake-grid";

const MIN_SAFE_DISTANCE = 10;

export function getSnake(snakeId: string) {
	return store.getState(selectSnakeById(snakeId));
}

export function getCandy(candyId: string) {
	return store.getState(selectCandyById(candyId));
}

export function killSnake(snakeId: string) {
	store.setSnakeIsDead(snakeId);

	setTimeout(() => {
		store.removeSnake(snakeId);
	}, 2);
}

export function playerIsSpawned(player: Player) {
	return getSnake(player.Name) !== undefined;
}

/**
 * Returns a random point in the world. If the margin is specified,
 * the point will be within this percentage of the world bounds.
 */
export function getRandomPointInWorld(margin = 1) {
	const random = new Random();
	let position = new Vector2();

	do {
		const x = random.NextNumber(-margin, margin);
		const y = random.NextNumber(-margin, margin);
		position = new Vector2(x, y).mul(WORLD_BOUNDS);
	} while (position.Magnitude > WORLD_BOUNDS);

	return position;
}

/**
 * Returns a random point in the world that is more likely to be
 * closer to the origin.
 */
export function getRandomPointNearWorldOrigin(margin = 1, passes = 2) {
	let currentPosition = new Vector2();
	let currentDistance = math.huge;

	for (const _ of $range(0, passes)) {
		const position = getRandomPointInWorld(margin);
		const distance = position.Magnitude;

		if (distance < currentDistance) {
			currentPosition = position;
			currentDistance = distance;
		}
	}

	return currentPosition;
}

/**
 * Returns a safe point in the world. This should be a point that is
 * not too close to any other snake, but not the farthest point either.
 */
export function getSafePointInWorld() {
	const spawns: { position: Vector2; safety: number }[] = [];

	const scoreSafety = (spawn: Vector2) => {
		const nearest = snakeGrid.nearest(spawn, MIN_SAFE_DISTANCE * 2);
		const distance = nearest ? nearest.position.sub(spawn).Magnitude : math.huge;
		return distance;
	};

	for (const _ of $range(0, 10)) {
		const position = getRandomPointNearWorldOrigin(0.8);
		const safety = scoreSafety(position);
		spawns.push({ position, safety });
	}

	const sorted = spawns.sort((a, b) => a.safety < b.safety);

	// Find the first safe spawn that is still close to another snake
	for (const spawn of sorted) {
		if (spawn.safety > MIN_SAFE_DISTANCE) {
			return spawn.position;
		}
	}

	return sorted[sorted.size() - 1].position;
}


-- This file was @generated by Tarmac. It is not intended for manual editing.
return {
	images = {
		skins = {
			snake_awesome_body = "rbxassetid://14884183308",
			snake_awesome_head = "rbxassetid://14884183395",
			snake_black_ice = "rbxassetid://14836390041",
			snake_canada = "rbxassetid://14884212831",
			snake_eye_left = "rbxassetid://14835197016",
			snake_eye_right = "rbxassetid://14835197048",
			snake_jelly = "rbxassetid://14835608619",
			snake_jelly_blue = "rbxassetid://14836431074",
			snake_jelly_red = "rbxassetid://14836431136",
			snake_main = "rbxassetid://14884217464",
			snake_no_eye = "rbxassetid://14883700305",
			snake_outlined = "rbxassetid://14884183614",
			snake_stare_body = "rbxassetid://14884183698",
			snake_stare_head = "rbxassetid://14884183771",
			snake_stars = "rbxassetid://14884212936",
			snake_vamp_body = "rbxassetid://14884117497",
			snake_vamp_head = "rbxassetid://14884117564",
		},
		ui = {
			alert_dismiss = "rbxassetid://14402269904",
			backdrop = "rbxassetid://14921324346",
			blur = "rbxassetid://14120516187",
			button_glow_top = "rbxassetid://14367671668",
			circle = "rbxassetid://14120516288",
			heart = "rbxassetid://14789182848",
			heart_glow = "rbxassetid://14789182940",
			leader_pointer = "rbxassetid://14917242400",
			map_crosshair = "rbxassetid://14127467312",
			map_cursor = "rbxassetid://14127465927",
			menu_title = "rbxassetid://14120569384",
			nav_heart = "rbxassetid://14183565399",
			nav_heart_alt = "rbxassetid://14916522329",
			nav_home = "rbxassetid://14183565573",
			nav_home_alt = "rbxassetid://14183565737",
			nav_skins = "rbxassetid://14916522483",
			nav_skins_alt = "rbxassetid://14916522599",
			skin_card_gradient = "rbxassetid://14271107508",
			skin_indicator = "rbxassetid://14282707759",
			skin_indicator_locked = "rbxassetid://14282707833",
			tile_stripes = "rbxassetid://14137143166",
			vignette = "rbxassetid://14120516929",
		},
	},
}


import { throttle } from "@rbxts/set-timeout";

import { playSound } from "./play-sound";
import { sounds } from "./sounds";

export type ButtonSoundVariant = "default" | "alt" | "none";

const BUTTON_DELAY = 0.1;

let lastPressed: number | undefined;

export const playButtonDown = throttle((variant: ButtonSoundVariant = "default") => {
	if (variant === "none" || (lastPressed !== undefined && os.clock() - lastPressed < 2 * BUTTON_DELAY)) {
		return;
	}

	lastPressed = os.clock();

	return playSound(variant === "default" ? sounds.button_down : sounds.button_down_alt, {
		volume: 0.25,
	});
}, 2 * BUTTON_DELAY);

export async function playButtonUp(variant: ButtonSoundVariant = "default") {
	if (variant === "none" || lastPressed === undefined) {
		return;
	}

	const difference = os.clock() - lastPressed;

	lastPressed = undefined;

	if (difference < BUTTON_DELAY) {
		task.wait(BUTTON_DELAY - difference);
	}

	return playSound(variant === "default" ? sounds.button_up : sounds.button_up_alt, {
		volume: 0.25,
	});
}


import { SoundService } from "@rbxts/services";
import { IS_EDIT } from "shared/constants/core";

export interface SoundOptions {
	volume?: number;
	speed?: number;
	looped?: boolean;
	parent?: Instance;
}

export function createSound(
	soundId: string,
	{ volume = 0.5, speed = 1, looped = false, parent = SoundService }: SoundOptions = {},
) {
	const sound = new Instance("Sound");

	sound.SoundId = soundId;
	sound.Volume = volume;
	sound.PlaybackSpeed = speed;
	sound.Looped = looped;
	sound.Parent = parent;

	return sound;
}

export function playSound(soundId: string, options?: SoundOptions) {
	if (IS_EDIT) {
		return;
	}

	const sound = createSound(soundId, options);

	sound.Ended.Connect(() => sound.Destroy());
	sound.Play();

	return sound;
}


import { Players, RunService } from "@rbxts/services";

// Premium benefit applied when earning money passively
// during a game or when purchasing a product from the shop.
export const PREMIUM_BENEFIT = 1.2;

export const WORLD_BOUNDS = 128;
export const WORLD_TICK = 1 / 20;

export const SNAKE_SPEED = 6;
export const SNAKE_BOOST_SPEED = 12;

export const REMOTE_TICK = 1 / 20; // Roblox limits

export const IS_PROD = game.PlaceId === 14162747150;
export const IS_CANARY = !IS_PROD;
export const IS_EDIT = RunService.IsStudio() && !RunService.IsRunning();

export const USER_ID = Players.LocalPlayer ? Players.LocalPlayer.UserId : 0;
export const USER_NAME = Players.LocalPlayer ? Players.LocalPlayer.Name : "(server)";


import BitBuffer from "@rbxts/bitbuffer2";
import { CandyState } from "shared/store/candy";
import { countProperties } from "shared/utils/object-utils";

import { readColor3, readVector2, writeColor3, writeVector2 } from "../utils";

export function serializeCandy(state: CandyState): string {
	const buffer = new BitBuffer();

	buffer.WriteUInt(16, countProperties(state));

	for (const [, candy] of pairs(state)) {
		buffer.WriteString(candy.id);
		buffer.WriteInt(8, candy.type);
		buffer.WriteFloat32(candy.size);
		writeVector2(buffer, candy.position);
		writeColor3(buffer, candy.color);
	}

	return buffer.ToString();
}

export function deserializeCandy(data: string): CandyState {
	const state: Writable<CandyState> = {};
	const buffer = BitBuffer.FromString(data);
	const size = buffer.ReadUInt(16);

	for (const _ of $range(1, size)) {
		const id = buffer.ReadString();

		state[id] = {
			id,
			type: buffer.ReadInt(8),
			size: buffer.ReadFloat32(),
			position: readVector2(buffer),
			color: readColor3(buffer),
		};
	}

	return state;
}


import BitBuffer from "@rbxts/bitbuffer2";
import { SnakesState } from "shared/store/snakes";
import { countProperties } from "shared/utils/object-utils";

import { readArray, readVector2, writeArray, writeVector2 } from "../utils";

export function serializeSnakes(state: SnakesState): string {
	const buffer = new BitBuffer();

	buffer.WriteUInt(16, countProperties(state));

	for (const [, snake] of pairs(state)) {
		buffer.WriteString(snake.id);
		buffer.WriteString(snake.name);
		writeVector2(buffer, snake.head);
		buffer.WriteFloat32(snake.angle);
		buffer.WriteFloat32(snake.desiredAngle);
		buffer.WriteUInt(32, snake.score);
		buffer.WriteBool(snake.boost);
		writeArray(buffer, snake.tracers, writeVector2);
		buffer.WriteString(snake.skin);
		buffer.WriteBool(snake.dead);
		buffer.WriteUInt(16, snake.eliminations);
	}

	return buffer.ToString();
}

export function deserializeSnakes(data: string): SnakesState {
	const state: Writable<SnakesState> = {};
	const buffer = BitBuffer.FromString(data);
	const size = buffer.ReadUInt(16);

	for (const _ of $range(1, size)) {
		const id = buffer.ReadString();

		state[id] = {
			id,
			name: buffer.ReadString(),
			head: readVector2(buffer),
			angle: buffer.ReadFloat32(),
			desiredAngle: buffer.ReadFloat32(),
			score: buffer.ReadUInt(32),
			boost: buffer.ReadBool(),
			tracers: readArray(buffer, readVector2),
			skin: buffer.ReadString(),
			dead: buffer.ReadBool(),
			eliminations: buffer.ReadUInt(16),
		};
	}

	return state;
}


import BitBuffer from "@rbxts/bitbuffer2";

export function writeVector2(buffer: BitBuffer, vector: Vector2) {
	buffer.WriteFloat32(vector.X);
	buffer.WriteFloat32(vector.Y);
}

export function readVector2(buffer: BitBuffer) {
	const x = buffer.ReadFloat32();
	const y = buffer.ReadFloat32();
	return new Vector2(x, y);
}

export function writeColor3(buffer: BitBuffer, color: Color3) {
	const hex = color.ToHex();
	const int = tonumber(hex, 16) ?? 0;
	buffer.WriteUInt(24, int);
}

export function readColor3(buffer: BitBuffer) {
	const int = buffer.ReadUInt(24);
	const hex = string.format("%x", int);
	return Color3.fromHex(hex);
}

export function writeArray<T extends defined>(
	buffer: BitBuffer,
	array: readonly T[],
	write: (buffer: BitBuffer, value: T) => void,
) {
	buffer.WriteUInt(16, array.size());

	for (const value of array) {
		write(buffer, value);
	}
}

export function readArray<T extends defined>(buffer: BitBuffer, read: (buffer: BitBuffer) => T): T[] {
	const size = buffer.ReadUInt(16);
	const array: T[] = [];

	for (const _ of $range(1, size)) {
		array.push(read(buffer));
	}

	return array;
}


import Object from "@rbxts/object-utils";
import { createSelector } from "@rbxts/reflex";
import { mapProperties } from "shared/utils/object-utils";

import { SharedState } from "..";
import { CandyEntity, CandyType } from "./candy-slice";

export const identifyCandy = (candy: CandyEntity) => {
	return candy.id;
};

export const selectCandiesById = (state: SharedState) => {
	return state.candy;
};

export const selectCandies = createSelector(selectCandiesById, (byId) => {
	return Object.values(byId);
});

export const selectCandiesByIdOfType = (candyType: CandyType) => {
	return createSelector(selectCandiesById, (byId) => {
		return mapProperties(byId, (candy) => {
			return candy.type === candyType && !candy.eatenAt ? candy : undefined;
		});
	});
};

export const selectCandiesUneaten = createSelector(selectCandies, (candies) => {
	return candies.filter((candy) => !candy.eatenAt);
});

export const selectCandyCount = (filter?: CandyType) => {
	return createSelector(selectCandiesById, (byId) => {
		let size = 0;

		for (const [, candy] of pairs(byId)) {
			if (candy.eatenAt || (filter !== undefined && candy.type !== filter)) {
				continue;
			}

			size += 1;
		}

		return size;
	});
};

export const selectCandyById = (id: string) => {
	return (state: SharedState) => {
		return state.candy[id];
	};
};


import { createProducer } from "@rbxts/reflex";
import { assign, mapProperties, mapProperty } from "shared/utils/object-utils";

export interface CandyState {
	readonly [id: string]: CandyEntity | undefined;
}

export interface CandyEntity {
	readonly id: string;
	readonly size: number;
	readonly position: Vector2;
	readonly color: Color3;
	readonly type: CandyType;
	readonly eatenAt?: Vector2;
}

export enum CandyType {
	Default,
	Loot,
	Dropping,
}

const initialState: CandyState = {};

export const candySlice = createProducer(initialState, {
	populateCandy: (state, candy: CandyEntity[]) => {
		return assign(
			state,
			candy.reduce<{ [id: string]: CandyEntity }>((map, candy) => {
				map[candy.id] = candy;
				return map;
			}, {}),
		);
	},

	addCandy: (state, candy: CandyEntity) => {
		return assign(state, { [candy.id]: candy });
	},

	removeCandy: (state, id: string) => {
		return mapProperty(state, id, () => undefined);
	},

	setCandyEatenAt: (state, id: string, eatenAt: Vector2) => {
		return mapProperty(state, id, (candy) => ({
			...candy,
			eatenAt,
		}));
	},

	bulkRemoveStaleCandy: (state, candyType: CandyType, amount: number) => {
		const staleIds = new Set<string>();
		const candyList: CandyEntity[] = [];
		let candyListSize = 0;

		// reconstruct a list of candy, lower id = older
		for (const [id, candy] of pairs(state)) {
			if (candy.type !== candyType) {
				continue;
			}

			const insertAt = candyList.findIndex((otherCandy) => {
				return tonumber(otherCandy.id)! > tonumber(id)!;
			});

			if (insertAt !== -1) {
				candyList.insert(insertAt, candy);
			} else {
				candyList.push(candy);
			}

			candyListSize += 1;
		}

		// pick the amount of candy to remove, oldest first
		for (const index of $range(0, math.min(amount, candyListSize) - 1)) {
			staleIds.add(candyList[index].id);
		}

		return mapProperties(state, (candy) => {
			return !staleIds.has(candy.id) ? candy : undefined;
		});
	},
});


import { CombineStates } from "@rbxts/reflex";

import { candySlice } from "./candy";
import { saveSlice } from "./saves";
import { snakesSlice } from "./snakes";

export type SharedState = CombineStates<typeof slices>;

export const slices = {
	candy: candySlice,
	snakes: snakesSlice,
	saves: saveSlice,
};


import { ProducerMiddleware } from "@rbxts/reflex";
import { IS_PROD } from "shared/constants/core";

export const profilerMiddleware: ProducerMiddleware = () => {
	return (dispatch, name) => {
		if (IS_PROD) {
			return dispatch;
		}

		return (...args) => {
			debug.profilebegin(name);
			const result = dispatch(...args);
			debug.profileend();
			return result;
		};
	};
};


import { createProducer } from "@rbxts/reflex";
import { mapProperty } from "shared/utils/object-utils";

import { PlayerSave } from "./save-types";

export interface SaveState {
	readonly [id: string]: PlayerSave | undefined;
}

const initialState: SaveState = {};

export const saveSlice = createProducer(initialState, {
	setPlayerSave: (state, player: string, save: PlayerSave) => ({
		...state,
		[player]: save,
	}),

	deletePlayerSave: (state, player: string) => ({
		...state,
		[player]: undefined,
	}),

	patchPlayerSave: (state, player: string, patch: Partial<PlayerSave>) => {
		return mapProperty(state, player, (save) => ({
			...save,
			...patch,
		}));
	},

	givePlayerBalance: (state, player: string, amount: number) => {
		return mapProperty(state, player, (save) => ({
			...save,
			balance: math.max(save.balance + amount, 0),
		}));
	},

	givePlayerSkin: (state, player: string, skin: string) => {
		return mapProperty(state, player, (save) => ({
			...save,
			skins: [...save.skins, skin],
		}));
	},

	setPlayerSkin: (state, player: string, skin: string) => {
		return mapProperty(state, player, (save) => ({
			...save,
			skin,
		}));
	},
});


import { t } from "@rbxts/t";
import { baseSnakeSkins } from "shared/constants/skins";

export interface PlayerSave {
	readonly balance: number;
	readonly skins: readonly string[];
	readonly skin: string;
}

export const RANDOM_SKIN = "__random__";

export const defaultPlayerSave: PlayerSave = {
	balance: 100,
	skins: [RANDOM_SKIN, ...baseSnakeSkins.map((skin) => skin.id)],
	skin: RANDOM_SKIN,
};

export const playerSaveSchema: t.check<PlayerSave> = t.interface({
	balance: t.number,
	skins: t.array(t.string),
	skin: t.string,
});


import Object from "@rbxts/object-utils";
import { createSelector, shallowEqual } from "@rbxts/reflex";
import { USER_NAME } from "shared/constants/core";
import { SharedState } from "shared/store";
import { mapProperties } from "shared/utils/object-utils";
import { getPlayerByName } from "shared/utils/player-utils";

import { SnakeEntity } from "./snake-slice";
import { snakeIsBoosting } from "./snake-utils";

export const identifySnake = (snake: SnakeEntity) => {
	return snake.id;
};

export const cycleNextSnake = (currentId: string) => (state: SharedState) => {
	const snakes = selectSnakesSorted((a, b) => a.score > b.score)(state);
	const index = snakes.findIndex((snake) => snake.id === currentId);

	if (index !== -1) {
		return snakes[(index + 1) % snakes.size()]?.id;
	} else {
		return snakes[0]?.id;
	}
};

export const selectSnakesById = (state: SharedState) => {
	return state.snakes;
};

export const selectSnakeCount = createSelector(selectSnakesById, (snakesById) => {
	let count = 0;

	for (const [,] of pairs(snakesById)) {
		count++;
	}

	return count;
});

export const selectDeadSnakesById = createSelector(selectSnakesById, (snakesById) => {
	return mapProperties(snakesById, (snake) => (snake.dead ? snake : undefined));
});

export const selectAliveSnakesById = createSelector(selectSnakesById, (snakesById) => {
	return mapProperties(snakesById, (snake) => (!snake.dead ? snake : undefined));
});

export const selectPlayerSnakesById = createSelector(selectSnakesById, (snakesById) => {
	return mapProperties(snakesById, (snake) => (getPlayerByName(snake.id) ? snake : undefined));
});

export const selectPlayerCountIsAbove = (count: number) => {
	return createSelector(selectPlayerSnakesById, (snakesById) => {
		let playerCount = 0;

		for (const [,] of pairs(snakesById)) {
			playerCount++;

			if (playerCount >= count) {
				return true;
			}
		}

		return false;
	});
};

export const selectLocalSnake = (state: SharedState) => {
	return state.snakes[USER_NAME];
};

export const selectLocalScore = (state: SharedState) => {
	return state.snakes[USER_NAME]?.score;
};

export const selectLocalEliminations = (state: SharedState) => {
	return state.snakes[USER_NAME]?.eliminations;
};

export const selectHasLocalSnake = (state: SharedState) => {
	return USER_NAME in state.snakes;
};

export const selectSnakes = createSelector(selectSnakesById, (snakesById) => {
	return Object.values(snakesById);
});

export const selectTopSnake = createSelector(selectSnakesById, (snakesById) => {
	let topSnake: SnakeEntity | undefined;

	for (const [, snake] of pairs(snakesById)) {
		if (topSnake === undefined || snake.score > topSnake.score) {
			topSnake = snake;
		}
	}

	return topSnake;
});

export const selectSnakesSorted = (comparator: (current: SnakeEntity, existing: SnakeEntity) => boolean) => {
	return createSelector(selectSnakesById, (snakesById) => {
		const topSnakes: SnakeEntity[] = [];

		for (const [, snake] of pairs(snakesById)) {
			const index = topSnakes.findIndex((topSnake) => comparator(snake, topSnake));

			if (index === -1) {
				topSnakes.push(snake);
			} else {
				topSnakes.insert(index, snake);
			}
		}

		return topSnakes;
	});
};

export const selectSnakeIds = createSelector(
	[selectSnakesById],
	(snakesById) => {
		return Object.keys(snakesById) as readonly string[];
	},
	shallowEqual,
);

export const selectSnakeById = (id: string) => {
	return (state: SharedState) => state.snakes[id];
};

export const selectSnakeScore = (id: string) => {
	return (state: SharedState) => state.snakes[id]?.score;
};

export const selectSnakeIsDead = (id: string) => {
	return (state: SharedState) => {
		const snake = state.snakes[id];
		return snake ? snake.dead : true;
	};
};

export const selectSnakeIsBoosting = (id: string) => {
	return (state: SharedState) => {
		const snake = state.snakes[id];
		return snake ? snakeIsBoosting(snake) : false;
	};
};

export const selectSnakeRanking = (id: string) => {
	const comparator = (current: SnakeEntity, existing: SnakeEntity) => {
		return current.score > existing.score;
	};

	return createSelector(selectSnakesSorted(comparator), (snakes) => {
		const index = snakes.findIndex((snake) => snake.id === id);

		return index === -1 ? undefined : index + 1;
	});
};

export const selectLocalSnakeRanking = selectSnakeRanking(USER_NAME);

export const selectRankForDisplay = (state: SharedState) => {
	const ranking = selectLocalSnakeRanking(state);

	if (ranking === undefined) {
		return;
	}

	const lastDigit = ranking % 10;
	const lastTwoDigits = ranking % 100;

	if (lastTwoDigits >= 11 && lastTwoDigits <= 13) {
		return `${ranking}th`;
	} else if (lastDigit === 1) {
		return `${ranking}st`;
	} else if (lastDigit === 2) {
		return `${ranking}nd`;
	} else if (lastDigit === 3) {
		return `${ranking}rd`;
	} else {
		return `${ranking}th`;
	}
};


import { createProducer } from "@rbxts/reflex";
import { SNAKE_BOOST_SPEED, SNAKE_SPEED, WORLD_TICK } from "shared/constants/core";
import { map, turnRadians } from "shared/utils/math-utils";
import { mapProperties, mapProperty } from "shared/utils/object-utils";

import { describeSnakeFromScore, snakeIsBoosting } from "./snake-utils";

export interface SnakesState {
	readonly [id: string]: SnakeEntity | undefined;
}

export interface SnakeEntity {
	readonly id: string;
	readonly name: string;
	readonly head: Vector2;
	readonly angle: number;
	readonly desiredAngle: number;
	readonly score: number;
	readonly boost: boolean;
	readonly tracers: readonly Vector2[];
	readonly skin: string;
	readonly dead: boolean;
	readonly eliminations: number;
}

// Used to prevent tracers from overlapping
const TINY = 0.0001;

const defaultEntity: SnakeEntity = {
	id: "",
	name: "",
	head: new Vector2(),
	angle: 0,
	desiredAngle: 0,
	score: 10,
	boost: false,
	tracers: [],
	skin: "",
	dead: false,
	eliminations: 0,
};

const initialState: SnakesState = {};

export const snakesSlice = createProducer(initialState, {
	addSnake: (state, id: string, patch?: Partial<SnakeEntity>) => ({
		...state,
		[id]: { ...defaultEntity, id, name: id, ...patch },
	}),

	removeSnake: (state, id: string) => ({
		...state,
		[id]: undefined,
	}),

	snakeTick: (state, deltaTime: number = WORLD_TICK) => {
		return mapProperties(state, (snake) => {
			if (snake.dead) {
				return snake;
			}

			if (snake.score < 0) {
				// It's possible for score to be patched to a negative value, so
				// correct it here
				snake = { ...snake, score: 0 };
			}

			const description = describeSnakeFromScore(snake.score);

			const speed = snakeIsBoosting(snake) ? SNAKE_BOOST_SPEED : SNAKE_SPEED;
			const angle = turnRadians(snake.angle, snake.desiredAngle, description.turnSpeed * deltaTime);
			const direction = new Vector2(math.cos(angle), math.sin(angle));
			const head = snake.head.add(direction.mul(speed * deltaTime));

			const currentLength = snake.tracers.size();
			const desiredLength = math.floor(description.length);
			let tail = head;

			const tracers = snake.tracers.mapFiltered((tracer, index) => {
				if (index >= desiredLength) {
					return;
				}

				const previous = snake.tracers[index - 1] || snake.head;

				// spacing should be longer near the end of the snake to allow longer
				// snakes but with less tracers
				const spacing = map(index, 0, currentLength, description.spacingAtHead, description.spacingAtTail);

				// the alpha of the interpolation that will decide the space between
				// the current tracer and the previous tracer
				const alpha = math.clamp((deltaTime * speed) / spacing, TINY, 1 - TINY);

				if (index === desiredLength - 1) {
					// the tail's spacing from the previous tracer should be proportional
					// to the score needed to reach the next length
					tail = tail.Lerp(tracer.Lerp(previous, alpha), math.max(description.length % 1, TINY));
				} else {
					tail = tracer.Lerp(previous, alpha);
				}

				return tail;
			});

			if (currentLength < desiredLength) {
				for (const index of $range(currentLength, desiredLength - 1)) {
					tracers.push(tail.add(new Vector2(TINY * (index + 1), 0)));
				}
			}

			return { ...snake, head, angle, tracers };
		});
	},

	turnSnake: (state, id: string, desiredAngle: number) => {
		return mapProperty(state, id, (snake) => ({
			...snake,
			desiredAngle,
		}));
	},

	boostSnake: (state, id: string, boost: boolean) => {
		return mapProperty(state, id, (snake) => ({
			...snake,
			boost,
		}));
	},

	setSnakeIsDead: (state, id: string) => {
		return mapProperty(state, id, (snake) => ({
			...snake,
			dead: true,
		}));
	},

	patchSnake: (state, id: string, intersection: Partial<SnakeEntity>) => {
		return mapProperty(state, id, (snake) => ({
			...snake,
			...intersection,
		}));
	},

	incrementSnakeScore: (state, id: string, amount: number) => {
		return mapProperty(state, id, (snake) => ({
			...snake,
			score: math.max(snake.score + amount, 0),
		}));
	},

	incrementSnakeEliminations: (state, id: string) => {
		return mapProperty(state, id, (snake) => ({
			...snake,
			eliminations: snake.eliminations + 1,
		}));
	},
});


import { Players } from "@rbxts/services";
import { promiseTree } from "@rbxts/validate-tree";

const characterSchema = {
	$className: "Model",
	HumanoidRootPart: "BasePart",
	Humanoid: {
		$className: "Humanoid",
		Animator: "Animator",
	},
} as const;

export interface Character extends Model {
	HumanoidRootPart: BasePart;
	Humanoid: Humanoid & {
		Animator: Animator;
	};
}

export async function promiseCharacter(character: Model): Promise<Character> {
	return promiseTree(character, characterSchema).timeout(30, "Character timed out");
}

export async function promisePlayerDisconnected(player: Player): Promise<void> {
	if (!player.IsDescendantOf(Players)) {
		return;
	}

	await Promise.fromEvent(Players.PlayerRemoving, (playerWhoLeft) => playerWhoLeft === player);
}

export function getPlayerByName(name: string) {
	const player = Players.FindFirstChild(name);

	if (player?.IsA("Player")) {
		return player;
	}
}

export function onPlayerAdded(callback: (player: Player) => void) {
	const connection = Players.PlayerAdded.Connect(callback);

	for (const player of Players.GetPlayers()) {
		callback(player);
	}

	return () => connection.Disconnect();
}


import { RunService } from "@rbxts/services";

interface SchedulerOptions {
	readonly name: string;
	readonly tick: number;
	readonly phase?: number;
	readonly onTick?: (deltaTime: number) => void;
	readonly onRender?: (deltaTime: number, alpha: number) => void;
}

const connected = new Set<RBXScriptConnection>();

export function createScheduler({ name, tick, phase, onTick, onRender }: SchedulerOptions) {
	let timer = phase ?? 0;

	const connection = RunService.Heartbeat.Connect((deltaTime) => {
		const frameTime = math.min(deltaTime, tick);

		timer += frameTime;

		while (timer >= tick) {
			timer -= tick;
			debug.profilebegin(name);
			onTick?.(tick);
			debug.profileend();
		}

		onRender?.(frameTime, timer / tick);
	});

	connected.add(connection);

	return () => {
		connection.Disconnect();
		connected.delete(connection);
	};
}

export function disconnectAllSchedulers() {
	for (const connection of connected) {
		connection.Disconnect();
	}

	connected.clear();
}


import Signal from "@rbxts/lemon-signal";
import { CometError } from "../core/enum";
import { CometState } from "./../types/comet.d";

/**
 * Toolbar button instance with many great features.
 */
export class Button {
	private button: PluginToolbarButton;
	private buttonState: boolean;

	private clickEvent: Signal;

	constructor(
		readonly state: CometState,
		readonly text: string,
		readonly toolTip: string,
		readonly image: string,
		public readonly toggleable = true,
		readonly enabledOutsideViewport = false
	) {
		assert(state.appPlugin, CometError.APP_NOT_CREATED);

		if (!state.toolbar) {
			state.toolbar = state.appPlugin.CreateToolbar(state.appName);
		}

		this.button = state.toolbar.CreateButton(text, toolTip, image);
		this.button.ClickableWhenViewportHidden = enabledOutsideViewport;
		this.toggleable = toggleable;
		this.buttonState = false;
		this.clickEvent = new Signal();

		state.tracker.handle(
			this.button.Click.Connect(() => {
				if (this.toggleable) {
					this.buttonState = !this.buttonState;
				}

				this.button.SetActive(this.buttonState);
				this.clickEvent.Fire();
			})
		);
	}

	/**
	 * Set the button's disabled state.
	 * @param state
	 */
	public setEnabled(state: boolean) {
		this.button.Enabled = state;
	}

	/**
	 * Set the button's press state (visually)
	 * @param state New state of the button.
	 * @param noUpdate If true, the click event will not fire.
	 */
	public setPressed(state: boolean, noUpdate = false) {
		this.buttonState = state;
		this.button.SetActive(this.buttonState);
		if (!noUpdate) this.clickEvent.Fire();
	}

	/**
	 * Listen to button presses on this toolbar button.
	 * @param cb State of button (if toggleable)
	 */
	public onPress(cb: (state: boolean) => void) {
		this.clickEvent.Connect(() => {
			cb(this.buttonState);
		});

		return this;
	}
}


import { CometState } from "../types/comet";
import { CometError } from "../core/enum";
import Signal from "@rbxts/lemon-signal";
import { Button } from "./button";
import { Logger } from "../util/logger";

const HttpService = game.GetService("HttpService");

/**
 * A view is a container for GUI elements.
 */
export class View {
	public readonly container: DockWidgetPluginGui | ScreenGui;
	private onCloseBind: Signal;
	private onOpenBind: Signal;

	/**
	 * Create a window that renders on the viewport.
	 * @param app
	 * @param name
	 */
	constructor(state: CometState, name: string);
	/**
	 * Create a window that renders in a widget.
	 * @param name
	 * @param size
	 * @param minSize
	 */
	constructor(
		state: CometState,
		name: string,
		size: Vector2,
		minSize: Vector2,
		dockState?: Enum.InitialDockState
	);
	constructor(
		private readonly state: CometState,
		readonly name: string,
		readonly size?: Vector2,
		readonly minSize?: Vector2,
		readonly dockState = Enum.InitialDockState.Float
	) {
		assert(
			!state.windows.has(name),
			"[Comet] Detected multiple windows with the same name."
		);
		assert(state.appPlugin, CometError.APP_NOT_CREATED);

		this.onCloseBind = new Signal();
		this.onOpenBind = new Signal();

		// If both size and maxSize are given, we know we are creating a dock widget.
		if (size && minSize) {
			this.container = state.appPlugin.CreateDockWidgetPluginGui(
				HttpService.GenerateGUID(),
				new DockWidgetPluginGuiInfo(
					dockState,
					false,
					true,
					size.X,
					size.Y,
					minSize.X,
					minSize.Y
				)
			);

			this.container.Title = name;
			this.container.BindToClose(() => this.onCloseBind.Fire());
		} else {
			this.container = new Instance(
				"ScreenGui",
				game.GetService("CoreGui")
			);
			this.container.IgnoreGuiInset = true;
			this.container.Enabled = false;
			state.tracker.handle(this.container);
		}
	}

	/**
	 * Callback will be invoked whenever the window closes.
	 * @param cb
	 */
	public onClose(cb: () => void) {
		this.onCloseBind.Connect(cb);
	}

	/**
	 * Fired whenever the window opens.
	 * @param cb
	 */
	public onOpen(cb: () => void) {
		this.onOpenBind.Connect(cb);
	}

	/**
	 * Set visibility of the window.
	 * @param state boolean
	 */
	public setVisible(state: boolean) {
		if (this.container) {
			this.container.Enabled = state;
			if (state) this.onOpenBind.Fire();
			else this.onCloseBind.Fire();
		}
	}

	/**
	 * Get the visibility state of the window.
	 * @returns boolean
	 */
	public isVisible(): boolean {
		return this.container.Enabled;
	}

	/**
	 * Link a toolbar button to the view so that the button's state controls the view's visibility and vice-versa.
	 * @param button Button
	 */
	public linkButton(button: Button) {
		if (!button.toggleable) {
			Logger.fatal(
				"Attempted to link a button to a view, but the button isn't toggleable. Please adjust the button's constructor to fix this."
			);
		}

		this.onCloseBind.Connect(() => button.setPressed(false, true));
		button.onPress((state) => this.setVisible(state));
	}

	/**
	 * Returns the viewport size, but only will only function as expected in viewport mode.
	 * @returns Vector2
	 */
	public getViewportSize(): Vector2 {
		return this.container.AbsoluteSize;
	}

	/**
	 * Mount a GuiBase instance to the window.
	 * The `createCopy` parameter defaults to false, but if true it will copy it before re-parenting.
	 * @param element `GuiBase`
	 * @param createCopy boolean
	 * @returns element
	 */
	public mount<T extends GuiBase>(element: T, createCopy?: boolean): T;
	/**
	 * Custom mounting method. Used to integrate ui-libraries like react, vide or fusion.
	 * @param method (root: Instance) => (() => void)
	 */
	public mount<T extends GuiBase>(
		method: (root: Instance) => () => void
	): void;
	public mount<T extends GuiBase>(
		element: T | ((root: Instance) => () => void),
		createCopy = false
	) {
		assert(
			this.container,
			"Container instance is nill. This is most likely a bug."
		);

		if (typeIs(element, "function")) {
			const cb = element(this.container);
			this.state.tracker.handle(() => cb());
		} else {
			if (createCopy) element = element.Clone();
			(element as GuiBase).Parent = this.container;
			this.state.tracker.handle(element);
			return element;
		}
	}
}


import { InternalSystem } from "../core";
import { CometState } from "../types/comet";
import { Logger } from "../util/logger";

const ChangeHistoryService = game.GetService("ChangeHistoryService");

interface RecordConfig {
	name: string;
	description?: string;
	onSuccess?: "commit" | "append" | "cancel";
}

/**
 * A system that contains utilities for managing undo/redo waypoints.
 *
 * **You should never instantiate this class!** Instead, import it via the
 * `Dependency()` method within a constructor
 */
@InternalSystem()
export class History {
	private lastRecording: string | undefined;

	constructor(private state: CometState) {}

	/**
	 * A pcall-like wrapper for `record()` that allows you to quickly try an operation and abort changes if it fails.
	 *
	 * @param config
	 * @param fn
	 * @returns value or error.
	 */
	public async try<T extends Callback>(
		name: string,
		fn: T,
		...args: Parameters<T>
	): Promise<ReturnType<T>>;
	/**
	 * A pcall-like wrapper for `record()` that allows you to quickly try an operation and abort changes if it fails.
	 *
	 * Within the configuration, you may choose the action that is taken upon successful execution.
	 * This will default to `commit`.
	 *
	 * @param config
	 * @param fn
	 * @returns value or error.
	 */
	public async try<T extends Callback>(
		config: RecordConfig,
		fn: T,
		...args: Parameters<T>
	): Promise<ReturnType<T>>;
	public async try<T extends Callback>(
		config: RecordConfig | string,
		fn: T,
		...args: Parameters<T>
	): Promise<ReturnType<T>> {
		if (typeIs(config, "string")) config = { name: config };
		config.onSuccess ??= "commit";

		const recording = this.record(config.name, config.description);
		const [success, result] = pcall(fn, ...(args as unknown[]));

		if (success) {
			recording[config.onSuccess]();
			return result;
		}

		recording.cancel();
		throw result;
	}

	/**
	 * API for recoding waypoints to undo/redo history.
	 *
	 * ```ts
	 * const waypoint = this.record("My waypoint!");
	 *
	 * // Do some logic
	 * const part = new Instance("Part", Workspace);
	 *
	 * if (part) waypoint.commit();
	 * else waypoint.cancel();
	 * ```
	 * @param name
	 * @param description
	 * @returns object
	 */
	public record(name: string, description?: string) {
		let recording = ChangeHistoryService.TryBeginRecording(
			name,
			description
		);

		if (recording === undefined) {
			Logger.warn(
				`Recording '${this.lastRecording}' has been canceled because a new one was started. Refactor your code to ensure the recording is always handled properly.`
			);
			ChangeHistoryService.FinishRecording(
				"",
				Enum.FinishRecordingOperation.Cancel,
				undefined
			);
			this.lastRecording = undefined;
			recording = ChangeHistoryService.TryBeginRecording(
				name,
				description
			);
		}

		this.lastRecording = recording;

		// NOTE: returning an object here as a class would be a bit unnecessary. This may change as it's not very uniform.
		return {
			/**
			 * Commits the recorded work to the history.
			 * @param options
			 */
			commit: (options?: object) => {
				ChangeHistoryService.FinishRecording(
					recording!,
					Enum.FinishRecordingOperation.Commit,
					options
				);
			},

			/**
			 * Commits the recorded work and merges with the previous recording if possible.
			 * @param options
			 */
			append: (options?: object) => {
				ChangeHistoryService.FinishRecording(
					recording!,
					Enum.FinishRecordingOperation.Append,
					options
				);
			},

			/**
			 * Cancel the recording, undoing any changes made.
			 * @param options
			 */
			cancel: (options?: object) => {
				ChangeHistoryService.FinishRecording(
					recording!,
					Enum.FinishRecordingOperation.Cancel,
					options
				);
			}
		};
	}

	/**
	 * Undo the last action.
	 */
	public undo() {
		ChangeHistoryService.Undo();
	}

	/**
	 * Redo the last action.
	 */
	public redo() {
		ChangeHistoryService.Redo();
	}
}


local assets = {
	add = "rbxasset://.asphalt-plugin/add.png",
	caretDown = "rbxasset://.asphalt-plugin/caretDown.png",
	checkmark = "rbxasset://.asphalt-plugin/checkmark.png",
	extension = "rbxasset://.asphalt-plugin/extension.png",
	icons = {
		toolbarIcon = "rbxasset://.asphalt-plugin/icons/toolbarIcon.png",
	},
	search = "rbxasset://.asphalt-plugin/search.png",
}

return assets


import { atom } from "@rbxts/charm";

export const enum ComponentRealm {
	Client = 1,
	Server = 2,
}
export const enum EditorComponentFieldType {
	String = 1,
	Number = 2,
	Boolean = 3,
	Vector3 = 4,
	Vector2 = 5,
}
export interface EditorComponent {
	instance: ModuleScript;
	name: string;
	realm: ComponentRealm;
	id: string;
	fields?: Record<string, EditorComponentField>;
	isDirty?: boolean;
}

export interface EditorComponentField {
	default: unknown;
	field: EditorComponentFieldType;
}

export interface ComponentBuilder {
	constructor: (instance: Instance, fields: []) => Component;
	fields: Record<string, EditorComponentField>;
}

export interface Component {}
export const EditorComponentsAtom = atom<Record<string, EditorComponent>>({});


import { Atom, atom } from "@rbxts/charm";

export const ThemeColors = {
    "MainBackground" : Enum.StudioStyleGuideColor.MainBackground,
    "MainText" : Enum.StudioStyleGuideColor.MainText,
    "ScrollBar" : Enum.StudioStyleGuideColor.ScrollBar,
    "ScrollBarBackground" : Enum.StudioStyleGuideColor.ScrollBarBackground,
    "TabItem" : Enum.StudioStyleGuideColor.TableItem,
    "Button" : Enum.StudioStyleGuideColor.Button,
    "Light" : Enum.StudioStyleGuideColor.Light,
    "Dark" : Enum.StudioStyleGuideColor.Dark
}

type Modifiers = "Default" | "Disabled" | "Hover" | "Pressed" | "Selected"
export type StudioTheme = Record<keyof typeof ThemeColors, Record<Modifiers, Color3>>;

export const ThemeAtom = atom<StudioTheme>() as Atom<StudioTheme>


import { useEventListener } from "@rbxts/pretty-react-hooks";
import React, { useCallback, useEffect, useState } from "@rbxts/react";
import { act } from "@rbxts/react-roblox";
import { COMPONENT_ACTIVE } from "constants";

interface ComponentState {
	active: boolean;
}

export default function useComponent(id: string, instance: Instance) {
    const activeAttributeName = COMPONENT_ACTIVE + id;
	const [state, setState] = useState<ComponentState>({ active: true });

    useEffect(() => {
		let active = instance.GetAttribute(activeAttributeName) as boolean | undefined;
		if (active === undefined) {
			instance.SetAttribute(activeAttributeName, true);
			active = true;
		}
		setState({ active: active });
	}, []);

	useEventListener(instance.GetAttributeChangedSignal(activeAttributeName), () => {
		setState({ active: instance.GetAttribute(activeAttributeName) as boolean });
	});

	return $tuple(
		state,
		useCallback((active: boolean) => instance.SetAttribute(activeAttributeName, active), [instance]),
	);
}


import { useCallback, useEffect, useState } from "@rbxts/react";
import { EditorComponentField } from "atoms/editorComponents";
import useEditorComponets from "./useEditorComponents";
import { COMPONENT_FIELD } from "constants";
import { useEventListener } from "@rbxts/pretty-react-hooks";
import { String } from "@rbxts/jsnatives";
import { string } from "@rbxts/react/src/prop-types";

export default function useComponentFields(componentID: string, instance: Instance) {
	const [fields, setFields] = useState<Record<string, EditorComponentField>>({});
	const editorComponents = useEditorComponets();

	useEffect(() => {
		const component = editorComponents[componentID]!;
		const fields: Record<string, EditorComponentField> = {};

		if (!component.fields) return;
		for (const [name, field] of pairs(component.fields)) {
			const expectedAttribute = COMPONENT_FIELD + component.id + name;
			let value: unknown = instance.GetAttribute(expectedAttribute);
			if (value === undefined) {
				instance.SetAttribute(expectedAttribute, field.default as never);
				value = field.default;
			}
			fields[name] = { ...field, default: value };
		}
		setFields(fields);
	}, [editorComponents, componentID, instance]);

	useEventListener(instance.AttributeChanged, (name: string) => {
		const component = editorComponents[componentID]!;
		const prefix = COMPONENT_FIELD + component.id;
		if (!String.startsWith(name, prefix)) return;
		const refinatedName = name.sub(prefix.size() + 1);

		if (component.fields![refinatedName]) {
			const field = component.fields![refinatedName];
			const value = instance.GetAttribute(name);
			setFields((fields) => ({ ...fields, [refinatedName]: { ...field, default: value } }));
		}
	});

	return {
		fields: fields,
		setFields: useCallback(
			(field: string, value: unknown) =>
				instance.SetAttribute(COMPONENT_FIELD + componentID + field, value as never),
			[componentID, instance],
		),
	};
}


import { useAtom } from "@rbxts/react-charm";
import { EditorComponentsAtom } from "atoms/editorComponents";

export default function useEditorComponets() {
	return useAtom(EditorComponentsAtom);
}


import { String } from "@rbxts/jsnatives";
import { useCallback, useEffect, useState } from "@rbxts/react";
import { CollectionService } from "@rbxts/services";
import { Trash } from "@rbxts/trash";
import { EditorComponent, EditorComponentsAtom } from "atoms/editorComponents";
import { COMPONENT_ADDED_FLAG, COMPONENT_PREFIX, COMPONENT_REMOVED_FLAG } from "constants";

export default function useInstanceComponents(instance: Instance) {
	const [components, setComponents] = useState<EditorComponent[]>([]);

	useEffect(() => {
		setComponents([]);
		function checkTag(tag: string) {
			const isComponentTag = String.startsWith(tag, COMPONENT_PREFIX);
			if (!isComponentTag) return;

			const id = String.split(tag, ":")[1];
			return EditorComponentsAtom()[id];
		}

		function update() {
			const components: EditorComponent[] = [];
			for (const [_, tag] of ipairs(instance.GetTags())) {
				const component = checkTag(tag);
				if (!component) continue;
				components.push(component);
			}

			setComponents(components);
		}

		const trash = new Trash();
		trash.add(
			CollectionService.GetInstanceAddedSignal(COMPONENT_ADDED_FLAG).Connect((added) => {
				if (instance !== added) return;
				update();
			}),
		);

		trash.add(
			CollectionService.GetInstanceAddedSignal(COMPONENT_REMOVED_FLAG).Connect((added) => {
				if (instance !== added) return;
				update();
			}),
		);

		update();
		return () => trash.destroy();
	}, [instance]);

	return $tuple(
		// Components
		components,
		// setComponent
		useCallback(
			(id: string) => {
				CollectionService.AddTag(instance, COMPONENT_PREFIX + id);
				// this fires a new tag added change to the hook
				instance.AddTag(COMPONENT_ADDED_FLAG);
				instance.RemoveTag(COMPONENT_ADDED_FLAG);
			},
			[instance],
		),
		// removeComponent
		useCallback(
			(id: string) => {
				CollectionService.RemoveTag(instance, COMPONENT_PREFIX + id);
				// this fires a new tag removed change to the hook
				instance.AddTag(COMPONENT_REMOVED_FLAG);
				instance.RemoveTag(COMPONENT_REMOVED_FLAG);
			},
			[instance],
		),
	);
}


import { useAtom } from "@rbxts/react-charm";
import { SelectionAtom } from "atoms/selection";

export default function useSelection() {
    return useAtom(SelectionAtom)
}


import { useMemo } from "@rbxts/react";

const StudioService = game.GetService("StudioService");

export default function useInstanceIcon(instance: Instance | undefined) {
	return useMemo(() => {
		if (!instance) return "";
		const icon = (StudioService.GetClassIcon(instance.ClassName) as { Image: string }).Image;
		return icon;
	}, [instance]);
}


import { useAtom } from "@rbxts/react-charm";
import { ThemeAtom } from "atoms/theme";

export default function useTheme() {
    return useAtom(ThemeAtom)
}


import { Comet, LogLevel } from "@rbxts/comet";

Comet.createApp("Compo", true);
Comet.addPaths(script.WaitForChild("systems"));

Comet.configureLogger(LogLevel.VERBOSE)
Comet.launch();


import Attributes from "@rbxts/attributes";
import { Dependency, OnStart, System, Track } from "@rbxts/comet";
import { String } from "@rbxts/jsnatives";
import { ServerScriptService, StarterPlayer, Workspace } from "@rbxts/services";
import { Trash } from "@rbxts/trash";
import { ComponentRealm, EditorComponentsAtom } from "atoms/editorComponents";
import { COMPONENT_ACTIVE, COMPONENT_ID } from "constants";
import { newUUID } from "utility/uuidGenerator";
import { EditorComponentFieldSystem } from "./editorComponentField";

@System()
export class EditorComponentCollector implements OnStart {
	private editorComponentFieldSystem = Dependency(EditorComponentFieldSystem);

	private trackingPaths = [StarterPlayer, ServerScriptService];
	onStart() {
		for (const [_, path] of ipairs(this.trackingPaths)) {
			for (const [_, instance] of ipairs(path.GetDescendants())) {
				this.checkComponent(instance, path);
			}

			Track(
				path.DescendantAdded.Connect((child) => {
					this.checkComponent(child, path);
				}),
			);
		}
	}

	private checkComponent(instance: Instance, ancestor: Instance) {
		if (!instance.IsA("ModuleScript")) return;

		// is a component
		if (this.checkName(instance)) {
			this.registerComponent(instance, ancestor);
		}

		const potentialComponentTrash = new Trash();
		// is not a component (currently, so checking the instance name to know if we can turn it into a component)
		potentialComponentTrash.linkToInstance(instance, {
			trackInstance: false,
		});

		// checking name changes
		potentialComponentTrash.add(
			instance.GetPropertyChangedSignal("Name").Connect(() => {
				if (this.checkName(instance)) {
					this.registerComponent(instance, ancestor);
					potentialComponentTrash.destroy();
				}
			}),
		);

		// checking parent changes
		potentialComponentTrash.add(
			instance.AncestryChanged.Connect((_, parent) => {
				if (!instance.IsDescendantOf(ancestor)) {
					potentialComponentTrash.destroy();
				}
			}),
		);
	}

	private registerComponent(component: ModuleScript, ancestor: Instance) {
		const attributes = Attributes<{ [COMPONENT_ID]: string | undefined }>(component);
		if (!attributes._component_id) {
			attributes._component_id = newUUID();
		}

		const components = {
			...EditorComponentsAtom(),
			[attributes._component_id]: {
				instance: component,
				name: this.getName(component),
				realm: ancestor === StarterPlayer ? ComponentRealm.Client : ComponentRealm.Server,
				id: attributes._component_id,
				isDirty: true,
			},
		};

		const componentTrash = new Trash();
		componentTrash.linkToInstance(component, {
			trackInstance: false,
		});

		componentTrash.add(() => {
			this.unregisterComponent(component);
		});

		componentTrash.add(
			component.AncestryChanged.Connect((_, parent) => {
				if (!component.IsDescendantOf(ancestor)) {
					componentTrash.destroy();
				}
			}),
		);

		componentTrash.add(
			component.GetPropertyChangedSignal("Name").Connect(() => {
				if (!this.checkName(component)) {
					componentTrash.destroy();
				}
			}),
		);

		EditorComponentsAtom(components);
		this.editorComponentFieldSystem.updateFields();
	}

	private unregisterComponent(component: Instance) {
		const attributes = Attributes<{ _component_id: string | undefined }>(component);
		const id = attributes._component_id;
		attributes._component_id = undefined;

		// never was a component
		if (!id) return;

		const components = { ...EditorComponentsAtom() };
		delete components[id];

		EditorComponentsAtom(components);
	}

	private checkName(instance: Instance) {
		const splitted = instance.Name.split(".");
		return splitted.size() > 1 && splitted[splitted.size() - 1] === "component";
	}

	private getName(instance: Instance) {
		const splitted = instance.Name.split(".");
		splitted.remove(splitted.size() - 1);
		return String.toPascalCase(splitted.join(" ").gsub("(%u)", " %1")[0].gsub("^ ", "")[0]);
	}
}


import { COMPONENT_ID } from "constants";
import { Dependency, OnEnd, OnStart, Studio, System } from "@rbxts/comet";
import { ComponentBuilder, EditorComponent, EditorComponentField, EditorComponentsAtom } from "atoms/editorComponents";
import { sandboxload } from "utility/sandboxLoad";
import { effect } from "@rbxts/charm";
import { Trash } from "@rbxts/trash";
import { RunService } from "@rbxts/services";
const ScriptEditorService = game.GetService("ScriptEditorService");

@System()
export class EditorComponentFieldSystem implements OnStart, OnEnd {
	private Studio = Dependency(Studio);
	private fetchingTrash!: Trash;
	onStart() {
		this.startFetching();
	}
	onEnd(): void {
		this.stopFetching();
	}
	public startFetching() {
		this.fetchingTrash = new Trash();
		this.fetchingTrash.add(
			ScriptEditorService.TextDocumentDidChange.Connect((document) => {
				const documentScript = document.GetScript();
				if (!documentScript) return;

				const id = documentScript.GetAttribute(COMPONENT_ID) as string;
				const editorComponents = { ...EditorComponentsAtom() };

				const editorComponent = editorComponents[id];
				if (!editorComponent) return;
				if (editorComponent.isDirty) return;

				editorComponent.isDirty = true;
				EditorComponentsAtom(editorComponents);
			}),
		);

		//this.fetchingTrash.add(StudioService.GetPropertyChangedSignal("ActiveScript").Connect(() => this.updateFields()));
		this.fetchingTrash.add(ScriptEditorService.TextDocumentDidClose.Connect(() => this.updateFields()));
	}

	public stopFetching() {
		this.fetchingTrash?.destroy();
	}

	public getFields(component: ModuleScript): Record<string, EditorComponentField> | undefined {
		try {
			let builder: ComponentBuilder;
			// sandboxload fails if you're in run mode, so, change sandboxload() for require()
			if (RunService.IsRunning()) builder = require(component) as ComponentBuilder;
			else builder = (sandboxload(component.Source, component) as () => ComponentBuilder)();

			return builder.fields;
		} catch (e) {
			warn(
				`[🟠WARNING] Tried to read ${component.Name}, but an error ocurred: ${e}. Can't get component's fields`,
			);
			return undefined;
		}
	}

	public updateFields() {
		const editorComponents = { ...EditorComponentsAtom() };
		for (const [_, component] of pairs(editorComponents)) {
			if (component.isDirty) {
				component.isDirty = false;
				const fieds = this.getFields(component.instance);
				component.fields = fieds;
			}
		}

		EditorComponentsAtom(editorComponents);
	}
}


import { Dependency, GUI, OnStart, System, View } from "@rbxts/comet";
import { MainSystem } from "./main";
import { MountInspector } from "ui/inspector/mount";

@System()
export class InspectorSystem implements OnStart {
	private mainSystem = Dependency(MainSystem);
	private gui = Dependency(GUI);
	public View: View;

	constructor() {
		this.View = this.gui.createWidget(
			"Inspector",
			new Vector2(300, 500),
			new Vector2(100, 100),
			Enum.InitialDockState.Left,
		);
	}

	onStart() {
		this.View.linkButton(this.mainSystem.button);
		this.View.mount(MountInspector);
	}
}


import { History, GUI, Button, Dependency, System, Studio } from "@rbxts/comet";
import assets from "assets/assets";

@System()
export class MainSystem {
	private gui = Dependency(GUI);
	public button: Button;

	constructor() {
		this.button = this.gui.createButton(
			"Inspector",
			"Open the inspector window",
			assets.icons.toolbarIcon,
			true,
			false,
		);
	}
}


import { Dependency, OnInit, Studio, System } from "@rbxts/comet";
import { SelectionAtom } from "atoms/selection";

@System()
export class SelectionSystem implements OnInit{
    private studio = Dependency(Studio)
    onInit(): void {
        SelectionAtom(this.studio.getSelection()[0])
        this.studio.onSelectionChanged((newSelection) => {
            SelectionAtom(newSelection[0])
        })
    }
}


import { Logger, OnInit, System } from "@rbxts/comet";
import { StudioTheme, ThemeAtom, ThemeColors } from "atoms/theme";

@System()
export class StudioThemeSystem implements OnInit   {
    onInit () {
        this.onThemeChanged()
        settings().Studio.ThemeChanged.Connect(() => this.onThemeChanged())
    }

    private onThemeChanged() {
        const theme = settings().Studio.Theme

        let themes = {} as StudioTheme;
        for (const [name, color] of pairs(ThemeColors)) {
            const modifiers : Record<string, Color3>= {}
            for (const [_,mod] of ipairs(Enum.StudioStyleGuideModifier.GetEnumItems())) {
                modifiers[mod.Name] = theme.GetColor(color, mod)
            }

            themes[name] = modifiers
        }

        ThemeAtom(themes)
    }
}


import { useEventListener } from "@rbxts/pretty-react-hooks";
import React, { ReactNode, useRef, useState } from "@rbxts/react";

interface Props {
	children?: ReactNode;
	Size?: UDim2;
	Position?: UDim2;
	AnchorPoint?: Vector2;
	BackgroundColor3?: Color3;
	BackgroundTransparency?: number;
	BorderSizePixel?: number;
	LayoutOrder?: number;
	ZIndex?: number;
	Visible?: boolean;
}
export default function AutomaticScrollingFrame({
	children,
	Size,
	Position,
	AnchorPoint,
	BackgroundColor3,
	BackgroundTransparency,
	BorderSizePixel,
	LayoutOrder,
	ZIndex,
	Visible,
}: Props) {
	return (
		<scrollingframe
			Size={Size}
			Position={Position}
			AnchorPoint={AnchorPoint}
			BackgroundColor3={BackgroundColor3}
			BackgroundTransparency={BackgroundTransparency}
			BorderSizePixel={BorderSizePixel}
			LayoutOrder={LayoutOrder}
			ZIndex={ZIndex}
			Visible={Visible}
		>
			{children}
		</scrollingframe>
	);
}


import React from "@rbxts/react";
import { checkmark } from "assets/assets";
import useTheme from "hooks/visuals/useTheme";

interface Props {
	checked: boolean;
	onClick?: () => void;
}

export default function CheckBox({ checked, onClick }: Props) {
	const theme = useTheme();
	return (
		<imagebutton
			Size={UDim2.fromOffset(14, 14)}
			BackgroundColor3={theme.Dark.Default}
			BorderSizePixel={0}
			Event={{ MouseButton1Click: onClick }}
		>
			<uicorner CornerRadius={new UDim(0, 5)} />
			<imagelabel
				Image={checked ? checkmark : undefined}
				ImageColor3={theme.MainText.Default}
				Size={UDim2.fromOffset(12, 12)}
				Position={UDim2.fromScale(0.5, 0.5)}
				AnchorPoint={new Vector2(0.5, 0.5)}
				BackgroundTransparency={1}
			/>
		</imagebutton>
	);
}


import React from "@rbxts/react";

interface PaddingProps {
    Padding? : UDim,
    Top? : UDim,
    Right? : UDim,
    Bottom? : UDim,
    Left? : UDim
}

export default function Padding({
    Padding,
    Top,
    Right,
    Bottom,
    Left
} : PaddingProps) {
    return (
        <uipadding PaddingBottom={Padding ?? Bottom} PaddingLeft={Padding ?? Left} PaddingRight={Padding ?? Right} PaddingTop={Padding ?? Top}/>
    )
}


import React from "@rbxts/react";
import useTheme from "hooks/visuals/useTheme";

export default function Separator() {
	const theme = useTheme();
	return (
		<frame Size={new UDim2(1, 0, 0, 1)} BackgroundColor3={theme.Dark.Default} BackgroundTransparency={0.25}>
			<uicorner CornerRadius={new UDim(1, 0)} />
		</frame>
	);
}


import React from "@rbxts/react";
import { InspectorView } from "ui/inspector/components/inspectorView";

export default function App() {
	return <InspectorView />;
}


import React from "@rbxts/react";
import assets from "assets/assets";
import useTheme from "hooks/visuals/useTheme";
import Padding from "ui/components/padding";

export default function AddComponentButton({ OnClick }: { OnClick?: () => void }) {
	const theme = useTheme();
	return (
		<frame BackgroundTransparency={1} AutomaticSize={"XY"}>
			<imagebutton
				BackgroundColor3={theme.Button.Selected}
				AutomaticSize={"XY"}
				Event={{ MouseButton1Click: OnClick }}
			>
				<uicorner CornerRadius={new UDim(0, 5)} />
				<Padding Top={new UDim(0, 5)} Bottom={new UDim(0, 5)} Left={new UDim(0, 30)} Right={new UDim(0, 30)} />
				<uilistlayout FillDirection={"Horizontal"} ItemLineAlignment={"Center"} />
				<imagelabel
					Size={UDim2.fromOffset(12, 12)}
					Image={assets.add}
					BackgroundTransparency={1}
					ImageColor3={theme.MainText.Selected}
				/>
				<textlabel
					Text={"Add Component"}
					BackgroundTransparency={1}
					TextColor3={theme.MainText.Selected}
					AutomaticSize={"XY"}
				/>
			</imagebutton>
		</frame>
	);
}


import React, { ReactNode } from "@rbxts/react";
import useTheme from "hooks/visuals/useTheme";
import Padding from "ui/components/padding";

export default function InspectorSection({ children, color }: { children: ReactNode; color?: Color3 }) {
	const theme = useTheme();
	return (
		<frame AutomaticSize={"Y"} Size={new UDim2(1, 0, 0, 0)} BackgroundColor3={color ?? theme.TabItem.Default}>
			<Padding Padding={new UDim(0, 5)} />
			<uicorner CornerRadius={new UDim(0, 5)} />
			<uistroke Thickness={1} Color={theme.Dark.Default} Transparency={0.25} />
			{children}
		</frame>
	);
}


import React, { ReactNode, useEffect, useState } from "@rbxts/react";
import useTheme from "hooks/visuals/useTheme";
import Padding from "ui/components/padding";

export default function InspectorSectionButton({
	Selected,
	OnClick,
	children,
}: {
	children: ReactNode;
	OnClick?: () => void;
	Selected?: boolean;
}) {
	const theme = useTheme();
	const [color, setColor] = useState(Selected ? theme.Button.Selected : theme.Button.Default);

	useEffect(() => {
		setColor(Selected ? theme.Button.Selected : theme.Button.Default);
	}, [Selected, theme]);
	return (
		<imagebutton
			AutoButtonColor={false}
			AutomaticSize={"Y"}
			Size={new UDim2(1, 0, 0, 0)}
			BackgroundColor3={color}
			Event={{
				MouseEnter: () => setColor(Selected ? theme.Button.Selected : theme.Button.Hover),
				MouseLeave: () => setColor(Selected ? theme.Button.Selected : theme.Button.Default),
				MouseButton1Down: () => setColor(Selected ? theme.Button.Selected : theme.Button.Pressed),
				MouseButton1Up: () => setColor(Selected ? theme.Button.Selected : theme.Button.Hover),
				MouseButton1Click: OnClick,
			}}
		>
			<Padding Padding={new UDim(0, 5)} />
			<uicorner CornerRadius={new UDim(0, 5)} />
			{children}
		</imagebutton>
	);
}


import { Object, String } from "@rbxts/jsnatives";
import React, { Fragment, useMemo, useState } from "@rbxts/react";
import useEditorComponets from "hooks/logic/useEditorComponents";
import useTheme from "hooks/visuals/useTheme";
import Padding from "ui/components/padding";
import assets from "assets/assets";
import { ComponentRealm } from "atoms/editorComponents";
import InspectorSectionButton from "./common/inspectorSectionButton";
import useInstanceComponents from "hooks/logic/useInstanceComponents";

export default function InspectorComponentsList({ close, instance }: { close: () => void; instance: Instance }) {
	const [search, setSearch] = useState("");
	const theme = useTheme();
	const [components, setComponent] = useInstanceComponents(instance);
	const editorComponents = useEditorComponets();
	const filteredEditorComponents = useMemo(() => {
		return Object.values(editorComponents!).filter((component) =>
			String.includes(component.name.lower(), search.lower()),
		);
	}, [editorComponents, search]);

	return (
		<Fragment>
			<imagebutton
				Size={new UDim2(2, 0, 2, 0)}
				AnchorPoint={new Vector2(0.5, 0.5)}
				Position={new UDim2(0.5, 0, 0.5, 0)}
				BackgroundTransparency={1}
				Event={{
					MouseButton1Click: close,
				}}
			/>
			<imagebutton
				AutoButtonColor={false}
				Size={new UDim2(1, 0, 1, 0)}
				AnchorPoint={new Vector2(0.5, 0.5)}
				Position={UDim2.fromScale(0.5, 0.5)}
				BackgroundColor3={theme.TabItem.Default}
			>
				<uicorner CornerRadius={new UDim(0, 5)} />
				<uisizeconstraint MaxSize={new Vector2(350, 425)} />
				<uistroke Color={theme.TabItem.Hover} />
				<uilistlayout FillDirection={"Vertical"} HorizontalFlex={"Fill"} Padding={new UDim(0, 5)} />
				<Padding Padding={new UDim(0, 5)} />

				<frame AutomaticSize={"Y"} BackgroundTransparency={1}>
					<Padding Padding={new UDim(0, 5)} />
					<uilistlayout
						FillDirection={"Horizontal"}
						HorizontalFlex={"Fill"}
						Padding={new UDim(0, 5)}
						ItemLineAlignment={"Center"}
					/>
					<imagelabel
						Size={UDim2.fromOffset(12, 12)}
						BackgroundTransparency={1}
						Image={assets.search}
						ImageColor3={theme.MainText.Default}
					/>
					<textbox
						AutomaticSize={"Y"}
						Size={new UDim2(1, 0, 0, 0)}
						BackgroundTransparency={0}
						BackgroundColor3={theme.Dark.Default}
						TextColor3={theme.MainText.Default}
						Text={""}
						TextXAlignment={"Left"}
						Change={{
							Text: (box) => setSearch(box.Text),
						}}
					>
						<Padding Padding={new UDim(0, 3)} />
						<uicorner CornerRadius={new UDim(0, 5)} />
					</textbox>
				</frame>

				<scrollingframe
					AutomaticCanvasSize={"Y"}
					Size={new UDim2(1, 0, 1, 0)}
					CanvasSize={new UDim2(1, 0, 0, 0)}
					BackgroundTransparency={1}
				>
					<Padding Padding={new UDim(0, 1)} />
					<uilistlayout FillDirection={"Vertical"} HorizontalFlex={"Fill"} Padding={new UDim(0, 5)} />

					{filteredEditorComponents.map((component) => (
						<InspectorSectionButton OnClick={() => setComponent(component.id)}>
							<uistroke
								Color={components.includes(component) ? theme.Button.Selected : theme.Dark.Default}
							/>
							<uilistlayout
								FillDirection={"Horizontal"}
								ItemLineAlignment={"Center"}
								Padding={new UDim(0, 5)}
							/>
							<imagelabel
								Image={assets.extension}
								BackgroundTransparency={1}
								Size={UDim2.fromOffset(16, 16)}
								ImageColor3={
									component.realm === ComponentRealm.Client
										? Color3.fromHex("#4cbcff")
										: Color3.fromHex("#26d964")
								}
							/>
							<textlabel
								BackgroundTransparency={1}
								Text={component.name}
								AutomaticSize={"XY"}
								TextColor3={
									components.includes(component) ? theme.MainText.Selected : theme.MainText.Default
								}
							/>
						</InspectorSectionButton>
					))}
				</scrollingframe>
			</imagebutton>
		</Fragment>
	);
}


import React, { ReactNode } from "@rbxts/react";
import useTheme from "hooks/visuals/useTheme";
import Padding from "ui/components/padding";
export default function InspectorContainer({ children }: { children: ReactNode }) {
	const theme = useTheme();
	return (
		<scrollingframe
			Size={UDim2.fromScale(1, 1)}
			BorderSizePixel={0}
			ScrollBarImageColor3={theme.ScrollBar.Default}
			ScrollBarThickness={8}
			BackgroundColor3={theme.MainBackground.Default}
			AutomaticCanvasSize={"Y"}
			CanvasSize={UDim2.fromScale(0, 0)}
		>
			<Padding Padding={new UDim(0, 1)} />
			{children}
		</scrollingframe>
	);
}


import React from "@rbxts/react";
import useSelection from "hooks/logic/useSelection";
import InspectorSection from "./common/inspectorSection";
import useTheme from "hooks/visuals/useTheme";
import useInstanceIcon from "hooks/visuals/useInstanceIcon";
export default function InspectorInstance() {
	const selection = useSelection();
	const theme = useTheme();
	const selectionIcon = useInstanceIcon(selection);
	return (
		<InspectorSection>
			<uilistlayout FillDirection={"Horizontal"} ItemLineAlignment={"Center"} Padding={new UDim(0, 5)} />
			<imagelabel BackgroundTransparency={1} Image={selectionIcon} Size={UDim2.fromOffset(16, 16)} />
			<textlabel
				Text={selection?.Name}
				AutomaticSize={"XY"}
				TextColor3={theme.MainText.Default}
				BackgroundTransparency={1}
			/>
		</InspectorSection>
	);
}


import React, { Fragment, useEffect, useState } from "@rbxts/react";
import InspectorContainer from "./inspectorContainer";
import useSelection from "hooks/logic/useSelection";
import Padding from "ui/components/padding";
import useTheme from "hooks/visuals/useTheme";
import InspectorInstance from "./inspectorInstance";
import AddComponentButton from "./addComponent";
import Separator from "ui/components/separator";
import InspectorComponentsList from "./inspectorComponentsList";
import InstanceComponents from "./instanceComponents";

export function InspectorView() {
	const theme = useTheme();
	const selection = useSelection();
	const [showComponentsList, setShowComponentsList] = useState(false);

	useEffect(() => {
		setShowComponentsList(false);
	}, [selection]);

	return (
		<frame Size={UDim2.fromScale(1, 1)} BackgroundTransparency={1}>
			<Padding Padding={new UDim(0, 6)} />
			<InspectorContainer>
				<uilistlayout FillDirection={"Vertical"} Padding={new UDim(0, 5)} HorizontalAlignment={"Center"} />
				{selection && (
					<Fragment>
						<InspectorInstance />
						<Separator />
						<InstanceComponents Instance={selection} />
						<Separator />
						<AddComponentButton OnClick={() => setShowComponentsList((previous) => !previous)} />
					</Fragment>
				)}
			</InspectorContainer>

			{showComponentsList && selection && (
				<InspectorComponentsList instance={selection} close={() => setShowComponentsList(false)} />
			)}
		</frame>
	);
}


import React, { Fragment } from "@rbxts/react";
import useInstanceComponents from "hooks/logic/useInstanceComponents";
import InspectorSection from "./common/inspectorSection";
import Padding from "ui/components/padding";
import useTheme from "hooks/visuals/useTheme";
import { ComponentRealm } from "atoms/editorComponents";
import assets from "assets/assets";
import CheckBox from "ui/components/checkbox";
import useComponent from "hooks/logic/useComponent";
import SerializedComponent from "./serializedComponent";
export default function InstanceComponents({ Instance }: { Instance: Instance }) {
	const [components] = useInstanceComponents(Instance);
	const theme = useTheme();
	return (
		<frame BackgroundTransparency={1} AutomaticSize={"Y"} Size={UDim2.fromScale(1, 0)}>
			<uilistlayout Padding={new UDim(0, 5)} FillDirection={"Vertical"} />
			{components.map((component) => {
				return <SerializedComponent component={component} instance={Instance} key={component.id} />;
			})}
		</frame>
	);
}


import React, { useState } from "@rbxts/react";
import InspectorSection from "./common/inspectorSection";
import useComponent from "hooks/logic/useComponent";
import Padding from "ui/components/padding";
import assets from "assets/assets";
import useTheme from "hooks/visuals/useTheme";
import { EditorComponent } from "atoms/editorComponents";
import { ComponentRealm } from "atoms/editorComponents";
import CheckBox from "ui/components/checkbox";
import SerializedComponentFields from "./serializedFields";

interface Props {
	component: EditorComponent;
	instance: Instance;
}
export default function SerializedComponent({ component, instance }: Props) {
	const [componentState, setActive] = useComponent(component.id, instance);
	const [compact, setCompact] = useState(false);

	const theme = useTheme();
	return (
		<InspectorSection>
			<Padding Padding={new UDim(0, 5)} />
			<uilistlayout FillDirection={"Vertical"} Padding={new UDim(0, 5)} />
			<frame AutomaticSize={"Y"}>
				<uilistlayout FillDirection={"Horizontal"} ItemLineAlignment={"Center"} Padding={new UDim(0, 0)} />

				<frame BackgroundTransparency={1} Size={UDim2.fromOffset(16, 16)}>
					<imagebutton
						Image={assets.caretDown}
						Rotation={compact ? -90 : 0}
						BackgroundTransparency={1}
						ImageColor3={theme.MainText.Default}
						Size={UDim2.fromOffset(16, 16)}
						ImageTransparency={0.5}
						Event={{ MouseButton1Click: () => setCompact((val) => !val) }}
					/>
				</frame>

				<frame AutomaticSize={"XY"} BackgroundTransparency={1}>
					<frame AutomaticSize={"XY"} BackgroundTransparency={1}>
						<uilistlayout
							FillDirection={"Horizontal"}
							Padding={new UDim(0, 5)}
							ItemLineAlignment={"Center"}
						/>
						<imagelabel
							Image={assets.extension}
							BackgroundTransparency={1}
							Size={UDim2.fromOffset(16, 16)}
							ImageColor3={
								component.realm === ComponentRealm.Client
									? Color3.fromHex("#4cbcff")
									: Color3.fromHex("#26d964")
							}
						/>

						<CheckBox checked={componentState.active} onClick={() => setActive(!componentState.active)} />

						<textlabel
							Text={component.name}
							BackgroundTransparency={1}
							TextColor3={theme.MainText.Default}
							AutomaticSize={"XY"}
						/>
					</frame>
				</frame>
			</frame>

			{!compact && (
				<frame AutomaticSize={"Y"} Size={UDim2.fromScale(1, 0)} BackgroundTransparency={1}>
					<Padding Left={new UDim(0, 17)} />
					<SerializedComponentFields component={component} instance={instance} />
				</frame>
			)}
		</InspectorSection>
	);
}


import { Object, String } from "@rbxts/jsnatives";
import React, { Fragment, useEffect } from "@rbxts/react";
import { EditorComponent, EditorComponentFieldType } from "atoms/editorComponents";
import useTheme from "hooks/visuals/useTheme";
import useComponentFields from "hooks/logic/useComponentFields";
import RenderField from "../fields/renderField";

export default function SerializedComponentFields({
	component,
	instance,
}: {
	component: EditorComponent;
	instance: Instance;
}) {
	const theme = useTheme();
	const { fields, setFields } = useComponentFields(component.id, instance);
	return (
		<frame BackgroundTransparency={1} AutomaticSize={"XY"} Size={UDim2.fromScale(1, 0)}>
			<uilistlayout FillDirection={"Vertical"} Padding={new UDim(0, 5)} SortOrder={"Name"} />
			{component.fields
				? Object.entries(fields).map(([name, field]) => {
						return (
							<frame
								BackgroundTransparency={1}
								AutomaticSize={"XY"}
								Size={UDim2.fromScale(1, 0)}
								key={name}
							>
								<uilistlayout
									FillDirection={"Horizontal"}
									ItemLineAlignment={"Center"}
									Padding={new UDim(0, 5)}
								/>
								<textlabel
									Text={name}
									BackgroundTransparency={1}
									TextColor3={theme.MainText.Default}
									AutomaticSize={"XY"}
									Size={UDim2.fromOffset(100)}
									TextXAlignment={"Left"}
								/>

								<RenderField
									name={name}
									field={field as never}
									setFields={(val) => setFields(name, val)}
								/>
							</frame>
						);
					})
				: undefined}
		</frame>
	);
}


import React, { Fragment, useEffect } from "@rbxts/react";
import useTheme from "hooks/visuals/useTheme";
import CheckBox from "ui/components/checkbox";
import Padding from "ui/components/padding";

export default function BooleanInspectorField({
	value,
	onChange,
}: {
	value: boolean;
	onChange: (value: boolean) => void;
}) {
	return <CheckBox checked={value} onClick={() => onChange?.(!value)} />;
}


import React, { Fragment } from "@rbxts/react";
import useTheme from "hooks/visuals/useTheme";
import Padding from "ui/components/padding";

export default function NumberInspectorField({
	value,
	onChange,
}: {
	value: number;
	onChange: (value: number) => void;
}) {
	const theme = useTheme();

	return (
		<textbox
			Text={tostring(value)}
			TextColor3={theme.MainText.Default}
			BackgroundColor3={theme.Dark.Default}
			AutomaticSize={"XY"}
			BorderSizePixel={0}
			TextXAlignment={"Left"}
			ClearTextOnFocus={false}
			Event={{
				FocusLost: (rbx) => {
					const num = tonumber(rbx.Text.gsub("[^%d%.-]", "")[0])! ?? 0;
					rbx.Text = tostring(num);
					onChange?.(num);
				},
			}}
		>
			<uicorner CornerRadius={new UDim(0, 5)} />
			<uiflexitem FlexMode={"Fill"} />
			<Padding Padding={new UDim(0, 3)} />
		</textbox>
	);
}


import React, { Fragment, useEffect } from "@rbxts/react";
import { EditorComponentField, EditorComponentFieldType } from "atoms/editorComponents";
import StringInspectorField from "./stringField";
import NumberInspectorField from "./numberField";
import BooleanInspectorField from "./booleanField";
import Vector3InspectorField from "./vector3Field";
import Vector2InspectorField from "./vector2Field";

interface Field {
	field: EditorComponentFieldType;
	default: string | number | boolean | Vector3 | Vector2;
}

export default function RenderField({
	name,
	field,
	setFields,
}: {
	name: string;
	field: Field;
	setFields: (value: unknown) => void;
}) {
	switch (field.field) {
		case EditorComponentFieldType.String:
			return (
				<StringInspectorField value={field.default as string} onChange={(value: string) => setFields(value)} />
			);
		case EditorComponentFieldType.Number:
			return (
				<NumberInspectorField value={field.default as number} onChange={(value: number) => setFields(value)} />
			);
		case EditorComponentFieldType.Boolean:
			return (
				<BooleanInspectorField
					value={field.default as boolean}
					onChange={(value: boolean) => setFields(value)}
				/>
			);

		case EditorComponentFieldType.Vector3:
			return (
				<Vector3InspectorField
					value={field.default as Vector3}
					onChange={(value: Vector3) => setFields(value)}
				/>
			);

		case EditorComponentFieldType.Vector2:
			return (
				<Vector2InspectorField
					value={field.default as Vector2}
					onChange={(value: Vector2) => setFields(value)}
				/>
			);
		default:
			return <Fragment />;
	}
}


import React, { Fragment } from "@rbxts/react";
import useTheme from "hooks/visuals/useTheme";
import Padding from "ui/components/padding";

export default function StringInspectorField({
	value,
	onChange,
}: {
	value: string;
	onChange: (value: string) => void;
}) {
	const theme = useTheme();
	return (
		<textbox
			Text={value}
			TextColor3={theme.MainText.Default}
			BackgroundColor3={theme.Dark.Default}
			AutomaticSize={"XY"}
			BorderSizePixel={0}
			TextXAlignment={"Left"}
			ClearTextOnFocus={false}
			Change={{
				Text: (rbx) => {
					onChange(rbx.Text);
				},
			}}
		>
			<uicorner CornerRadius={new UDim(0, 5)} />
			<uiflexitem FlexMode={"Fill"} />
			<Padding Padding={new UDim(0, 3)} />
		</textbox>
	);
}


import React, { Fragment } from "@rbxts/react";
import useTheme from "hooks/visuals/useTheme";
import Padding from "ui/components/padding";

export default function Vector2InspectorField({
	value,
	onChange,
}: {
	value: Vector2;
	onChange: (value: Vector2) => void;
}) {
	const theme = useTheme();
	return (
		<frame AutomaticSize={"XY"} BackgroundTransparency={1}>
			<uiflexitem FlexMode={"Fill"} />
			<uilistlayout FillDirection={"Horizontal"} Padding={new UDim(0, 3)} ItemLineAlignment={"Center"} />

			<textlabel AutomaticSize={"XY"} BackgroundTransparency={1} Text={"X"} TextColor3={theme.MainText.Default} />
			<textbox
				Text={tostring(value.X)}
				TextColor3={theme.MainText.Default}
				AutomaticSize={"Y"}
				BackgroundColor3={theme.Dark.Default}
				TextXAlignment={"Left"}
				ClearTextOnFocus={false}
				ClipsDescendants
				Event={{
					FocusLost: (rbx) => {
						const num = tonumber(rbx.Text.gsub("[^%d%.-]", "")[0])! ?? 0;
						onChange(new Vector2(num, value.Y));
					},
				}}
			>
				<uicorner CornerRadius={new UDim(0, 5)} />
				<uiflexitem FlexMode={"Fill"} />
				<Padding Padding={new UDim(0, 3)} />
			</textbox>

			<textlabel AutomaticSize={"XY"} BackgroundTransparency={1} Text={"Y"} TextColor3={theme.MainText.Default} />
			<textbox
				Text={tostring(value.Y)}
				TextColor3={theme.MainText.Default}
				AutomaticSize={"Y"}
				BackgroundColor3={theme.Dark.Default}
				TextXAlignment={"Left"}
				ClearTextOnFocus={false}
				ClipsDescendants
				Event={{
					FocusLost: (rbx) => {
						const num = tonumber(rbx.Text.gsub("[^%d%.-]", "")[0])! ?? 0;
						onChange(new Vector2(value.X, num));
					},
				}}
			>
				<uicorner CornerRadius={new UDim(0, 5)} />
				<uiflexitem FlexMode={"Fill"} />
				<Padding Padding={new UDim(0, 3)} />
			</textbox>
		</frame>
	);
}


import React, { Fragment } from "@rbxts/react";
import useTheme from "hooks/visuals/useTheme";
import Padding from "ui/components/padding";

export default function Vector3InspectorField({
	value,
	onChange,
}: {
	value: Vector3;
	onChange: (value: Vector3) => void;
}) {
	const theme = useTheme();
	return (
		<frame AutomaticSize={"XY"} BackgroundTransparency={1}>
			<uiflexitem FlexMode={"Fill"} />
			<uilistlayout FillDirection={"Horizontal"} Padding={new UDim(0, 3)} ItemLineAlignment={"Center"} />

			<textlabel AutomaticSize={"XY"} BackgroundTransparency={1} Text={"X"} TextColor3={theme.MainText.Default} />
			<textbox
				Text={tostring(value.X)}
				TextColor3={theme.MainText.Default}
				AutomaticSize={"Y"}
				BackgroundColor3={theme.Dark.Default}
				TextXAlignment={"Left"}
				ClearTextOnFocus={false}
				ClipsDescendants
				Event={{
					FocusLost: (rbx) => {
						const num = tonumber(rbx.Text.gsub("[^%d%.-]", "")[0])! ?? 0;
						onChange(new Vector3(num, value.Y, value.Z));
					},
				}}
			>
				<uicorner CornerRadius={new UDim(0, 5)} />
				<uiflexitem FlexMode={"Fill"} />
				<Padding Padding={new UDim(0, 3)} />
			</textbox>

			<textlabel AutomaticSize={"XY"} BackgroundTransparency={1} Text={"Y"} TextColor3={theme.MainText.Default} />
			<textbox
				Text={tostring(value.Y)}
				TextColor3={theme.MainText.Default}
				AutomaticSize={"Y"}
				BackgroundColor3={theme.Dark.Default}
				TextXAlignment={"Left"}
				ClearTextOnFocus={false}
				ClipsDescendants
				Event={{
					FocusLost: (rbx) => {
						const num = tonumber(rbx.Text.gsub("[^%d%.-]", "")[0])! ?? 0;
						onChange(new Vector3(value.X, num, value.Z));
					},
				}}
			>
				<uicorner CornerRadius={new UDim(0, 5)} />
				<uiflexitem FlexMode={"Fill"} />
				<Padding Padding={new UDim(0, 3)} />
			</textbox>

			<textlabel AutomaticSize={"XY"} BackgroundTransparency={1} Text={"Z"} TextColor3={theme.MainText.Default} />
			<textbox
				Text={tostring(value.Z)}
				TextColor3={theme.MainText.Default}
				AutomaticSize={"Y"}
				BackgroundColor3={theme.Dark.Default}
				TextXAlignment={"Left"}
				ClearTextOnFocus={false}
				ClipsDescendants
				Event={{
					FocusLost: (rbx) => {
						const num = tonumber(rbx.Text.gsub("[^%d%.-]", "")[0])! ?? 0;
						onChange(new Vector3(value.X, value.Y, num));
					},
				}}
			>
				<uicorner CornerRadius={new UDim(0, 5)} />
				<uiflexitem FlexMode={"Fill"} />
				<Padding Padding={new UDim(0, 3)} />
			</textbox>
		</frame>
	);
}


import { createPortal, createRoot } from "@rbxts/react-roblox";
import App from "./app";
import React, { StrictMode } from "@rbxts/react";
export function MountInspector(parent: Instance) {
    const mountRoot = createRoot(parent)
    mountRoot.render(<StrictMode>{createPortal(<App />, parent)}</StrictMode>)

    return () => mountRoot.unmount()
}


local compo = require(script.Packages.compo)
export type component<TInstance, TFields> = compo.component<TInstance, TFields>

return compo


local RunService = game:GetService("RunService")
local StarterPlayer = game:GetService("StarterPlayer")
local ServerScriptService = game:GetService("ServerScriptService")

return {
	realm = if RunService:IsServer() then ServerScriptService else StarterPlayer,
	update = if RunService:IsServer() then RunService.Heartbeat else RunService.PreRender,
	COMPONENT_ID = "_component_id",
	COMPONENT_TAG = "_compo_component:",
	COMPONENT_ACTIVE = "_component_active",
	COMPONENT_FIELD = "_component_field"
}


local types = require(script.Parent.types)

local function createComponent<TComponent>(
	transformer: (component: TComponent) -> (),
	fields: {}
): types.componentBuilder<any, any>
	return {
		constructor = function(instance: Instance, refinatedFields : any): types.component<any, any>
			local component = {
				instance = instance,
				fields = refinatedFields,
			}

			transformer(component :: any)
			return component
		end,
		fields = fields,
	}
end

return createComponent


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

local function field<T>(checker: (T) -> { any }, defaultVal: T, fieldID: number) : (val: T) -> { default : T, field : number}
	return function(val)
        if (checker(val)) then
            return { default = val, field = fieldID }
        end

        return { default = defaultVal, field = fieldID } :: any
	end
end

return {
	string = field(t.string, "", 1),
	number = field(t.number, 0, 2),
    boolean = field(t.boolean, true, 3),
    vector3 = field(t.vector, Vector3.zero, 4),
    vector2 = field(t.Vector2, Vector2.zero, 5)
}


-- hi ^.^
local types = require("@self/types")
export type component<TInstance, TFields> = types.component<TInstance, TFields>

return {
	launch = require("@self/launch"),
	createComponent = require("@self/createComponent"),
	fields = require("@self/fields")
}


local promise = require(script.Parent.Parent.promise)
local componentCollector = require("./componentCollector")
local prepareComponent = require("./prepareComponent")
local scheduler = require("./scheduler")
local _launched = false

--[=[
	Launches Compo and starts its lifecycle
	@return A promise that resolves when the components enter the start() phase
]=]
return function() : promise.Promise
	return promise
		.new(function(resolve, reject)
			if _launched then
				reject("launch() has already been called and cannot be called again!")
				return
			end

			_launched = true

			-- Registering every component into the scheduler
			for _, component in ipairs(componentCollector()) do
				prepareComponent(component)
			end

			-- Initializing scheduler (yields until start())
			scheduler.initializeLifecycle()
			resolve()
		end)
		:catch(function(e: string)
			error("[🔴FATAL] An unknown error occurred launching compo: " .. e)
		end)

end


local CollectionService = game:GetService("CollectionService")
local constants = require("./constants")
local validate = require("@self/validate")
local scheduler = require("./scheduler")
local janitor = require(script.Parent.Parent.janitor)
local t = require(script.Parent.Parent.t)

local function getRefinatedFields(fieldDefinitions: { [string]: any }, instance: Instance, id: string, janitor: janitor.Janitor)
	local fieldSuffix = constants.COMPONENT_FIELD .. id
	local fields = {}

	for attribute, value in pairs(instance:GetAttributes()) do
		-- The current attribute is not a component field
		if not attribute:match(fieldSuffix) then
			continue
		end

		local fieldName = attribute:gsub(fieldSuffix, "")
		-- The field is not an actual field
		if not fieldDefinitions[fieldName] then
			continue
		end

		fields[fieldName] = value
		janitor:Add(instance:GetAttributeChangedSignal(attribute):Connect(function()
			fields[fieldName] = instance:GetAttribute(attribute) -- This caches the value, and now we can get rid of unnecessary :GetAttribute calls
		end))
	end

	return setmetatable({}, {
		__index = function(_, index) -- Getting
			return fields[index]
		end,
		__newIndex = function(_, index, value) -- Setting
			instance:SetAttribute(fieldSuffix .. index, value)
		end
	})
end

--[=[
	Tracks the given instance.
]=]
local function manage(componentBuilder: any, id: string, tag: string, instance: Instance, name: string)
	local activeAttributeName = constants.COMPONENT_ACTIVE .. id
	local active = instance:GetAttribute(activeAttributeName)
	local trash = janitor.new()
	trash:LinkToInstance(instance)

	if not (t.boolean(active)) then
		warn(`[🟠WARNING] 'active' attribute is missing or is invalid!: {instance.Name}`)
		return
	end

	local function mount()
		-- Building the component and registering it
		local component = componentBuilder.constructor(instance, getRefinatedFields(componentBuilder.fields, instance, id, trash) :: any)
		scheduler.registerComponent(component)

		-- when trash is destroyed, unregistering the component
		trash:Add(function()
			scheduler.unregisterComponent(component)
		end)

		-- when the instance parent changes, checking if destroyed to cleanup component (link to instance already do this, but in studio if you remove an instance, it's not destroyed)
		trash:Add(instance.AncestryChanged:Connect(function(_, parent)
			if parent == nil then
				return
			end
			trash:Destroy()
		end))

		-- when tag removed, destroying component
		trash:Add(CollectionService:GetInstanceRemovedSignal(tag):Connect(function(removedInstance: Instance)
			if instance == removedInstance then
				trash:Destroy()
			end
		end))
		-- listening for active attribute changes
		trash:Add(instance:GetAttributeChangedSignal(activeAttributeName):Connect(function()
			scheduler.setComponentActive(component, instance:GetAttribute(activeAttributeName) :: boolean)
		end))
	end

	-- if the component is not active, we wait until it is active
	if not active then
		local c
		c = trash:Add(instance:GetAttributeChangedSignal(activeAttributeName):Connect(function()
			if instance:GetAttribute(activeAttributeName) then
				c:Disconnect()
				mount()
			end
		end))
		return
	end
	-- if it is already active, mounting it
	mount()
end

--[=[
	Prepares the given component (module script)
]=]
return function(component: ModuleScript)
	-- Component's id
	local id = component:GetAttribute(constants.COMPONENT_ID) :: string

	if not t.string(id) then
		-- Maybe you deleted component's id attribute
		-- or something went wrong with Compo Inspector
		warn(
			`[🟠WARNING] A component was defined with '.component' ({component.Name}), but there's no ID associated with it (or is not a string). Did you forget to add a '{constants.COMPONENT_ID}' attribute?.`
		)
		return
	end

	-- Requiring component. Returns both constructor and fields
	local s, componentBuilder = pcall(require, component)

	if not s then
		warn(`[🟠WARNING] An error ocurred loading '{component.Name}' component: {componentBuilder}`) -- componentBuilder is the error message.
		return
	end

	if not validate(componentBuilder) then
		warn(`[🟠WARNING] '{component.Name}' has a no valid component format!.`)
		return
	end

	-- Expected component tag
	local targetTag = constants.COMPONENT_TAG .. id

	-- Getting all tagged instances to run the component
	for _, instance in ipairs(CollectionService:GetTagged(targetTag)) do
		manage(componentBuilder, id, targetTag, instance, component.Name)
	end

	-- Listening to new tagged instances
	CollectionService:GetInstanceAddedSignal(targetTag):Connect(function(instance)
		manage(componentBuilder, id, targetTag, instance, component.Name)
	end)
end


local t = require(script.Parent.Parent.Parent.t)
local types = require("../types")

local IComponent = t.interface({
	constructor = t.callback,
	fields = t.optional(t.table)
})

return function(component: types.componentBuilder<any, any>)
	return IComponent(component)
end


--[=[
    Calls a function, if an error ocurred doesnt stop the thread.
    @param The callback to be called
    @return Whatever the callback returned
]=]
local function call<T...>(callable: ((T...) -> any)?, yield: boolean?, ...:T...): any
    if not callable  then return nil end
    local arg: any = {...}

    if yield then
        local s, e = pcall(callable, ...)
        if not s then warn(`[🟠WARNING] an error ocurred running component phase: {e}`) end
        return e
    else
        xpcall(function() return callable(unpack(arg)) end, function(e)
            warn(`[🟠WARNING] an error ocurred running component phase: {e}`)
        end)
        return nil
    end
end

return {
    call = call
}


--!strict

export type component<TInstance, TFields> = {
	fields: TFields,
	instance: TInstance,
	-- life cycle methods
	awake: (() -> ())?,
	onEnable: (() -> ())?,
	start: (() -> ())?,
	update: ((deltaTime: number) -> ())?,
	fixedUpdate: (() -> ())?,
	onDisable: (() -> ())?,
	onDestroy: (() -> ())?,
}

export type componentBuilder<TInstance, TFields> = {
	constructor: (instance: Instance, refinatedFields : {}) -> component<TInstance, TFields>,
	fields: {},
}

return nil


local compo = require(game.ReplicatedStorage.compo)
type component = compo.component<Part, {
	damage: number,
}>

return compo.createComponent(function(component: component)
	local c: RBXScriptConnection = nil
	local originalMaterial = component.instance.Material
	local originalColor = component.instance.Color
	local firstUpdate = false

	function component.awake()
		error("OMG SOMETHING HAPPENED")
		print("awake phase")
	end

	function component.onEnable()
		print("on enable phase")
		c = component.instance.Touched:Connect(function(hit: Instance)
			local humanoid = hit.Parent and hit.Parent:FindFirstChildOfClass("Humanoid")
			if not humanoid then
				return
			end

			humanoid.Health -= component.fields.damage
		end)

		component.instance.Color = Color3.new(1, 0, 0)
		component.instance.Material = Enum.Material.Neon
	end

	function component.start()
		print("start phase")
	end

	function component.update(d)
		if firstUpdate then
			return
		end
		firstUpdate = true
		print("update phase")
	end

	function component.onDisable()
		print("disable phase")
		if c then
			c:Disconnect()
		end
		component.instance.Material = originalMaterial
		component.instance.Color = originalColor
	end

	function component.onDestroy()
		print("on destroy phase")
	end
end, { damage = compo.fields.number(1) })


local compo = require(game:GetService("ReplicatedStorage").compo)

compo.launch():andThen(function()
    print("resolved")
end)