import { RunService } from "@rbxts/services";
import { MultiBlockConfigControl } from "client/gui/BlockConfigControls";
import { ConfirmPopup } from "client/gui/popup/ConfirmPopup";
import { ScaledScreenGui } from "client/gui/ScaledScreenGui";
import { Element } from "engine/shared/Element";
import { RocketBlocks } from "shared/blocks/blocks/RocketEngineBlocks";
import { BlockManager } from "shared/building/BlockManager";
import type { MainScreenLayout } from "client/gui/MainScreenLayout";
import type { PopupController } from "client/gui/PopupController";
import type { BuildingMode } from "client/modes/build/BuildingMode";
import type { ToolController } from "client/tools/ToolController";
import type { TutorialDescription } from "client/tutorial2/TutorialDescription";
import type { TutorialStarter } from "client/tutorial2/TutorialStarter";
import type { Component } from "engine/shared/component/Component";

const start = (tutorial: TutorialStarter, firstTime: boolean) => {
	tutorial.$onInjectAuto(
		(
			mainScreen: MainScreenLayout,
			toolController: ToolController,
			buildingMode: BuildingMode,
			popupController: PopupController,
			di: DIContainer,
		) => {
			const tc = tutorial.controller;
			const plot = tutorial.plot;
			const step = tutorial.stepController;

			const gui = tc.gui;
			const tools = buildingMode.tools;

			if (firstTime) {
				gui.progress.setStopAction((stop) => {
					const popup = popupController.showPopup(
						new ConfirmPopup("Are you sure you want to skip the tutorial?", "You DIE if you do that", stop),
					);

					// fix for tutorial fullscreen fade going over popups
					popup.instance.DisplayOrder = gui.instance.DisplayOrder + 1;
				});
			}

			gui.progress.setTitle("Basics tutorial");
			gui.progress.setText("Teaching about basics of the gaming");
			if (RunService.IsStudio()) {
				gui.progress.enableSkip();
			}

			// intro
			step.step((parent, finish) => {
				parent.parent(tc.disableAllInput());
				parent.parentFunc(
					() => toolController.enabledTools.disableAll(),
					() => toolController.enabledTools.enableAll(),
				);
				parent.parent(gui.createFullScreenFade());
				parent.parent(
					gui
						.createText() //
						.withText("Hi engineer! I am play engineers and i'll teach you how to engineer")
						.withText("Click NEXT to CONTINUE")
						.withText("Or click big red STOP to SKIP the tutorial but then you will DIE")
						.withNext(finish),
				);
			});

			// select build tool
			step.sequence()
				.withOnStart(() => toolController.enabledTools.enableOnly(buildingMode.tools.buildTool))
				.withOnEnd(() => toolController.enabledTools.enableAll())
				.withOnStart(() => buildingMode.tools.buildTool.gui.blockSelector.highlightedBlocks.set(["block"]))
				.withOnEnd(() => buildingMode.tools.buildTool.gui.blockSelector.highlightedBlocks.set([]))

				.conditional({
					condition: () => toolController.selectedTool.get() === buildingMode.tools.buildTool,
					run: (parent) => {
						parent.parent(tc.disableAllInputExcept([Enum.KeyCode.One]));
						parent.parent(gui.createFullScreenFadeWithHoleAround(mainScreen.hotbar.instance, Vector2.zero));
						parent.parent(
							gui
								.createText()
								.withPositionAround(mainScreen.hotbar.instance, "up")
								.withText("This is your TOOLBAR")
								.withText("Your TOOLS are here")
								.withText("look CAREFOULY then press BUILD TOOL which is the first one")
								.withText("or key 1 on keyboard or whatevber idk on console"),
						);
					},
				})
				.conditional({
					condition: () =>
						tools.buildTool.gui.blockSelector.selectedCategory.get().sequenceEquals(["Blocks"]),
					run: (parent) => {
						parent.parent(tc.disableAllInput());
						parent.parent(
							gui.createFullScreenFadeWithHoleAround(tools.buildTool.gui.blockSelector.instance),
						);
						parent.parent(
							gui
								.createText()
								.withPositionAround(tools.buildTool.gui.blockSelector.instance, "right")
								.withText("to build you need blocks This is a block list it has blocks")
								.withText("select cateegory 'blocks'")
								.withText("there category bocks"),
						);
					},
				})
				.conditional({
					condition: () => tools.buildTool.selectedBlock.get()?.id === "block",
					run: (parent) => {
						parent.parent(tc.disableAllInput());
						parent.parent(
							gui.createFullScreenFadeWithHoleAround(tools.buildTool.gui.blockSelector.instance),
						);
						parent.parent(
							gui
								.createText()
								.withPositionAround(tools.buildTool.gui.blockSelector.instance, "right")
								.withText("good, NOW")
								.withText("select bock BLOCK"),
						);
					},
				});

			// place block
			step.step((parent, finish) => {
				parent.parentFunc(
					() => toolController.enabledTools.enableOnly(buildingMode.tools.buildTool),
					() => toolController.enabledTools.enableAll(),
				);
				parent.parent(
					gui
						.createText() //
						.withText("Now using your BUILD TOOL and your BLOCKS.BLOCK, place a BLOCK in the HIGHGLITH"),
				);

				parent.parent(
					plot.processDiff(
						{
							version: 32,
							added: [
								{ uuid: "b0" as BlockUuid, id: "block", location: new CFrame(0, 1.5, 0) },
								{ uuid: "b1" as BlockUuid, id: "block", location: new CFrame(2, 1.5, 0) },
							],
						},
						finish,
					),
				);
			});

			// select delete tool
			step.conditional({
				condition: () => toolController.selectedTool.get() === buildingMode.tools.deleteTool,
				run: (parent) => {
					parent.parentFunc(
						() =>
							toolController.enabledTools.enableOnly(
								buildingMode.tools.buildTool,
								buildingMode.tools.deleteTool,
							),
						() => toolController.enabledTools.enableAll(),
					);
					parent.parent(tc.disableAllInputExcept([Enum.KeyCode.One, Enum.KeyCode.Three]));
					parent.parent(gui.createFullScreenFadeWithHoleAround(mainScreen.hotbar.instance, Vector2.zero));
					parent.parent(plot.disableBuilding());
					parent.parent(
						gui
							.createText() //
							.withText("good. NO, WRONG. you are WRONG. you should NOT HAVE placed that BLOCK.")
							.withText("lets' DESTROY IT")
							.withText("SELECT delete TOOL")
							.withText("it deletes BLOCKS but not your debt"),
					);
				},
			});

			// delete block
			step.step((parent, finish) => {
				parent.parentFunc(
					() =>
						toolController.enabledTools.enableOnly(
							buildingMode.tools.buildTool,
							buildingMode.tools.deleteTool,
						),
					() => toolController.enabledTools.enableAll(),
				);
				parent.parent(plot.disableBuilding());
				parent.parent(
					gui
						.createText() //
						.withText("destroy the IMPOSTER")
						.withText("(hes highlighted red, you see)")
						.withText("(sus or something)")
						.withText("(delete him before he vents)"),
				);

				parent.parent(plot.processDiff({ version: 32, removed: ["b0" as BlockUuid] }, finish));
			});

			// place rocket block
			step.step((parent, finish) => {
				parent.parentFunc(
					() =>
						toolController.enabledTools.enableOnly(
							buildingMode.tools.buildTool,
							buildingMode.tools.deleteTool,
						),
					() => toolController.enabledTools.enableAll(),
				);
				parent.parent(plot.disableDeleting());
				parent.parent(
					gui
						.createText() //
						.withText("okay now the ROCKET, it makes FLY"),
				);

				parent.parent(
					plot.processDiff(
						{
							version: 32,
							added: [
								{
									uuid: "rocket0" as BlockUuid,
									id: "smallrocketengine",
									location: new CFrame(5, 1.5, 0),
								},
							],
						},
						finish,
					),
				);
			});

			// select config tool
			step.conditional({
				condition: () => toolController.selectedTool.get() === buildingMode.tools.configTool,
				run: (parent) => {
					parent.parentFunc(
						() =>
							toolController.enabledTools.enableOnly(
								buildingMode.tools.buildTool,
								buildingMode.tools.deleteTool,
								buildingMode.tools.configTool,
							),
						() => toolController.enabledTools.enableAll(),
					);
					parent.parent(tc.disableAllInputExcept([Enum.KeyCode.One, Enum.KeyCode.Three, Enum.KeyCode.Four]));
					parent.parent(gui.createFullScreenFadeWithHoleAround(mainScreen.hotbar.instance, Vector2.zero));
					parent.parent(plot.disableBuilding());
					parent.parent(plot.disableDeleting());
					parent.parent(
						gui
							.createText() //
							.withText("now THIRD TOOL is the CONFIG TOOL"),
					);
				},
			});

			/** @deprecated */
			const createConfigExample = (parent: Component) => {
				const exampleGui = Element.create(
					"ScreenGui",
					{
						Parent: gui.instance,
					},
					{
						frame: Element.create(
							"Frame",
							{
								Position: new UDim2(0.3, 0, 0.033, 0),
								Size: new UDim2(0, 324, 0, 600),
								BackgroundColor3: Color3.fromRGB(1, 4, 9),
								BackgroundTransparency: 0.5,
								Interactable: false,
							},
							{
								padding: Element.create("UIPadding", {
									PaddingBottom: new UDim(0, 5),
									PaddingLeft: new UDim(0, 5),
									PaddingRight: new UDim(0, 2),
									PaddingTop: new UDim(0, 5),
								}),
								list: Element.create("UIListLayout", { Padding: new UDim(0, 5) }),
							},
						),
					},
				);

				const configPreview = di.resolveForeignClass(MultiBlockConfigControl, [
					exampleGui.frame,
					RocketBlocks[0].logic!.definition.input,
					{
						["rocket0" as BlockUuid]: {
							thrust: {
								controlConfig: {
									enabled: true,
									mode: { type: "instant", instant: { mode: "onRelease" } },
									keys: [
										{ key: "R", value: 100 },
										{ key: "F", value: 0 },
									],
								},
							},
						},
					} as never,
					undefined,
					new Map(),
				]);
				parent.parent(configPreview);
				parent.parent(new ScaledScreenGui(exampleGui));

				return $tuple(configPreview, exampleGui);
			};

			// config tool explanation
			step.sequence()
				.withOnStart(() =>
					toolController.enabledTools.enableOnly(
						buildingMode.tools.buildTool,
						buildingMode.tools.deleteTool,
						buildingMode.tools.configTool,
					),
				)
				.withOnEnd(() => toolController.enabledTools.enableAll())
				.withOnStart((parent) => parent.parent(plot.disableBuilding()))
				.withOnStart((parent) => parent.parent(plot.disableDeleting()))
				.withOnStart(() => (buildingMode.tools.configTool.gui.configContainer.instance.Interactable = false))
				.withOnEnd(() => (buildingMode.tools.configTool.gui.configContainer.instance.Interactable = true))
				.conditional({
					condition: () => {
						const sel = buildingMode.tools.configTool.selected.get();
						return sel.size() === 1 && BlockManager.manager.uuid.get(sel.first()!) === "rocket0";
					},
					run: (parent) => {
						parent.event.subscribeRegistration(() => plot.plot.highlight(["rocket0" as BlockUuid]));
						parent.parent(
							gui
								.createText() //
								.withText("ok now i teach you the CONFIGURATION WINDOW")
								.withText("SELECT ROCKET BLOCK"),
						);
					},
				})
				.step((parent, finish) => {
					parent.parent(
						gui.createFullScreenFadeWithHoleAround(
							buildingMode.tools.configTool.gui.configContainer.instance,
						),
					);

					parent.parent(
						gui
							.createText() //
							.withText("ok now i teach you the CONFIGURATION WINDOW")
							.withText("THIS is CONFIGURATION WINDOW")
							.withText("it makes CONFIGURATION WINDOW")
							.withText("yiu CONFIGURE in this WINDOW")
							.withText("-the blocks")
							.withNext(finish),
					);
				})
				.step((parent, finish) => {
					parent.parent(
						gui.createFullScreenFadeWithHoleAround(
							buildingMode.tools.configTool.gui.configContainer.instance.FindFirstChild(
								"MultiKeys",
								true,
							) as GuiObject,
						),
					);

					parent.parent(
						gui
							.createText() //
							.withText("this configurable thing is KEY control")
							.withNext(finish),
					);
				})
				.step((parent, finish) => {
					parent.parent(
						gui.createFullScreenFadeWithHoleAround(
							buildingMode.tools.configTool.gui.configContainer.instance
								.FindFirstChild("MultiKeys", true)!
								.FindFirstChild("Template", true)!
								.FindFirstChild("Button", true) as GuiObject,
							new Vector2(4, 4),
						),
					);

					parent.parent(
						gui
							.createText() //
							.withText("look this configurable thing is KEY control")
							.withText("when you presst this key...")
							.withNext(finish),
					);
				})
				.step((parent, finish) => {
					parent.parent(
						gui.createFullScreenFadeWithHoleAround(
							buildingMode.tools.configTool.gui.configContainer.instance
								.FindFirstChild("MultiKeys", true)!
								.FindFirstChild("Template", true)!
								.FindFirstChild("Number", true) as GuiObject,
							new Vector2(4, 4),
						),
					);

					parent.parent(
						gui
							.createText() //
							.withText("look this configurable thing is KEY control")
							.withText("when you presst this key the number MOVE to the number")
							.withNext(finish),
					);
				})
				.step((parent, finish) => {
					buildingMode.tools.configTool.gui.configContainer.instance.Interactable = true;
					parent.parent(
						gui.createFullScreenFadeWithHoleAround(
							buildingMode.tools.configTool.gui.configContainer.instance.FindFirstChild(
								"MultiKeys",
								true,
							) as GuiObject,
						),
					);

					parent.parent(
						gui
							.createText() //
							.withText("we'll use this to control our plane thrust")
							.withText("so configure")
							.withText("key R = 100%")
							.withText("key F = 0%")
							.withText("// TODO: doesn't work on mobile probably"),
					);

					parent.parent(
						plot.processDiff(
							{
								version: 32,
								configChanged: {
									["rocket0" as BlockUuid]: {
										thrust: {
											controlConfig: {
												enabled: true,
												keys: [
													{ key: "R", value: 100 },
													{ key: "F", value: 0 },
												],
											},
										},
									},
								},
							},
							finish,
						),
					);
				})
				.step((parent, finish) => {
					parent.parent(
						gui.createFullScreenFadeWithHoleAround(
							buildingMode.tools.configTool.gui.configContainer.instance //
								.FindFirstChild("Smooth change", true) as GuiObject,
						),
					);

					parent.parent(
						plot.processDiff(
							{
								version: 32,
								configChanged: {
									["rocket0" as BlockUuid]: {
										thrust: {
											controlConfig: {
												enabled: true,
												mode: {
													type: "instant",
													instant: { mode: "onRelease" },
												},
												keys: [
													{ key: "R", value: 100 },
													{ key: "F", value: 0 },
												],
											},
										},
									},
								},
							},
							finish,
						),
					);

					parent.parent(
						gui
							.createText() //
							.withText("this thing controls whether number goes smoothly or instant")
							.withText("disable IT"),
					);
				});

			//

			// end
			step.step((parent, finish) => {
				parent.parent(gui.createFullScreenFade());
				parent.parent(
					gui
						.createText() //
						.withText("damn good work")
						.withNext(finish),
				);
			});

			tutorial.start();
		},
	);
};

export const TestTutorial = {
	name: "Basics",
	description: "Teaching basics of the game by building a simple plane",
	start,
} satisfies TutorialDescription;


import { ContextActionService, UserInputService } from "@rbxts/services";
import { ObservableMap } from "engine/shared/event/ObservableMap";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { Signal } from "engine/shared/event/Signal";
import { Keys } from "engine/shared/fixes/Keys";

type KeybindSubscription = {
	readonly func: (input: InputObject) => Enum.ContextActionResult | Enum.ContextActionResult["Name"];
	readonly connection: SignalConnection;
};

export type { KeybindRegistration };
class KeybindRegistration {
	private readonly indices: number[] = [];
	private readonly subscriptions: {
		[k in Enum.UserInputState["Name"]]?: { [k in number]?: Set<KeybindSubscription> };
	} = {};
	private keys: readonly KeyCombination[];

	private readonly _isPressed = new ObservableValue(false);
	readonly isPressed = this._isPressed.asReadonly();

	constructor(
		readonly action: string,
		readonly displayPath: readonly string[],
		defaultKeys: readonly KeyCombination[],
	) {
		this.keys = defaultKeys;
		this.register();

		this.onDown(() => {
			this._isPressed.set(true);
			return "Pass";
		});
		this.onUp(() => {
			this._isPressed.set(false);
			return "Pass";
		});
	}

	private register() {
		ContextActionService.UnbindAction(this.action);
		ContextActionService.BindAction(
			this.action,
			(name, state, input) => {
				if (name !== this.action) return;

				const process = (subs: { [x: number]: Set<KeybindSubscription> | undefined }) => {
					for (const k of [...this.indices]) {
						if (!subs[k]) continue;

						for (const { func } of [...subs[k]]) {
							const result = func(input);
							if (result === Enum.ContextActionResult.Sink || result === "Sink") {
								return Enum.ContextActionResult.Sink;
							}
						}
					}
				};

				const anyPressed = this.keys.any((comb) => comb.all((k) => UserInputService.IsKeyDown(k)));
				if (!anyPressed) {
					if (input.UserInputState === Enum.UserInputState.End) {
						const result = process(this.subscriptions[input.UserInputState.Name] ?? []);
						if (result) return result;
					}

					return Enum.ContextActionResult.Pass;
				}

				const result = process(this.subscriptions[input.UserInputState.Name] ?? {});
				if (result) return result;

				return Enum.ContextActionResult.Pass;
			},
			false,
			...this.keys.flatmap((k) => k.map((k) => Keys.Keys[k])),
		);
	}

	getKeys(): readonly KeyCombination[] {
		return this.keys;
	}
	setKeys(keys: readonly KeyCombination[]) {
		this.keys = keys;
		this.register();
	}

	onDown(func: KeybindSubscription["func"], priority?: number): SignalConnection {
		return this.on(Enum.UserInputState.Begin, func, priority);
	}
	onUp(func: KeybindSubscription["func"], priority?: number): SignalConnection {
		return this.on(Enum.UserInputState.End, func, priority);
	}
	private on(state: Enum.UserInputState, func: KeybindSubscription["func"], priority?: number): SignalConnection {
		priority ??= 0;

		const subs = (this.subscriptions[state.Name] ??= {});

		const connection = Signal.connection(() => subs[priority]?.delete(sub));
		const sub = { func, connection };

		if (!subs[priority]) {
			this.indices.push(priority);
			this.indices.sort();
		}
		(subs[priority] ??= new Set()).add(sub);

		return connection;
	}
}

export type KeyCombination = readonly KeyCode[];
export interface KeybindDefinition {
	readonly action: string;
	readonly displayPath: readonly string[];
	readonly keys: readonly KeyCombination[];
}

export class Keybinds {
	private static readonly definitions = new ObservableMap<string, KeybindDefinition>();

	static registerDefinition(
		action: string,
		displayPath: readonly string[],
		keys: readonly KeyCombination[],
	): KeybindDefinition {
		let definition = this.definitions.get(action);
		if (!definition) {
			this.definitions.set(action, (definition = { action, displayPath, keys }));
		}

		return definition;
	}

	private readonly _registrations = new ObservableMap<string, KeybindRegistration>();
	readonly registrations = this._registrations.asReadonly();

	fromDefinition({ action, displayPath, keys }: KeybindDefinition): KeybindRegistration {
		return this.register(action, displayPath, keys);
	}

	register(action: string, displayPath: readonly string[], keys: readonly KeyCombination[]): KeybindRegistration {
		let registration = this._registrations.get(action);
		if (!registration) {
			this._registrations.set(action, (registration = new KeybindRegistration(action, displayPath, keys)));
		}

		return registration;
	}

	get(action: string): KeybindRegistration {
		const registration = this._registrations.get(action);
		if (!registration) {
			throw `Unknown registration ${action}`;
		}

		return registration;
	}
}


import { Players } from "@rbxts/services";
import { LocalPlayer } from "engine/client/LocalPlayer";
import { ComponentKeyedChildren } from "engine/shared/component/ComponentKeyedChildren";
import { HostedService } from "engine/shared/di/HostedService";
import type { PlayerInventoryItem } from "engine/client/PlayerInventoryItem";

@injectable
export class PlayerInventory extends HostedService {
	private readonly registeredTools = new Map<string, (instance: Tool) => PlayerInventoryItem>();
	private readonly items = this.parentDestroyOnly(new ComponentKeyedChildren<Instance, PlayerInventoryItem>());

	constructor() {
		super();

		const add = (tool: Instance) => {
			if (!tool.IsA("Tool")) return;
			if (this.items.getAll().has(tool)) return;

			const registered = this.registeredTools.get(tool.Name);
			if (!registered) return;

			const controller = registered(tool);
			this.items.add(tool, controller);
		};

		// backpack is recreated every time a Character is created
		this.event.subscribeObservable(
			LocalPlayer.character,
			() => {
				const backpack = Players.LocalPlayer.WaitForChild("Backpack");
				backpack.ChildAdded.Connect(add);
				for (const child of backpack.GetChildren()) {
					add(child);
				}
			},
			true,
		);
	}

	registerTool(id: string, func: (instance: Tool) => PlayerInventoryItem) {
		this.registeredTools.set(id, func);
	}
	registerToolClass<TArgs extends unknown[]>(
		id: string,
		di: DIContainer,
		clazz: ConstructorOf<PlayerInventoryItem, [Tool, ...TArgs]>,
		args?: Partial<TArgs>,
	) {
		this.registerTool(id, (instance) => di.resolveForeignClass(clazz, [instance, ...(args ?? [])]));
	}
}


import { UserInputService } from "@rbxts/services";
import { GlobalInputHandler } from "engine/client/event/GlobalInputHandler";
import { ThinSignalWrapper } from "engine/client/event/SignalWrapper";
import type { ISignalWrapper } from "engine/client/event/SignalWrapper";

type InputCallback = (input: InputObject) => void;
type FullInputCallback = (input: InputObject, gameProcessedEvent: boolean) => void;
type TouchCallback = (inputPositions: readonly Vector2[], gameProcessedEvent: boolean) => void;

const filterMouse1 = (callback: InputCallback, allowGameProcessedEvents: boolean): FullInputCallback => {
	return (input, gameProcessedEvent) => {
		if (!allowGameProcessedEvents && gameProcessedEvent) return;
		if (input.UserInputType !== Enum.UserInputType.MouseButton1) return;
		callback(input);
	};
};
const filterMouse2 = (callback: InputCallback, allowGameProcessedEvents: boolean): FullInputCallback => {
	return (input, gameProcessedEvent) => {
		if (!allowGameProcessedEvents && gameProcessedEvent) return;
		if (input.UserInputType !== Enum.UserInputType.MouseButton2) return;
		callback(input);
	};
};
const filterMouse3 = (callback: InputCallback, allowGameProcessedEvents: boolean): FullInputCallback => {
	return (input, gameProcessedEvent) => {
		if (!allowGameProcessedEvents && gameProcessedEvent) return;
		if (input.UserInputType !== Enum.UserInputType.MouseButton3) return;
		callback(input);
	};
};

const keyPressed = new Map<KeyCode, Map<InputHandler, Callback[]>>();
const keyReleased = new Map<KeyCode, Map<InputHandler, Callback[]>>();
const maps = new Set<Map<InputHandler, Callback[]>>();

const inputBegan = GlobalInputHandler.inputBegan;
const inputChanged = GlobalInputHandler.inputChanged;
const inputEnded = GlobalInputHandler.inputEnded;
const touchTap = GlobalInputHandler.touchTap;

{
	const subKeys = (
		signal: ISignalWrapper<[input: InputObject, gameProcessedEvent: boolean]>,
		map: ReadonlyMap<KeyCode, Map<InputHandler, Callback[]>>,
	) => {
		signal.subscribe((input, gameProcessedEvent) => {
			if (gameProcessedEvent) return;
			if (input.KeyCode === Enum.KeyCode.Unknown) return;

			const eventmap = map.get(input.KeyCode.Name);
			if (!eventmap) return;

			for (const [_, events] of [...eventmap]) {
				for (const event of [...events]) {
					event(input, gameProcessedEvent);
				}
			}
		});
	};
	subKeys(inputBegan, keyPressed);
	subKeys(inputEnded, keyReleased);
}

export type ReadonlyInputHandler = Pick<InputHandler, Exclude<keyof InputHandler, "destroy" | "unsubscribeAll">>;
export class InputHandler {
	private inputBegan?: ThinSignalWrapper<Parameters<FullInputCallback>>;
	private inputChanged?: ThinSignalWrapper<Parameters<FullInputCallback>>;
	private inputEnded?: ThinSignalWrapper<Parameters<FullInputCallback>>;
	private touchTap?: ThinSignalWrapper<Parameters<TouchCallback>>;

	onInputBegan(callback: FullInputCallback) {
		(this.inputBegan ??= new ThinSignalWrapper(inputBegan)).subscribe(callback);
	}
	onInputChanged(callback: FullInputCallback) {
		(this.inputChanged ??= new ThinSignalWrapper(inputChanged)).subscribe(callback);
	}
	onInputEnded(callback: FullInputCallback) {
		(this.inputEnded ??= new ThinSignalWrapper(inputEnded)).subscribe(callback);
	}
	onTouchTap(callback: TouchCallback, allowGameProcessedEvents = true) {
		if (!allowGameProcessedEvents) {
			(this.touchTap ??= new ThinSignalWrapper(touchTap)).subscribe((positions, gameProcessedEvent) => {
				if (gameProcessedEvent) return;
				callback(positions, gameProcessedEvent);
			});
		} else {
			(this.touchTap ??= new ThinSignalWrapper(touchTap)).subscribe(callback);
		}
	}

	onKeyDown(key: KeyCode, callback: () => void, executeImmediately: true): void;
	onKeyDown(key: KeyCode, callback: InputCallback | (() => void)): void;
	onKeyDown(key: KeyCode, callback: InputCallback | (() => void), executeImmediately = false): void {
		let map = keyPressed.get(key);
		if (!map) keyPressed.set(key, (map = new Map()));
		maps.add(map);

		let selfmap = map.get(this);
		if (!selfmap) map.set(this, (selfmap = []));

		selfmap.push(callback);

		if (executeImmediately) {
			if (UserInputService.IsKeyDown(key)) {
				callback(undefined!);
			}
		}
	}
	onKeyUp(key: KeyCode, callback: InputCallback) {
		let map = keyReleased.get(key);
		if (!map) keyReleased.set(key, (map = new Map()));
		maps.add(map);

		let selfmap = map.get(this);
		if (!selfmap) map.set(this, (selfmap = []));

		selfmap.push(callback);
	}

	onMouse1Down(callback: InputCallback, allowGameProcessedEvents: boolean) {
		this.onInputBegan(filterMouse1(callback, allowGameProcessedEvents));
	}
	onMouse1Up(callback: InputCallback, allowGameProcessedEvents: boolean) {
		this.onInputEnded(filterMouse1(callback, allowGameProcessedEvents));
	}
	onMouse2Down(callback: InputCallback, allowGameProcessedEvents: boolean) {
		this.onInputBegan(filterMouse2(callback, allowGameProcessedEvents));
	}
	onMouse2Up(callback: InputCallback, allowGameProcessedEvents: boolean) {
		this.onInputEnded(filterMouse2(callback, allowGameProcessedEvents));
	}
	onMouse3Down(callback: InputCallback, allowGameProcessedEvents: boolean) {
		this.onInputBegan(filterMouse3(callback, allowGameProcessedEvents));
	}
	onMouse3Up(callback: InputCallback, allowGameProcessedEvents: boolean) {
		this.onInputEnded(filterMouse3(callback, allowGameProcessedEvents));
	}
	onMouseMove(callback: InputCallback, allowGameProcessedEvents: boolean) {
		this.onInputChanged((input, gameProcessedEvent) => {
			if (!allowGameProcessedEvents && gameProcessedEvent) return;
			if (input.UserInputType !== Enum.UserInputType.MouseMovement) return;
			callback(input);
		});
	}

	unsubscribeAll() {
		this.inputBegan?.unsubscribeAll();
		this.inputChanged?.unsubscribeAll();
		this.inputEnded?.unsubscribeAll();
		this.touchTap?.unsubscribeAll();

		for (const map of maps) {
			map.delete(this);
		}
	}

	destroy() {
		this.unsubscribeAll();

		this.inputBegan?.destroy();
		this.inputChanged?.destroy();
		this.inputEnded?.destroy();
		this.touchTap?.destroy();
	}
}


import { ReplicatedStorage, ContentProvider } from "@rbxts/services";
import { Component } from "engine/shared/component/Component";
import { Element } from "engine/shared/Element";
import { ArgsSignal } from "engine/shared/event/Signal";
import type { ButtonDefinition } from "engine/client/gui/Button";
import type { InstanceComponent } from "engine/shared/component/InstanceComponent";

const clickSound = Element.create("Sound", {
	Name: "Click",
	SoundId: "rbxassetid://876939830",
	Parent: ReplicatedStorage,
});
task.spawn(() => ContentProvider.PreloadAsync([clickSound]));

/** Component that handles {@link GuiButton.Activated} event and provides a signal for it. */
export class ButtonComponent extends Component {
	readonly activated: ReadonlyArgsSignal;

	constructor(parent: InstanceComponent<ButtonDefinition>) {
		super();

		const activated = new ArgsSignal();
		this.activated = activated;

		const silent = parent.getAttribute<boolean>("silent") === true;
		this.event.subscribe(parent.instance.Activated, () => {
			if (!silent) clickSound.Play();
			activated.Fire();
		});
	}
}


import { Players, RunService, UserInputService } from "@rbxts/services";
import { ServiceIntegrityChecker } from "client/integrity/ServiceIntegrityChecker";
import { AutoUIScaledComponent } from "engine/client/gui/AutoUIScaledControl";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import { Colors } from "engine/shared/Colors";
import { Component } from "engine/shared/component/Component";
import { InstanceComponent } from "engine/shared/component/InstanceComponent";
import { Transforms } from "engine/shared/component/Transforms";
import { Element } from "engine/shared/Element";
import { ObservableCollectionArr } from "engine/shared/event/ObservableCollection";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import type { TransformProps } from "engine/shared/component/Transform";
import type { ReadonlyObservableValue } from "engine/shared/event/ObservableValue";

const screen = Element.create("ScreenGui", {
	Name: "TooltipScreen",
	ResetOnSpawn: false,
	IgnoreGuiInset: true,
	ClipToDeviceSafeArea: false,
	SafeAreaCompatibility: Enum.SafeAreaCompatibility.None,
	ScreenInsets: Enum.ScreenInsets.None,
	DisplayOrder: 99999,
	Parent: Players.LocalPlayer.FindFirstChildOfClass("PlayerGui")!,
});
ServiceIntegrityChecker.whitelistInstance(screen);

type TooltipDefinition = GuiObject & {
	readonly TextLabel: TextLabel;
};
const tooltipTemplate = Control.asTemplateWithMemoryLeak(
	Interface.getTemplates<{ Tooltip: TooltipDefinition }>().Tooltip,
	false,
);

class Tooltip extends InstanceComponent<TooltipDefinition> {
	readonly text = new ObservableValue<string | undefined>(undefined);

	constructor() {
		super(tooltipTemplate());

		this.instance.Visible = false;
		this.instance.BackgroundTransparency = 1;
		this.instance.TextLabel.TextTransparency = 1;

		this.instance.Parent = screen;
		this.getComponent(AutoUIScaledComponent);

		this.subscribeVisibilityFrom({
			main: this.enabledState,
			hasText: this.text.createBased((t) => t !== undefined && t.size() !== 0),
		});

		this.visibilityComponent().addTransformFunc((visible) =>
			Transforms.create() //
				.wait(visible ? 0.4 : 0.1)
				.then()
				.transform(this.instance, "BackgroundTransparency", visible ? 0 : 1, Transforms.quadOut02)
				.transform(this.instance.TextLabel, "TextTransparency", visible ? 0 : 1, Transforms.quadOut02),
		);

		{
			const bg = this.valuesComponent().get("BackgroundColor3");
			const flash = new ObservableValue(0);
			this.event.subscribeObservable(flash, () => {
				bg.effect("flash", (color) => color.Lerp(Colors.white, flash.get()), 999999);
			});

			this.event.subscribeObservable(
				this.text,
				(text) => {
					if (!text || text.size() === 0) return;

					this.instance.TextLabel.Text = text;

					Transforms.create() //
						.transformObservable(flash, 0.2)
						.transformObservable(flash, 0, { duration: 0.5 })
						.run(flash, true);
				},
				true,
			);
		}

		const getMousePosition = () => {
			const mousePos = UserInputService.GetMouseLocation();
			return new UDim2(0, mousePos.X + 10, 0, mousePos.Y - 20);
		};
		const moveToMouse = (props?: TransformProps) => {
			Transforms.create() //
				.move(this.instance, getMousePosition(), props)
				.run(this, true);
		};

		const props: TransformProps = { ...Transforms.quadOut02, duration: 0.1 };
		this.event.subscribe(RunService.Heartbeat, () => {
			if (!this.instance.Visible) return;
			moveToMouse(props);
		});
		this.event.subscribeObservable(
			this.event.observableFromInstanceParam(this.instance, "Visible"),
			(visible) => {
				if (!visible) return;
				moveToMouse();
			},
			true,
		);
	}
}

class TooltipController extends Component {
	private readonly tooltip;
	private readonly hovered = new ObservableCollectionArr<TooltipComponent>();

	constructor() {
		super();
		this.tooltip = this.parent(new Tooltip());

		this.event.subscribeObservable(
			this.hovered,
			() => this.tooltip.text.set(this.hovered.get()[this.hovered.size() - 1]?.text.get()),
			true,
		);
	}

	init(component: TooltipComponent): void {
		component.hovered.subscribe((hovered) => {
			if (hovered) {
				this.hovered.add(component);
			} else {
				this.hovered.remove(component);
			}
		});
	}
}

const controller = new TooltipController();
controller.enable();

export class TooltipComponent extends Component {
	readonly hovered: ReadonlyObservableValue<boolean>;
	readonly text = new ObservableValue<string | undefined>(undefined);

	constructor(parent: InstanceComponent<GuiObject>) {
		super();

		const hovered = new ObservableValue(false);
		this.hovered = hovered;

		const start = () => hovered.set(true);
		const stop = () => hovered.set(false);

		this.event.subscribe(parent.instance.MouseEnter, start);
		this.event.subscribe(parent.instance.MouseLeave, stop);
		this.onDisable(stop);

		controller.init(this);
	}
}


import { DataStoreService } from "@rbxts/services";
import { formatDatabaseBackendKeys } from "engine/server/backend/DatabaseBackend";
import { Element } from "engine/shared/Element";
import { JSON } from "engine/shared/fixes/Json";
import type { DatabaseBackend } from "engine/server/backend/DatabaseBackend";

const getOptions = Element.create("DataStoreGetOptions", { UseCache: false });

export class DataStoreDatabaseBackend<T> implements DatabaseBackend<T, defined[]> {
	static tryCreate<T>(name: string): DataStoreDatabaseBackend<T> | undefined {
		try {
			return new DataStoreDatabaseBackend(DataStoreService.GetDataStore(name));
		} catch {
			return undefined;
		}
	}

	constructor(private readonly dataStore: DataStore) {}

	GetAsync(keys: defined[]): T | undefined {
		const str = this.dataStore.GetAsync<string>(formatDatabaseBackendKeys(keys), getOptions)[0];
		return str ? (JSON.deserialize(str) as T) : undefined;
	}
	SetAsync(value: T, keys: defined[]): void {
		this.dataStore.SetAsync(formatDatabaseBackendKeys(keys), JSON.serialize(value));
	}
	RemoveAsync(keys: defined[]): void {
		this.dataStore.RemoveAsync(formatDatabaseBackendKeys(keys));
	}
}


import { RunService } from "@rbxts/services";
import { HostedService } from "engine/shared/di/HostedService";
import { BlockManager } from "shared/building/BlockManager";
import { RemoteEvents } from "shared/RemoteEvents";
import { TagUtils } from "shared/utils/TagUtils";
import type { PlayerDataStorage } from "client/PlayerDataStorage";
import type { SparksEffect } from "shared/effects/SparksEffect";

export type damageType = Partial<{
	//ansolute units
	heatDamage: number;
	impactDamage: number;
	explosiveDamage: number;
}>;

type health = number;

const blockMaxHealthList = new Map<BlockModel, health>();
const blockHealthList = new Map<BlockModel, health>();

const checkIfCanBeUnwelded = (damage: number, blockHealth: number) => blockHealth > 0 && damage > blockHealth;
const checkIfCanBeDestroyed = (damage: number, blockHealth: number) => blockHealth > 0 && damage > blockHealth * 2;
const testYourLuck = (num: number): boolean => math.random() < num;
const explode = (part: BasePart, radius: number) =>
	RemoteEvents.Explode.send({
		part,
		radius,
		pressure: 1,
		isFlammable: false,
	});

// // handle block health init here
// if (RunService.IsServer()) {
// 	Players.PlayerAdded.Connect((p) => {
// 		const dmg = blockHealthList.map((block, health) => ({ block, health }));
// 		CustomRemotes.damageSystem.healthInit.send(p, dmg);
// 	});
// }

// if (RunService.IsClient()) {
// 	CustomRemotes.damageSystem.healthInit.invoked.Connect((arr) => {
// 		for (const v of arr) blockHealthList.set(v.block, v.health);
// 	});
// }

/* hi
                0   0
                |   |
            ____|___|____
         0  |~ ~ ~ ~ ~ ~|   0
         |  |           |   |
      ___|__|___________|___|__
      |/\/\/\/\/\/\/\/\/\/\/\/|
  0   |       H a p p y       |   0
  |   |/\/\/\/\/\/\/\/\/\/\/\/|   |
 _|___|_______________________|___|__
|/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/|
|                                   |
|      B i r t h d a y @i3ym !      |
| ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ |
|___________________________________|

18.11.2025
- @samlovebutter
*/

const blockMaterialProperties = new Map<BlockModel, PhysicalProperties>();

//5% of the health
let minimalDamageModifier = 0.05;
let blockStrength = 900;
@injectable
export class BlockDamageController extends HostedService {
	private partBreakQueue: BasePart[] = [];
	private igniteBlocks: Map<BlockModel, number> = new Map();

	constructor(
		@inject private readonly sparksEffect: SparksEffect,
		@inject private readonly blockList: BlockList,
		@inject private readonly playerDataStorage: PlayerDataStorage,
	) {
		super();

		//init values
		this.event.subscribeObservable(
			playerDataStorage.config,
			(config) => {
				blockStrength = config.blockHealthModifier;
				minimalDamageModifier = config.blockMinimalDamageThreshold / 100;
			},
			true,
		);

		this.event.subscribe(RunService.Heartbeat, () => {
			if (this.partBreakQueue.size() === 0) return;
			for (const [block, heatDmage] of this.igniteBlocks) {
				// dunno what to do with the explosions so far
				// each explosion probably destroys welds
				// if (explosiveDamage > 0) explode(pp, explosiveDamage); //explode here
				const properties = blockMaterialProperties.get(block);
				if (!properties) continue; //throw "Trying to get properties for a destroyed block";

				const ignitionChance = //
					//basically density == chance, because density can't be bigger than 100, right? right..?
					// (1 - (density[0.01...100] / 100)) * fireChance
					(1 - properties.Density / 100) * math.clamp(heatDmage, 0, 1);

				if (testYourLuck(ignitionChance)) {
					RemoteEvents.Burn.send(block.GetDescendants().filter((v) => v.IsA("BasePart"))); //put on fire here
				}

				this.igniteBlocks.set(block, 0);
			}

			RemoteEvents.ImpactBreak.send(this.partBreakQueue);
			this.partBreakQueue = [];
		});
	}

	getHealth(block: BlockModel) {
		return blockHealthList.get(block);
	}

	initHealth(block: BlockModel) {
		const pp = block.PrimaryPart;
		if (!pp) throw "Trying to init block health with no PrimaryPart";

		const material = BlockManager.manager.material.get(block);
		const properties = new PhysicalProperties(material);

		blockMaterialProperties.set(block, properties);
		block.DescendantRemoving.Once(() => blockMaterialProperties.delete(block));

		// get smallest because it doesn't make sense for a giant
		// metal sheet to have 50k hp
		// also it doesn't make sense for a thing to get destroyed if it's too small
		// so 0.7 min to balance that
		const sizeModifier = math.max(pp.Size.findMin(), 0.7);

		// even more magic numbers below
		// it's a fine-tuned system

		// SOOO many modifiers
		let blockHealth =
			blockStrength *
			properties.Density * // div by 5 because it's too strong
			(1 - properties.Elasticity) *
			properties.ElasticityWeight *
			sizeModifier;

		if (pp.HasTag(TagUtils.allTags.IMPACT_STRONG)) blockHealth *= 2;

		const blockID = BlockManager.manager.id.get(block);
		const physicsConfig = this.blockList.blocks[blockID]?.physics;
		const impactStrengthModifier = physicsConfig?.impactDamageStrength ?? 1;
		const forcedThresholdModifier = math.max(physicsConfig?.impactDamageStrength ?? 0, minimalDamageModifier);

		const randomHealthPercentMultiplier = 0.15;
		blockHealth *=
			1 +
			(math.random(0, 100) / 100) *
				randomHealthPercentMultiplier *
				impactStrengthModifier *
				forcedThresholdModifier;

		blockHealthList.set(block, blockHealth);
		blockMaxHealthList.set(block, blockHealth);
		this.igniteBlocks.set(block, 0);
	}

	forceBreakBlock(block: BlockModel) {
		for (const p of block.GetDescendants()) {
			if (!(p.IsA("BasePart") || p.IsA("UnionOperation") || p.IsA("MeshPart"))) continue;
			this.partBreakQueue.push(p);
		}
	}

	forceBreakParts(...parts: BasePart[]) {
		for (const p of parts) this.partBreakQueue.push(p);
	}

	isBroken(block: BlockModel) {
		const health = this.getHealth(block);
		if (!health) return;
		return health <= 0;
	}

	applyDamage(block: BlockModel, damage: damageType) {
		const { explosiveDamage = 0, impactDamage = 0, heatDamage = 0 } = damage;

		// check if it's not destroyed
		const currentHealth = blockHealthList.get(block);
		if (!currentHealth || currentHealth <= 0) return;

		// also check if it's not destroyed
		const pp = block.PrimaryPart;
		if (!pp) return; //throw "Trying to apply damage to a block with no PrimaryPart";

		// do effect if damage is lower than treshold
		const minMod = currentHealth * minimalDamageModifier;
		if (impactDamage < minMod && impactDamage > minMod * 0.5) {
			this.sparksEffect.send(pp, { part: pp });
			return;
		}

		const totalDamage = heatDamage + impactDamage + explosiveDamage;
		const newHealth = currentHealth - totalDamage;
		blockHealthList.set(block, newHealth);

		// if (checkIfCanBeDestroyed(totalDamage, currentHealth)) {
		// 	blockHealthList.delete(block);
		// 	block.Destroy(); //destroy here
		// 	// the only case when else if is useful
		// } else
		if (newHealth <= 0) {
			//unweld here
			this.forceBreakBlock(block);
			this.igniteBlocks.set(block, heatDamage);
		}
	}
}


import { Players, RunService } from "@rbxts/services";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { C2S2CRemoteFunction } from "engine/shared/event/PERemoteEvent";
import { PlayerRank } from "engine/shared/PlayerRank";

@injectable
export class Switches {
	private readonly setSwitch = new C2S2CRemoteFunction<{ readonly name: string; readonly value: boolean }>(
		"adm_setsw",
	);

	private readonly _registered: { [k in string]: ObservableValue<boolean> } = {};
	readonly registered: { readonly [k in string]: ObservableValue<boolean> } = this._registered;

	constructor() {
		if (RunService.IsServer()) {
			this.setSwitch.subscribe((player, { name, value }) => {
				if (!PlayerRank.isAdmin(player)) {
					return {
						success: false,
						message: "Not enough permissions",
					};
				}

				this.registered[name]?.set(value);
				return { success: true };
			});
		}
	}

	registerOrGet(name: string, defaultValue: boolean) {
		const existing = this.registered[name];
		if (existing) return existing;

		const value = new ObservableValue<boolean>(defaultValue);
		this.register(name, value);

		return value;
	}
	register(name: string, value: ObservableValue<boolean>) {
		this._registered[name] = value;

		if (RunService.IsClient() && PlayerRank.isAdmin(Players.LocalPlayer)) {
			value.subscribe((value) => this.setSwitch.send({ name, value }));
		}
	}
}


import { RunService } from "@rbxts/services";
import { Component } from "engine/shared/component/Component";
import type { EasingDirection, EasingStyle } from "engine/shared/component/Easing";

export interface Transform {
	/** @returns True if completed */
	runFrame(time: number): boolean | TransformBuilder;

	/** Immediately finish a transform */
	finish(): void;
}

export interface TransformProps {
	readonly duration?: number;
	readonly style?: EasingStyle;
	readonly direction?: EasingDirection;
}

export class ParallelTransformSequence implements Transform {
	private readonly sequence: Transform[];

	constructor(sequence: readonly Transform[]) {
		this.sequence = [...sequence];
	}

	runFrame(time: number): boolean {
		if (this.sequence.size() === 0) {
			return true;
		}

		const run = (transform: Transform) => {
			const result = transform.runFrame(time);
			if (!result) return;

			this.sequence.remove(this.sequence.indexOf(transform));
			if (result !== true) {
				const seq = result.buildSequence();
				this.sequence.push(seq);

				if (run(seq)) return true;
			}

			if (this.sequence.size() === 0) {
				return true;
			}
		};

		for (const transform of [...this.sequence]) {
			if (run(transform)) {
				return true;
			}
		}

		return false;
	}

	finish() {
		for (const transform of this.sequence) {
			transform.finish();
		}

		this.sequence.clear();
	}
}

export class TransformSequence implements Transform {
	private readonly sequence: Transform[];
	private timeOffset = 0;

	constructor(sequence: readonly Transform[]) {
		this.sequence = [...sequence];
	}

	runFrame(time: number): boolean {
		if (this.sequence.size() === 0) {
			return true;
		}

		const result = this.sequence[0].runFrame(time - this.timeOffset);
		if (!result) return false;

		this.sequence.remove(0);
		this.timeOffset = time;

		if (result !== true) {
			const seq = result.buildSequence();
			this.sequence.push(seq);
		}

		if (this.runFrame(time)) {
			return true;
		}

		if (this.sequence.size() === 0) {
			return true;
		}

		return false;
	}

	finish() {
		for (const transform of this.sequence) {
			transform.finish();
		}

		this.sequence.clear();
	}
}

//

interface TransformBuilderBase {
	buildSequence(): TransformSequence;

	/** Add a transform into the current parallel sequence */
	push(transform: Transform | TransformBuilder): this;

	/** End the current parallel sequence and start another */
	then(): this;
}

export interface TransformBuilder extends TransformBuilderBase {}
export class TransformBuilder implements TransformBuilderBase {
	private readonly transforms: Transform[][] = [[]];

	buildSequence(): TransformSequence {
		return new TransformSequence(this.transforms.map((seq) => new ParallelTransformSequence(seq)));
	}

	then(): this {
		this.transforms.push([]);
		return this;
	}
	push(transform: Transform | TransformBuilder): this {
		if (!("finish" in transform)) {
			transform = transform.buildSequence();
		}

		this.transforms[this.transforms.size() - 1].push(transform);
		return this;
	}
}

//

export interface RunningTransform {
	isCompleted(): boolean;
	cancel(): void;
	finish(): void;
}
export class TransformRunner extends Component implements RunningTransform {
	private transform: Transform;
	private time = 0;

	constructor(transform: Transform) {
		super();
		this.transform = transform;

		const run = () => {
			const result = transform.runFrame(this.time);
			if (!result) return;

			if (result === true) {
				this.destroy();
			} else {
				transform = result.buildSequence();
				run();
			}
		};

		this.event.subscribe(RunService.Heartbeat, (dt) => {
			this.time += dt;
			run();
		});

		let firstRan = false;
		this.onEnable(() => {
			if (firstRan) return;

			run();
			firstRan = true;
		});
	}

	isCompleted() {
		return this.isDestroyed();
	}

	/** Immediately finish the transform */
	finish() {
		this.transform.finish();
		this.destroy();
	}

	/** Stop and disable the transform */
	cancel() {
		this.destroy();
	}
}


import { Players, RunService, UserInputService, Workspace } from "@rbxts/services";
import { Achievement } from "server/Achievement";
import { LogicOverclockBlock } from "shared/blocks/blocks/LogicOverclockBlock";
import { LuaCircuitBlock } from "shared/blocks/blocks/LuaCircuitBlock";
import { BlockManager } from "shared/building/BlockManager";
import { GameDefinitions } from "shared/data/GameDefinitions";
import { CustomRemotes } from "shared/Remotes";
import type { baseAchievementStats } from "server/Achievement";
import type { PlayerDatabase } from "server/database/PlayerDatabase";
import type { PlayModeController } from "server/modes/PlayModeController";
import type { ServerPlayerController } from "server/ServerPlayerController";
import type { SharedPlots } from "shared/building/SharedPlots";
import type { FireEffect } from "shared/effects/FireEffect";
import type { PlayerDataStorageRemotesBuilding } from "shared/remotes/PlayerDataRemotes";

type triggerInstances = Folder & Record<`trigger${number}`, BasePart>;

const ws = Workspace as Workspace & {
	Triggers: {
		Centrifuge: triggerInstances;
		AmogusTrack: triggerInstances;
		AirRingsEasy: triggerInstances;
		AirRingsMedium: triggerInstances;
		AirRingsHard: triggerInstances;
		OvalTrack: triggerInstances;
	};
	Map: Folder & {
		Banana: Model;
		UFO: Model;
		"Main Island": {
			Fun: {
				Destructibles: Folder;
			};
		};
	};
};

const _triggers = ws.Triggers;

//make triggers invisible on run
for (const f of Workspace.FindFirstChild("Triggers")!.GetChildren()) {
	f.GetChildren().forEach((v) => ((v as BasePart).Transparency = 1));
}

// DO NOT CHANGE! RETURNS SORTED ARRAY!
const getTriggerList = (n: keyof typeof _triggers) => {
	const tgs = _triggers[n];
	const rawlist = tgs.GetChildren() as (BasePart | UnionOperation)[];
	const list = [];
	for (let i = 0; i < rawlist.size(); i++) {
		const v = tgs.FindFirstChild(`trigger${i}`);
		if (!v) throw `Trigger init error: "trigger${i}" not found in triggers of ${n}`;
		list[i] = v as BasePart | UnionOperation;
	}

	const record = {} as triggerInstances;
	list.forEach((v) => (record[v.Name as `trigger${number}`] = v));
	return $tuple(list, record);
};

/*
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
	PLEASE DO NOT SPOIL THE ACHIEVEMENTS FOR OTHER PLAYERS!!!
*/

@injectable
class AchievementWelcome extends Achievement {
	constructor(@inject player: Player) {
		super(player, {
			id: "WELCOME",
			name: `Hello!`,
			description: `Welcome to OverEngineered!`,
			imageID: "78364064019512",
		});

		this.onEnable(() => this.set({ completed: true }));
	}
}

@injectable
class AchievementTheIssue extends Achievement {
	constructor(@inject player: Player) {
		super(player, {
			id: "THE_ISSUE",
			name: "DMCA abuse",
			description: "Now go to our community server and read #the-issue channel",
			hidden: true,
			imageID: "76517691012059",
		});

		this.event.subscribe(player.Chatted, (msg, recv) => {
			if (recv) return;
			this.set({ completed: msg.fullLower().contains("plane crazy") });
		});
	}
}

@injectable
class AchievementLuaCircuitObtained extends Achievement {
	constructor(@inject player: Player, @inject playerDatabase: PlayerDatabase) {
		super(player, {
			id: "LUA_CIRCUIT",
			name: "Oh yeah, it's big brain time",
			description: `Obtain ${LuaCircuitBlock.displayName} by joining our community server and following instructions there.`,
			imageID: "93831558669845",
		});

		this.onEnable(() => {
			this.set({ completed: (playerDatabase.get(player.UserId).features?.indexOf("lua_circuit") ?? -1) > -1 });
		});
	}
}

abstract class AchievementPlaytime extends Achievement<{ seconds_spent: number }> {
	constructor(player: Player, data: Partial<baseAchievementStats>, target_seconds: number) {
		//1 hour
		const target_hours = target_seconds / 60 / 60;
		super(player, {
			id: "SPEND_1_HOUR",
			name: `Spare time`,
			description: `Play for over ${target_hours} ${target_hours > 1 ? "hours" : "hour"} in total`,
			max: target_seconds,
			units: "time",
			imageID: "100755497882706",
			...data,
		});

		this.onEnable(() => {
			// getData will return 0 or undefined if run before enable
			let seconds_spent = this.getData()?.seconds_spent ?? 0;
			this.event.subscribe(RunService.Heartbeat, (delta) => {
				seconds_spent += delta;
				this.set({ progress: seconds_spent, seconds_spent });
			});
		});
	}
}

@injectable
class AchievementPlaytime1H extends AchievementPlaytime {
	constructor(@inject player: Player) {
		super(
			player,
			{
				id: "SPEND_1_HOUR",
				name: `Spare time`,
			},
			1 * 60 * 60,
		);
	}
}

@injectable
class AchievementPlaytime4H extends AchievementPlaytime {
	constructor(@inject player: Player) {
		super(
			player,
			{
				id: "SPEND_4_HOUR",
				name: `Time well spent`,
			},
			4 * 60 * 60,
		);
	}
}

@injectable
class AchievementPlaytime12H extends AchievementPlaytime {
	constructor(@inject player: Player) {
		super(
			player,
			{
				id: "SPEND_12_HOUR",
				name: `Master Engineer`,
			},
			12 * 60 * 60,
		);
	}
}

@injectable
class AchievementPlaytime36H extends AchievementPlaytime {
	constructor(@inject player: Player) {
		super(
			player,
			{
				id: "SPEND_36_HOUR",
				name: `Get a Life`,
				hidden: true,
			},
			36 * 60 * 60,
		);
	}
}

@injectable
class AchievementPlaytime72H extends AchievementPlaytime {
	constructor(@inject player: Player) {
		super(
			player,
			{
				id: "SPEND_72_HOUR",
				name: `Get More Life`,
				hidden: true,
			},
			72 * 60 * 60,
		);
	}
}

@injectable
class AchievementPlaytime120H extends AchievementPlaytime {
	constructor(@inject player: Player) {
		super(
			player,
			{
				id: "SPEND_120_HOUR",
				name: `Unemployed Amount of Gaming`,
				hidden: true,
			},
			120 * 60 * 60,
		);
	}
}

@injectable
class AchievementAfkTime extends Achievement<{ seconds_record: number }> {
	constructor(@inject player: Player) {
		//15 minutes
		const target_seconds = 15 * 60;
		const target_minutes = target_seconds / 60;
		super(player, {
			id: "BE_AFK_15_MINUTES",
			name: `DON'T TOUCH ANYTHING!`,
			description: `Be AFK for ${target_minutes} minutes`,
			hidden: true,
			max: target_seconds,
			units: "time",
			imageID: "100755497882706",
		});

		this.onEnable(() => {
			let seconds_record = this.getData()?.seconds_record ?? 0;
			let tsk: thread;
			let isAfk = false;
			this.event.subscribe(UserInputService.InputBegan, () => {
				isAfk = false;
				if (tsk) task.cancel(tsk);
				tsk = task.delay(60, () => (isAfk = true));
			});

			this.event.subscribe(RunService.Heartbeat, (delta) => {
				if (!isAfk) return;
				seconds_record += delta;
				this.set({ progress: seconds_record, seconds_record });
			});
		});
	}
}

abstract class AchievementHeightRecord extends Achievement<{ height_record: number }> {
	constructor(player: Player, name: string, description: string, targetHeight: number, hidden: boolean = false) {
		super(player, {
			id: `HEIGHT_TARGET_${targetHeight}`,
			name,
			description: `${description} (${targetHeight} studs traveled)`,
			max: targetHeight,
			hidden,
			imageID: "105060915517150",
		});

		this.onEnable(() => {
			let height_record = this.getData()?.height_record ?? 0;
			this.event.subscribe(RunService.Heartbeat, () => {
				const character = player.Character?.PrimaryPart;
				if (!character) return;

				height_record = math.max(character.Position.Y - GameDefinitions.HEIGHT_OFFSET, height_record);
				this.set({ progress: height_record, height_record });
			});
		});
	}
}

@injectable
class AchievementHeightRecord25k extends AchievementHeightRecord {
	constructor(@inject player: Player) {
		super(player, `Space tourism`, `Leave the atmosphere`, 25_000);
	}
}

@injectable
class AchievementHeightRecord75k extends AchievementHeightRecord {
	constructor(@inject player: Player) {
		super(player, `SPAAAAACE`, `Deeper into the void!`, 75_000);
	}
}

@injectable
class AchievementHeightRecord150k extends AchievementHeightRecord {
	constructor(@inject player: Player) {
		super(player, `Deepfried space`, `Things are wobbly over here`, 150_000);
	}
}

@injectable
class AchievementHeightRecord500k extends AchievementHeightRecord {
	constructor(@inject player: Player) {
		super(player, `Outer Spaced`, `Long trip home`, 500_000, true);
	}
}

abstract class AchievementSpeedRecord extends Achievement<{ time_record: number }> {
	constructor(player: Player, name: string, targetSpeed: number, hidden = false) {
		super(player, {
			id: `SPEED_TARGET_${targetSpeed}`,
			name: name,
			description: `Reach speed over ${targetSpeed} studs/second in horizontal axis for 3 seconds`,
			hidden,
			max: 3,
			units: "time",
			imageID: "84161963549773",
		});

		this.onEnable(() => {
			let counter = 0;
			let time_record = this.getData()?.time_record ?? 0;
			this.event.subscribe(RunService.Heartbeat, (delta) => {
				const character = player.Character?.PrimaryPart;
				if (!character) return (counter = 0);

				//exclude Y axis becuase it can be abused by helium and other things
				// should angular velocity really be included? I dunno
				const speed = character.AssemblyLinearVelocity.apply((v, a) => (a === "Y" ? 0 : v)).add(
					character.AssemblyAngularVelocity,
				).Magnitude;

				if (speed < targetSpeed) return (counter = 0);

				time_record = math.max((counter += delta), time_record);
				this.set({ progress: counter, time_record });
			});
		});
	}
}

@injectable
class AchievementSpeedRecord1k extends AchievementSpeedRecord {
	constructor(@inject player: Player) {
		super(player, `A bit fast, eh?`, 1000);
	}
}

@injectable
class AchievementSpeedRecord5k extends AchievementSpeedRecord {
	constructor(@inject player: Player) {
		super(player, `4.114 Machs doesn't sound like a lot`, 5000);
	}
}

@injectable
class AchievementSpeedRecord15k extends AchievementSpeedRecord {
	constructor(@inject player: Player) {
		super(player, `BRO WHERE ARE WE GOING?!`, 15_000, true);
	}
}

@injectable
class AchievementSpeedRecord50k extends AchievementSpeedRecord {
	constructor(@inject player: Player) {
		super(player, `Typical High Speed Fan`, 50_000, true);
	}
}

@injectable
class AchievementSpeedRecord100k extends AchievementSpeedRecord {
	constructor(@inject player: Player) {
		super(player, `Lightspeed Enjoyer`, 150_000, true);
	}
}

@injectable
class AchievementCatchOnFire extends Achievement {
	constructor(@inject player: Player, @inject fireffect: FireEffect, @inject plots: SharedPlots) {
		super(player, {
			id: "CATCH_ON_FIRE",
			name: "OverCooked!",
			description: "Better call the fire department! (We don't have one)",
			imageID: "89747760666734",
		});

		this.event.subscribe(fireffect.event.s2c.sent, (_, args) => {
			const owner = plots.plots.find((c) => args.part.IsDescendantOf(c.instance))?.ownerId?.get();
			if (!owner) return;

			this.set({ completed: owner === player.UserId });
		});
	}
}

@injectable
class AchievementOverclock extends Achievement {
	constructor(
		@inject player: Player,
		@inject playModeController: PlayModeController,
		@inject plots: SharedPlots,
		@inject plot: PlayerDataStorageRemotesBuilding,
	) {
		super(player, {
			id: "USE_OVERCLOCK",
			name: "OverClocked!",
			description: "What's that noise? OHHH MY PC",
			imageID: "75746597939007",
		});

		let hasOverClock = false;
		this.event.subscribe(Players.PlayerRemoving, (p) => {
			if (p !== player) return;
			hasOverClock = false;
		});

		this.event.subscribe(plot.placeBlocks.processed, (player, a, b) => {
			const id = plots.getPlotComponent(a.plot).ownerId.get();
			if (!id) return;

			const p = Players.GetPlayerByUserId(id);
			if (p !== player) return;

			for (const m of b.models) {
				if (BlockManager.manager.id.get(m) === LogicOverclockBlock.id) {
					hasOverClock = true;
					return;
				}
			}
		});

		this.event.subscribe(plot.deleteBlocks.processed, (player, a, b) => {
			const id = plots.getPlotComponent(a.plot).ownerId.get();
			if (!id) return;

			if (a.blocks === "all") {
				hasOverClock = false;
				return;
			}

			const p = Players.GetPlayerByUserId(id);
			if (p !== player) return;

			for (const m of a.blocks) {
				if (BlockManager.manager.id.get(m) === LogicOverclockBlock.id) {
					hasOverClock = false;
					return;
				}
			}
		});

		this.event.subscribe(CustomRemotes.modes.setOnClient.sent, () => {
			const mode = playModeController.getPlayerMode(player);
			if (mode !== "ride") return;

			this.set({ completed: hasOverClock });
		});
	}
}

abstract class AchievementCheckpoints extends Achievement<{ checkpoints_finished: string[] }> {
	constructor(
		player: Player,
		playModeController: PlayModeController,
		data: baseAchievementStats,
		triggerGroup: keyof typeof _triggers,
	) {
		super(player, data);

		this.onEnable(() => {
			const checkpoints_finished = new Set(this.getData()?.checkpoints_finished ?? []);
			const hitSequence: (BasePart | undefined)[] = [];
			const [triggersList, triggersRecord] = getTriggerList(triggerGroup);
			const listLen = triggersList.size();
			for (let i = 0; i < listLen; i++) {
				const t = triggersList[i];

				this.event.subscribe(t.Touched, (inst) => {
					//get player's mode
					// yes it DOESN'T actually check if player is in their own vehicle
					// but it doesn't matter because player doesn't know it
					if (playModeController.getPlayerMode(player) !== "ride") return;

					//check if part touched is player's
					if (inst.Parent !== player.Character) return;

					//add timeout to the trigger
					hitSequence[i] = t;
					checkpoints_finished.add(t.Name);

					//check if all triggered
					let allTriggered = true;
					for (let j = 0; j < listLen; j++) {
						const tr = hitSequence[j];

						if (tr === undefined) {
							allTriggered = false;
							break;
						}
					}

					this.set({ completed: allTriggered, checkpoints_finished: [...checkpoints_finished] });
				});
			}
		});
	}
}

//may be will be used some day
const getExtremesOfArray = (arr: number[]): LuaTuple<[number, number]> => $tuple(math.min(...arr), math.max(...arr));

abstract class AchievementCheckpointsWithTimeout extends Achievement {
	constructor(
		player: Player,
		playModeController: PlayModeController,
		data: baseAchievementStats,
		timeout_seconds: number,
		triggerGroup: keyof typeof _triggers,
	) {
		super(player, data);

		const hitSequence: (BasePart | undefined)[] = [];
		const [triggersList, triggersRecord] = getTriggerList(triggerGroup);
		const listLen = triggersList.size();
		for (let i = 0; i < listLen; i++) {
			const t = triggersList[i];

			//I believe it to be kinda clever actually
			let thread: thread;
			this.event.subscribe(t.Touched, (inst) => {
				//get player's mode
				// yes it DOESN'T actually check if player is in their own vehicle
				// but it doesn't matter because player doesn't know it
				if (playModeController.getPlayerMode(player) !== "ride") return;

				//check if part touched is player's
				if (inst.Parent !== player.Character) return;

				//add timeout to the trigger
				hitSequence[i] = t;
				if (thread) task.cancel(thread);
				thread = task.delay(timeout_seconds, () => (hitSequence[i] = undefined));

				//check if all triggered
				let allTriggered = true;
				for (let j = 0; j < listLen; j++) {
					const tr = hitSequence[j];
					if (tr === undefined) {
						allTriggered = false;
						break;
					}
				}

				this.set({ completed: allTriggered });
			});
		}
	}
}

abstract class AchievementCentrifuge extends AchievementCheckpointsWithTimeout {
	constructor(
		player: Player,
		playModeController: PlayModeController,
		name: string,
		timeout_seconds: number,
		hidden = false,
	) {
		super(
			player,
			playModeController,
			{
				id: `CENTRIFUGE_TARGET_${timeout_seconds}`,
				name,
				description: `Make a lap in the Centrifuge in ${timeout_seconds} seconds or less`,
				hidden,
				imageID: "109486075173347",
			},
			timeout_seconds,
			"Centrifuge",
		);
	}
}

@injectable
class AchievementCentrifuge30seconds extends AchievementCentrifuge {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, `30 Seconds or Less`, 30);
	}
}

@injectable
class AchievementCentrifuge20seconds extends AchievementCentrifuge {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, `Now We're Cooking with Gas!`, 20);
	}
}

@injectable
class AchievementCentrifuge10seconds extends AchievementCentrifuge {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, `Right round like a record, baby`, 10);
	}
}

@injectable
class AchievementCentrifuge5seconds extends AchievementCentrifuge {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, `KA-CHOW`, 5, true);
	}
}

abstract class AchievementAmogusTrack extends AchievementCheckpointsWithTimeout {
	constructor(
		player: Player,
		playModeController: PlayModeController,
		timeout_seconds: number,
		name: string,
		hidden = false,
		description = `Make a lap on the race track in ${timeout_seconds} seconds or less. No shortcuts.`,
	) {
		super(
			player,
			playModeController,
			{
				id: `RACE_TRACK_TARGET_${timeout_seconds}`,
				name,
				description,
				hidden,
				imageID: "103876818849553",
			},
			timeout_seconds,
			"AmogusTrack",
		);
	}
}

@injectable
class AchievementAmogusTrack20seconds extends AchievementAmogusTrack {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, 20, `Minimum Viable Product`, false, `Just running a lap is a solution too`);
	}
}

@injectable
class AchievementAmogusTrack15seconds extends AchievementAmogusTrack {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, 15, `Circuit Breaker`);
	}
}

@injectable
class AchievementAmogusTrack10seconds extends AchievementAmogusTrack {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, 10, `TAS Bot Approved`);
	}
}

abstract class AchievementOvalTrack extends AchievementCheckpointsWithTimeout {
	constructor(
		player: Player,
		playModeController: PlayModeController,
		timeout_seconds: number,
		name: string,
		description = `Make a lap on the Oval race track in ${timeout_seconds} seconds or less. No shortcuts.`,
	) {
		super(
			player,
			playModeController,
			{
				id: `RACE_TRACK_OVALS_TARGET_${timeout_seconds}`,
				name,
				description,
				imageID: "127597860492025",
			},
			timeout_seconds,
			"OvalTrack",
		);
	}
}

@injectable
class AchievementOvalTrack20seconds extends AchievementOvalTrack {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, 20, `First Lap`);
	}
}

@injectable
class AchievementOvalTrack15seconds extends AchievementOvalTrack {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, 15, `Qualifying Lap`);
	}
}

@injectable
class AchievementOvalTrack10seconds extends AchievementOvalTrack {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(player, playModeController, 10, `Grind Prix`);
	}
}

@injectable
class AchievementAirRingsEasy extends AchievementCheckpoints {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(
			player,
			playModeController,
			{
				id: `AIR_COURSE_EASY`,
				name: "Flight School Graduate",
				description: `Finish easy difficulty air course`,
				imageID: "101267722343574",
			},
			"AirRingsEasy",
		);
	}
}

@injectable
class AchievementAirRingsMedium extends AchievementCheckpointsWithTimeout {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(
			player,
			playModeController,
			{
				id: `AIR_COURSE_MEDIUM`,
				name: "Through John's Heart",
				description: `Finish medium difficulty air course in 25 seconds or less`,
				imageID: "101267722343574",
			},
			25,
			"AirRingsMedium",
		);
	}
}

@injectable
class AchievementAirRingsHard extends AchievementCheckpointsWithTimeout {
	constructor(@inject player: Player, @inject playModeController: PlayModeController) {
		super(
			player,
			playModeController,
			{
				id: `AIR_COURSE_HARD`,
				name: "No Room for Error",
				description: `Finish intentionally hard air course in 60 seconds or less.. Designed to test player's skills in engineering and piloting.`,
				imageID: "101267722343574",
			},
			60,
			"AirRingsHard",
		);
	}
}

abstract class AchievementFindGetNearObject extends Achievement {
	constructor(
		player: Player,
		data: baseAchievementStats,
		targetObject: BasePart | UnionOperation | undefined,
		activationDistance: number,
	) {
		super(player, data);

		let counter = 0;
		this.event.subscribe(RunService.Heartbeat, (delta) => {
			const character = player.Character?.PrimaryPart;

			if (!character || !targetObject) {
				counter = 0;
				return;
			}

			// same result, purposefully separated conditions
			if (character.Position.sub(targetObject.Position).Magnitude > activationDistance) {
				counter = 0;
				return;
			}

			counter += delta;

			this.set({ progress: counter });
		});
	}
}

@injectable
class AchievementFindBanana extends AchievementFindGetNearObject {
	constructor(@inject player: Player) {
		super(
			player,
			{
				id: "FIND_BANANA",
				name: "Completely bananas!",
				description: "Find the banana!",
				hidden: true,
				max: 4,
				imageID: "132805406903978",
			},
			ws.Map.Banana.PrimaryPart,
			40,
		);
	}
}

@injectable
class AchievementFindUFO extends AchievementFindGetNearObject {
	constructor(@inject player: Player) {
		super(
			player,
			{
				id: "FIND_UFO",
				name: "I Want to Believe!",
				description: "Find the UFO!",
				hidden: true,
				max: 4,
				imageID: "110520480308001",
			},
			ws.Map.UFO.PrimaryPart,
			150,
		);
	}
}

// I would not be surprised if the name gets changed (I couldent think of anything else that fits)
@injectable
class BonkBonkByeBye extends Achievement {
	constructor(@inject player: Player) {
		super(player, {
			id: "MAXWELL_BONK",
			name: "Cat-astrophe",
			description: "Knock Maxwell off Big John, how rude >:(",
			imageID: "136757634650350",
			hidden: true,
		});

		// Maxwell not important enough for a capital name???
		const maxwell = ws.Map["Main Island"].Fun.Destructibles.FindFirstChild("maxwell") as MeshPart;
		if (!maxwell) return;

		// keep track of the last player that touched maxwell
		let maxtag = maxwell.FindFirstChild("MaxwellPlayerTag") as IntValue;
		if (!maxtag) {
			// create it
			maxtag = new Instance("IntValue", maxwell);
		}

		maxwell.Touched.Connect((hitPart) => {
			const character = hitPart.FindFirstAncestorWhichIsA("Model");
			if (!character) return;
			const plr = Players.GetPlayerFromCharacter(character);
			// check if player exists
			if (plr && plr.UserId === player.UserId) {
				maxtag.Value = plr.UserId;
			}
		});

		this.event.subscribe(RunService.Heartbeat, () => {
			if (!maxwell) return;

			// this is how the game triggers *the screaming*
			// (slightly increased to make sure its falling)
			if (maxwell.AssemblyLinearVelocity.Magnitude > 20) {
				if (maxtag.Value !== player.UserId) return;
				this.set({ completed: true });
			}
		});
	}
}

@injectable
class AchievementBreakSomething extends Achievement {
	constructor(@inject player: Player) {
		super(player, {
			id: "BREAK_MAP_DESTRUCTABLE",
			name: "Breaking Change",
			description: "Break a hydrant or something, or be near when it happens",
			imageID: "79485719904367",
		});

		const activationDistance = 15;
		for (const o of ws.Map["Main Island"].Fun.Destructibles.GetChildren()) {
			if (o.Name !== "Fire Hydrant") continue;
			const obj = o as Model & {
				Main: BasePart & {
					TriggeredSound: Sound;
				};
			};

			this.event.subscribe(obj.Main.TriggeredSound.Played, () => {
				const character = player.Character?.PrimaryPart;
				if (!character) return;
				this.set({ completed: character.Position.sub(obj.Main.Position).Magnitude < activationDistance });
			});
		}
	}
}

@injectable
class AchievementFOVMax extends Achievement {
	constructor(@inject player: Player, @inject serverPlayerController: ServerPlayerController) {
		super(player, {
			id: "FOV_MAX",
			name: "Quake pro",
			description: "Set your FOV to the maximum value",
			hidden: true,
			max: 120,
			imageID: "129519370592474",
		});

		this.event.subscribe(serverPlayerController.remotes.player.updateSettings.invoked, (p, s) => {
			if (p !== player) return;
			if (!s.betterCamera?.fov) return;
			this.set({ progress: s.betterCamera.fov });
		});
	}
}

export const allAchievements: readonly ConstructorOf<Achievement>[] = [
	AchievementWelcome,
	AchievementLuaCircuitObtained,
	AchievementPlaytime1H,
	AchievementPlaytime4H,
	AchievementPlaytime12H,
	AchievementPlaytime36H,
	AchievementPlaytime72H,
	AchievementPlaytime120H,
	AchievementAfkTime,

	AchievementHeightRecord25k,
	AchievementHeightRecord75k,
	AchievementHeightRecord150k,
	AchievementHeightRecord500k,

	AchievementSpeedRecord1k,
	AchievementSpeedRecord5k,
	AchievementSpeedRecord15k,
	AchievementSpeedRecord50k,
	AchievementSpeedRecord100k,

	AchievementCatchOnFire,

	AchievementTheIssue,
	AchievementOverclock,
	AchievementFOVMax,

	// map-specific ones
	AchievementBreakSomething,
	AchievementFindBanana,
	AchievementFindUFO,
	BonkBonkByeBye,

	AchievementCentrifuge30seconds,
	AchievementCentrifuge20seconds,
	AchievementCentrifuge10seconds,
	AchievementCentrifuge5seconds,

	AchievementAmogusTrack20seconds,
	AchievementAmogusTrack15seconds,
	AchievementAmogusTrack10seconds,

	AchievementOvalTrack20seconds,
	AchievementOvalTrack15seconds,
	AchievementOvalTrack10seconds,

	AchievementAirRingsEasy,
	AchievementAirRingsMedium,
	AchievementAirRingsHard,
];


import { BadgeService } from "@rbxts/services";
import { HostedService } from "engine/shared/di/HostedService";
import { PlayerWatcher } from "engine/shared/PlayerWatcher";

@injectable
export class BadgeController extends HostedService {
	// Updated for 10/21/2025
	private readonly placeId = 116247414183109;
	private readonly badges = {
		BETA: 4025402536290574,
	} as const;

	constructor() {
		super();

		this.event.subscribeCollectionAdded(PlayerWatcher.players, (player) => {
			player.CharacterAdded.Once(() => {
				try {
					// BETA badge
					if (!BadgeService.UserHasBadgeAsync(player.UserId, this.badges.BETA)) {
						if (game.PlaceId !== this.placeId) {
							$log(`BadgeController is not configured for this place, skipping "BETA" badge award.`);
							return;
						}
						BadgeService.AwardBadge(player.UserId, this.badges.BETA);
					}
				} catch {
					$log(`Failed to give badge to ${player.Name}`);
				}
			});
		});
	}
}


import { HostedService } from "engine/shared/di/HostedService";
import { BidirectionalRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { PlayerConfigDefinition } from "shared/config/PlayerConfig";
import type { CreatableRemoteEvents } from "engine/shared/event/PERemoteEvent";
import type { PlayerDatabase } from "server/database/PlayerDatabase";
import type { ServerPlayersController } from "server/ServerPlayersController";
import type { SharedPlots } from "shared/building/SharedPlots";
import type { EEEffect, EffectCreator } from "shared/effects/EffectBase";

@injectable
class ServerEffect<T> implements EEEffect<T> {
	readonly event: BidirectionalRemoteEvent<T>;

	constructor(
		name: string,
		eventType: CreatableRemoteEvents,
		@inject private readonly plots: SharedPlots,
		@inject private readonly playerDatabase: PlayerDatabase,
		@inject private readonly playersController: ServerPlayersController,
	) {
		this.event = new BidirectionalRemoteEvent<T>(name, eventType);

		this.event.c2s.invoked.Connect((owner, arg) => {
			let players = playersController.getPlayers();
			players = players.filter((p) => p !== owner);
			players = players.filter((player) => this.filterPlayer(owner, player));

			this.event.s2c.send(players, arg);
		});
	}

	send(ownerPart: BasePart, arg: T): void {
		if (!ownerPart || !ownerPart.CanSetNetworkOwnership()[0]) return;

		const owner = ownerPart.GetNetworkOwner();
		const players = this.playersController.getPlayers().filter((player) => this.filterPlayer(owner, player));

		this.event.s2c.send(players, arg);
	}

	private filterPlayer(owner: Player | undefined, player: Player): boolean {
		if (player === owner) return true;

		if (owner) {
			const plot = this.plots.getPlotComponentByOwnerID(player.UserId);
			if (plot.isBlacklisted(owner)) {
				return false;
			}
		}

		const other =
			this.playerDatabase.get(player.UserId).settings?.graphics?.othersEffects ??
			PlayerConfigDefinition.graphics.config.othersEffects;

		return !!other;
	}
}

@injectable
export class ServerEffectCreator extends HostedService implements EffectCreator {
	constructor(@inject private readonly di: DIContainer) {
		super();
	}

	create<T>(name: string, eventType: CreatableRemoteEvents): EEEffect<T> {
		return this.di.resolveForeignClass(ServerEffect<T>, [name, eventType]);
	}
}


import { Players } from "@rbxts/services";
import { Component } from "engine/shared/component/Component";
import { ComponentInstance } from "engine/shared/component/ComponentInstance";
import { Element } from "engine/shared/Element";
import { PlayerDataController } from "server/PlayerDataController";
import { PlayerDataRemotes } from "shared/remotes/PlayerDataRemotes";

@injectable
export class ServerPlayerDataRemotesController extends Component {
	/**
	 * @param playerId ID of the player to get the data for
	 * @param player Player that the remotes folder is parented to
	 */
	static create(di: DIContainer, playerId: number, player?: Player): ServerPlayerDataRemotesController {
		player ??= Players.GetPlayerByUserId(playerId);
		if (!player) throw `Player ${playerId} is not online`;

		const remotesFolder = Element.create("ScreenGui", { Parent: player.WaitForChild("PlayerGui") });
		remotesFolder.ResetOnSpawn = false;
		remotesFolder.Name = "Remotes";

		return di.resolveForeignClass(ServerPlayerDataRemotesController, [playerId, remotesFolder]);
	}

	readonly slotRemotes;
	readonly playerRemotes;

	constructor(
		readonly playerId: number,
		readonly remotesFolder: Instance,
		@inject di: DIContainer,
	) {
		super();
		ComponentInstance.init(this, remotesFolder);

		this.slotRemotes = PlayerDataRemotes.createSlots(remotesFolder);
		this.playerRemotes = PlayerDataRemotes.createPlayer(remotesFolder);

		this.parent(di.resolveForeignClass(PlayerDataController, [playerId, this.playerRemotes]));
	}
}


import { Workspace } from "@rbxts/services";
import { LocalInstanceData } from "engine/shared/LocalInstanceData";
import { ServerPartUtils } from "server/plots/ServerPartUtils";
import { BlockManager } from "shared/building/BlockManager";
import { GameDefinitions } from "shared/data/GameDefinitions";
import { CustomRemotes } from "shared/Remotes";
import { CustomDebrisService } from "shared/service/CustomDebrisService";
import { TagUtils } from "shared/utils/TagUtils";
import type { ServerPlayersController } from "server/ServerPlayersController";
import type { FireEffect } from "shared/effects/FireEffect";

const overlapParams = new OverlapParams();
overlapParams.CollisionGroup = "Blocks";

@injectable
export class SpreadingFireController {
	constructor(
		@inject private readonly fireEffect: FireEffect,
		@inject private readonly playersController: ServerPlayersController,
	) {}

	private isPartBurnable(part: BasePart) {
		if (
			!BlockManager.isActiveBlockPart(part) ||
			LocalInstanceData.HasLocalTag(part, "Burn") ||
			part.HasTag(TagUtils.allTags.FIREPROOF_MATERIAL) ||
			(math.random(1, 8) !== 1 && part.Position.Y < 1 + GameDefinitions.HEIGHT_OFFSET)
		) {
			return false;
		}

		return true;
	}

	burn(part: BasePart) {
		if (!this.isPartBurnable(part)) {
			return;
		}

		LocalInstanceData.AddLocalTag(part, "Burn");
		if (CustomDebrisService.exists(part)) CustomDebrisService.remove(part);

		// Apply color
		const darkness = math.random(0, 50);
		part.Color = Color3.fromRGB(darkness, darkness, darkness);

		const duration = math.random(15, 30);

		// Apply fire effect
		this.fireEffect.send(part, { part, duration });

		task.delay(duration, () => {
			if (!part.Parent) return;
			if (!part.CanSetNetworkOwnership()[0]) return;

			// Break joints with a chance
			if (math.random(1, 4) === 1) {
				const players = this.playersController.getPlayers().filter((p) => p !== part.GetNetworkOwner());
				CustomRemotes.physics.normalizeRootparts.send(players, { parts: [part] });
				ServerPartUtils.BreakJoints(part);
			}

			// Burn closest parts
			const closestParts = Workspace.GetPartBoundsInRadius(part.Position, 3.5, overlapParams);
			closestParts.forEach((part) => this.burn(part));
		});
	}
}


import { ReplicatedStorage } from "@rbxts/services";
import { HostedService } from "engine/shared/di/HostedService";
import { PlayerRank } from "engine/shared/PlayerRank";
import { PlayerWatcher } from "engine/shared/PlayerWatcher";

export class UsernameGuiController extends HostedService {
	constructor() {
		super();

		this.event.subscribeRegistration(() =>
			PlayerWatcher.onCharacterAdded((character, player) => {
				task.spawn(() => {
					task.wait(0.1);

					const head = character.WaitForChild("Head") as BasePart;

					const gui = ReplicatedStorage.Assets.Guis.UsernameGui.Clone();

					gui.DisplaynameLabel.Text = player.DisplayName;
					gui.UsernameLabel.Text = `@${player.Name}`;
					gui.Adornee = head;
					gui.Parent = head;
					gui.PlayerToHideFrom = player;

					if (PlayerRank.isAdmin(player)) {
						gui.RankLabel.Text = "Developer";
						task.spawn(() => {
							while (gui && gui.FindFirstChild("RankLabel")) {
								const t = 5;
								const hue = (tick() % t) / t;
								const colorrr = Color3.fromHSV(hue, 1, 1);
								gui.RankLabel.TextColor3 = colorrr;
								task.wait();
							}
						});
					}
				});
			}),
		);
	}
}


import { Workspace } from "@rbxts/services";
import { SharedPlots } from "shared/building/SharedPlots";
import type { PlayModeController } from "server/modes/PlayModeController";
import type { GenericBlockLogicCtor } from "shared/blockLogic/BlockLogic";

export abstract class ServerBlockLogic<T extends GenericBlockLogicCtor> {
	readonly logic;

	constructor(
		logic: T,
		private readonly playModeController: PlayModeController,
	) {
		this.logic = logic;
	}

	static staticIsValidBlockNamed(
		block: BasePart | undefined,
		player: Player | undefined,
		playModeController: PlayModeController,
		checkOwnership: boolean = true,
		checkRideMode: boolean = true,
	): string | undefined {
		if (!block) return "No block";
		if (!block.IsDescendantOf(Workspace)) return "Block not in workspace";

		if (player) {
			if (checkRideMode && playModeController.getPlayerMode(player) !== "ride") {
				return "Invalid mode";
			}

			if (checkOwnership) {
				const plot = SharedPlots.staticTryGetPlotByOwnerID(player.UserId);
				if (!plot) return "No plot";

				if (!block?.Anchored && !block?.AssemblyRootPart?.Anchored && !block.IsDescendantOf(plot)) {
					return "Block anchored";
				}
			}
		}
	}
	static staticIsValidBlock(
		block: BasePart | undefined,
		player: Player | undefined,
		playModeController: PlayModeController,
		checkOwnership: boolean = true,
		checkRideMode: boolean = true,
	): boolean {
		return (
			this.staticIsValidBlockNamed(block, player, playModeController, checkOwnership, checkRideMode) === undefined
		);
	}
	protected isValidBlock(
		block: BlockModel | undefined,
		player: Player | undefined,
		checkOwnership: boolean = true,
		checkRideMode: boolean = true,
	): boolean {
		if (!block?.PrimaryPart) return false;
		return ServerBlockLogic.staticIsValidBlock(
			block.PrimaryPart,
			player,
			this.playModeController,
			checkOwnership,
			checkRideMode,
		);
	}
}


import { TextService } from "@rbxts/services";
import { ServerBlockLogic } from "server/blocks/ServerBlockLogic";
import { ButtonBlock } from "shared/blocks/blocks/ButtonBlock";
import type { PlayModeController } from "server/modes/PlayModeController";
import type { ButtonBlockLogic } from "shared/blocks/blocks/ButtonBlock";

@injectable
export class ButtonServerLogic extends ServerBlockLogic<typeof ButtonBlockLogic> {
	constructor(logic: typeof ButtonBlockLogic, @inject playModeController: PlayModeController) {
		super(logic, playModeController);

		ButtonBlock.logic.events.updateText.addServerMiddleware((player, arg) => {
			if (player && arg.text) {
				const text = TextService.FilterStringAsync(
					arg.text,
					player.UserId,
					"PublicChat",
				).GetNonChatStringForUserAsync(player.UserId);

				return { success: true, value: { ...arg, text } };
			}

			return { success: true, value: arg };
		});
	}
}


import { TextService } from "@rbxts/services";
import { ServerBlockLogic } from "server/blocks/ServerBlockLogic";
import { ScreenBlock } from "shared/blocks/blocks/ScreenBlock";
import type { PlayModeController } from "server/modes/PlayModeController";
import type { ScreenBlockLogic } from "shared/blocks/blocks/ScreenBlock";

@injectable
export class ScreenServerLogic extends ServerBlockLogic<typeof ScreenBlockLogic> {
	constructor(logic: typeof ScreenBlockLogic, @inject playModeController: PlayModeController) {
		super(logic, playModeController);

		ScreenBlock.logic.events.update.addServerMiddleware((player, arg) => {
			const needsTranslating = () => {
				const data = arg.data;
				if (typeIs(data, "string")) return true;
				if (typeIs(data, "table")) return true;

				return false;
			};

			const translate = player && needsTranslating();
			if (player && translate) {
				const data = TextService.FilterStringAsync(
					logic.dataToString(arg.data),
					player.UserId,
					"PublicChat",
				).GetNonChatStringForUserAsync(player.UserId);

				return { success: true, value: { ...arg, data } };
			}

			return { success: true, value: arg };
		});
	}
}


import { Players } from "@rbxts/services";
import { Db } from "engine/server/Database";
import { t } from "engine/shared/t";
import { PlayerConfigUpdater } from "server/PlayerConfigVersioning";
import type { DatabaseBackend } from "engine/server/backend/DatabaseBackend";
import type { AchievementData } from "shared/AchievementData";

export type PlayerFeature = "lua_circuit";
export type PlayerDatabaseData = {
	readonly purchasedSlots?: number;
	readonly settings?: Partial<PlayerConfig>;
	readonly slots?: readonly SlotMeta[];
	readonly data?: Partial<OePlayerData>;
	readonly features?: readonly PlayerFeature[];
	readonly achievements?: { readonly [k in string]: AchievementData };
};

export const PlayerBanned = t.interface({
	errorCode: t.const("playerBanned"),
	reason: t.string,
	until: t.number.orUndefined(),
});
export type PlayerBanned = t.Type<typeof PlayerBanned>;

export const ServerError = t.interface({
	errorCode: t.string,
	message: t.string.orUndefined(),
});
export type ServerError = t.Type<typeof ServerError>;

export class PlayerDatabase {
	private readonly onlinePlayers = new Set<number>();
	private readonly db;

	constructor(private readonly datastore: DatabaseBackend<PlayerDatabaseData, [id: number]>) {
		this.db = new Db<PlayerDatabaseData, PlayerDatabaseData, [id: number]>(
			this.datastore,
			() => ({}),
			(data) => ({
				...data,
				settings: data.settings === undefined ? undefined : PlayerConfigUpdater.update(data.settings),
			}),
			(data) => data,
		);

		Players.PlayerAdded.Connect((plr) => this.onlinePlayers.add(plr.UserId));
		Players.PlayerRemoving.Connect((plr) => {
			this.onlinePlayers.delete(plr.UserId);

			// Roblox Stuido Local Server
			if (plr.UserId <= 0) return;

			this.db.save([plr.UserId]);
			this.db.free([plr.UserId]);
		});
	}

	get(userId: number) {
		return this.db.get([userId]);
	}

	set(userId: number, data: PlayerDatabaseData) {
		this.db.set([userId], data);

		if (!this.onlinePlayers.has(userId)) {
			this.db.save([userId]);
		}
	}
}


import { Players } from "@rbxts/services";
import { Db } from "engine/server/Database";
import { BlocksSerializer } from "shared/building/BlocksSerializer";
import { GameDefinitions } from "shared/data/GameDefinitions";
import { SlotsMeta } from "shared/SlotsMeta";
import type { DatabaseBackend } from "engine/server/backend/DatabaseBackend";
import type { PlayerDatabase } from "server/database/PlayerDatabase";
import type { LatestSerializedBlocks } from "shared/building/BlocksSerializer";

@injectable
export class SlotDatabase {
	private readonly onlinePlayers = new Set<number>();
	private readonly blocksdb;

	constructor(
		private readonly datastore: DatabaseBackend<
			BlocksSerializer.JsonSerializedBlocks,
			[ownerId: number, slotId: number]
		>,
		@inject private readonly players: PlayerDatabase,
	) {
		this.blocksdb = new Db<
			LatestSerializedBlocks,
			BlocksSerializer.JsonSerializedBlocks,
			[ownerId: number, slotId: number]
		>(
			this.datastore,
			() => ({ version: BlocksSerializer.latestVersion, blocks: [] }),
			(slot) => BlocksSerializer.jsonToObject(slot),
			(slot) => BlocksSerializer.objectToJson(slot),
		);

		Players.PlayerAdded.Connect((plr) => this.onlinePlayers.add(plr.UserId));
		Players.PlayerRemoving.Connect((plr) => {
			this.onlinePlayers.delete(plr.UserId);

			// Roblox Stuido Local Server
			if (plr.UserId <= 0) return;

			const id = tostring(plr.UserId);

			for (const [key, { keys }] of this.blocksdb.loadedUnsavedEntries()) {
				if (key.find(id + "_")[0] === undefined) {
					continue;
				}

				$log("Saving " + key);
				this.blocksdb.save(keys, key);
				this.blocksdb.free(keys, key);
			}
		});
	}

	private ensureValidSlotIndex(userId: number, index: number) {
		if (SlotsMeta.getSpecial(index)) return;

		const pdata = this.players.get(userId);
		const player = Players.GetPlayerByUserId(userId);
		if (!player) return;

		const maxSlots = GameDefinitions.getMaxSlots(player, pdata.purchasedSlots ?? 0);

		if (index >= 0 && index < maxSlots) {
			return;
		}

		if (SlotsMeta.isTestSlot(index)) {
			return;
		}

		throw "Invalid slot index " + index;
	}

	private getMeta(userId: number) {
		return this.players.get(userId).slots ?? [];
	}
	private setMeta(userId: number, slots: readonly SlotMeta[]) {
		this.players.set(userId, {
			...this.players.get(userId),
			slots,
		});

		if (!this.onlinePlayers.has(userId)) {
			for (const slot of slots) {
				this.blocksdb.save([userId, slot.index]);
				this.blocksdb.free([userId, slot.index]);
			}

			$log(`Saving data of the OFFLINE player ${userId}`);
		}
	}

	getBlocks(userId: number, index: number): LatestSerializedBlocks {
		this.ensureValidSlotIndex(userId, index);
		return this.blocksdb.get([userId, index]);
	}
	setBlocks(userId: number, index: number, blocks: LatestSerializedBlocks | undefined) {
		this.ensureValidSlotIndex(userId, index);

		blocks ??= { version: BlocksSerializer.latestVersion, blocks: [] };
		this.blocksdb.set([userId, index], blocks);

		const meta = [...this.getMeta(userId)];
		SlotsMeta.set(meta, {
			...SlotsMeta.get(meta, index),
			blocks: blocks.blocks.size(),
			saveTime: DateTime.now().UnixTimestampMillis,
			index,
		});
		this.setMeta(userId, meta);
	}
	setBlocksFromAnotherSlot(userId: number, index: number, indexfrom: number) {
		this.ensureValidSlotIndex(userId, index);
		this.ensureValidSlotIndex(userId, indexfrom);
		this.blocksdb.set([userId, index], this.getBlocks(userId, indexfrom));

		const meta = [...this.getMeta(userId)];
		SlotsMeta.set(meta, { ...SlotsMeta.get(meta, indexfrom), ...(SlotsMeta.getSpecialNoTest(index) ?? {}), index });
		this.setMeta(userId, meta);
	}

	updateMeta(userId: number, index: number, metaUpdate: (meta: readonly SlotMeta[]) => readonly SlotMeta[]): void {
		this.ensureValidSlotIndex(userId, index);

		const meta = metaUpdate(this.getMeta(userId));
		this.setMeta(userId, meta);
	}
	delete(userId: number, index: number): void {
		this.ensureValidSlotIndex(userId, index);

		this.blocksdb.delete([userId, index]);
		this.updateMeta(userId, index, (meta) => SlotsMeta.withRemovedSlot(meta, index));
	}
}


import { Players, RunService, Workspace } from "@rbxts/services";
import { HostedService } from "engine/shared/di/HostedService";
import { ServerBlockLogic } from "server/blocks/ServerBlockLogic";
import { ServerPartUtils } from "server/plots/ServerPartUtils";
import { BlockManager } from "shared/building/BlockManager";
import { RemoteEvents } from "shared/RemoteEvents";
import { CustomRemotes } from "shared/Remotes";
import { PartUtils } from "shared/utils/PartUtils";
import type { PlayModeController } from "server/modes/PlayModeController";
import type { ServerPlayersController } from "server/ServerPlayersController";
import type { SpreadingFireController } from "server/SpreadingFireController";
import type { ExplosionEffect } from "shared/effects/ExplosionEffect";
import type { ImpactSoundEffect } from "shared/effects/ImpactSoundEffect";
import type { ExplodeArgs } from "shared/RemoteEvents";

@injectable
export class UnreliableRemoteController extends HostedService {
	constructor(
		@inject impactSoundEffect: ImpactSoundEffect,
		@inject spreadingFire: SpreadingFireController,
		@inject explosionEffect: ExplosionEffect,
		@inject playModeController: PlayModeController,
		@inject private readonly playersController: ServerPlayersController,
	) {
		super();

		const serverBreakQueue: Set<BasePart> = new Set();

		const impactBreakEvent = (player: Player | undefined, parts: BasePart[]) => {
			if (!player) {
				for (const part of parts) {
					serverBreakQueue.add(part);
				}
				return;
			}

			task.spawn(() => {
				const players = this.playersController.getPlayers().filter((p) => p !== player);
				CustomRemotes.physics.normalizeRootparts.send(players, { parts });

				for (const part of parts) {
					if (!BlockManager.isActiveBlockPart(part)) continue;
					ServerPartUtils.BreakJoints(part);
				}

				impactSoundEffect.send(parts[0], { blocks: parts, index: undefined });
			});
		};

		this.event.subscribe(RunService.Heartbeat, () => {
			if (serverBreakQueue.size() > 0) {
				const copy = [...serverBreakQueue];
				serverBreakQueue.clear();

				task.spawn(() => {
					const toSend = new Map<Player | 0, BasePart[]>();

					for (const block of copy) {
						impactSoundEffect.send(block, { blocks: [block], index: undefined });
						ServerPartUtils.BreakJoints(block);

						const owner = block.IsDescendantOf(Workspace) ? block.GetNetworkOwner() : undefined;
						toSend.getOrSet(owner ?? 0, () => []).push(block);
					}

					const players = this.playersController.getPlayers();
					for (const [player, parts] of toSend) {
						let sendTo = players;
						if (player !== 0) sendTo = players.except([player]);

						CustomRemotes.physics.normalizeRootparts.send(sendTo, { parts });
					}
				});
			}
		});

		const burnEvent = (parts: BasePart[]) => {
			parts.forEach((part) => {
				if (!BlockManager.isActiveBlockPart(part)) return;

				spreadingFire.burn(part);
			});
		};

		// TODO: Change this for some offensive update
		const explode = (player: Player | undefined, { part, isFlammable, pressure, radius }: ExplodeArgs) => {
			if (!ServerBlockLogic.staticIsValidBlock(part, player, playModeController)) {
				return;
			}

			radius = math.clamp(radius, 0, 16);
			pressure = math.clamp(pressure, 0, 2500);

			const hitParts = Workspace.GetPartBoundsInRadius(part.Position, radius);

			if (isFlammable) {
				const flameHitParts = Workspace.GetPartBoundsInRadius(part.Position, radius * 1.5);

				flameHitParts.forEach((part) => {
					if (math.random(1, 8) === 1) {
						spreadingFire.burn(part);
					}
				});
			}

			hitParts.forEach((part) => {
				if (!BlockManager.isActiveBlockPart(part)) {
					return;
				}

				if (math.random(1, 2) === 1) {
					const players = Players.GetPlayers().filter((p) => p !== player);
					CustomRemotes.physics.normalizeRootparts.send(players, { parts: [part] });
					ServerPartUtils.BreakJoints(part);
				}

				part.Velocity = new Vector3(
					math.random(0, pressure / 40),
					math.random(0, pressure / 40),
					math.random(0, pressure / 40),
				);
			});

			part.Transparency = 1;
			PartUtils.applyToAllDescendantsOfType("Decal", part, (decal) => decal.Destroy());

			// Explosion sound
			explosionEffect.send(part, { part, index: undefined });
		};

		this.event.subscribe(RemoteEvents.ImpactBreak.invoked, impactBreakEvent);
		this.event.subscribe(RemoteEvents.Burn.invoked, (_, parts) => burnEvent(parts));
		this.event.subscribe(RemoteEvents.Explode.invoked, explode);
	}
}


import { Players, ReplicatedStorage } from "@rbxts/services";
import { Component } from "engine/shared/component/Component";
import { ComponentChild } from "engine/shared/component/ComponentChild";
import { InstanceComponent } from "engine/shared/component/InstanceComponent";
import { PlayerRank } from "engine/shared/PlayerRank";
import type { SharedPlot } from "shared/building/SharedPlot";
import type { SharedPlots } from "shared/building/SharedPlots";

@injectable
export class PlotsFloatingImageController extends Component {
	constructor(@inject plots: SharedPlots) {
		super();

		for (const plot of plots.plots) {
			this.parent(new PlotFloatingImageController(plot));
		}
	}
}
class PlotFloatingImageController extends Component {
	constructor(plot: SharedPlot) {
		super();

		const container = this.parent(new ComponentChild());
		const create = (player: Player) => {
			const gui = ReplicatedStorage.Assets.Guis.PlotOwnerGui.Clone();
			gui.UserImage.Image = Players.GetUserThumbnailAsync(
				player.UserId,
				Enum.ThumbnailType.AvatarThumbnail,
				Enum.ThumbnailSize.Size420x420,
			)[0];
			gui.DisplayNameLabel.Text = player.DisplayName;
			gui.UsernameLabel.Text = `@${player.Name}`;
			gui.Parent = plot.instance;
			gui.Adornee = plot.instance.FindFirstChild("BuildingArea") as BasePart;

			if (PlayerRank.isAdmin(player)) {
				gui.RankLabel.Text = "Developer";
				spawn(() => {
					while (gui && gui.FindFirstChild("RankLabel")) {
						const t = 5;
						const hue = (tick() % t) / t;
						const colorrr = Color3.fromHSV(hue, 1, 1);
						gui.RankLabel.TextColor3 = colorrr;
						task.wait();
					}
				});
			}

			return new InstanceComponent(gui);
		};

		this.event.subscribeObservable(plot.ownerId, (owner) => {
			container.clear();

			if (owner !== undefined) {
				container.set(create(Players.GetPlayerByUserId(owner)!));
			}
		});
	}
}