import { UserInputService } from "@rbxts/services";
import { Color4Chooser } from "client/gui/Color4Chooser";
import { BlockPipetteButton } from "client/gui/controls/BlockPipetteButton";
import { MaterialChooser } from "client/gui/MaterialChooser";
import { TextButtonControl } from "engine/client/gui/Button";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import { Component } from "engine/shared/component/Component";
import { InstanceComponent } from "engine/shared/component/InstanceComponent";
import { Transforms } from "engine/shared/component/Transforms";
import { TransformService } from "engine/shared/component/TransformService";
import { Materials } from "engine/shared/data/Materials";
import { ArgsSignal } from "engine/shared/event/Signal";
import { SubmittableValue } from "engine/shared/event/SubmittableValue";
import { Color4 } from "shared/Color4";
import { Colors } from "shared/Colors";
import type { Color4ChooserDefinition } from "client/gui/Color4Chooser";
import type { MaterialChooserDefinition } from "client/gui/MaterialChooser";
import type { TextButtonDefinition } from "engine/client/gui/Button";
import type { ObservableValue } from "engine/shared/event/ObservableValue";
import type { ReadonlyObservableValue } from "engine/shared/event/ObservableValue";

export type MaterialColorEditControlDefinition = GuiObject & {
	readonly Color: TextButtonDefinition & {
		readonly Pipette: GuiButton & {
			readonly ImageLabel: ImageLabel;
		};
		readonly Arrow: GuiObject;
	};
	readonly Material: TextButtonDefinition & {
		readonly Pipette: ImageButton & {
			readonly ImageLabel: ImageLabel;
		};
		readonly Arrow: GuiObject;
	};
};

class ResizableWindow<T extends GuiObject & { readonly TextLabel: TextLabel }> extends Control<T> {
	constructor(gui: T, minSize: number, maxSize: number) {
		super(gui);

		class Dragger extends Component {
			readonly dragStarted;
			readonly dragged;
			readonly dragEnded;

			constructor(instance: GuiObject) {
				super();

				const dragStarted = new ArgsSignal();
				this.dragStarted = dragStarted.asReadonly();
				const dragEnded = new ArgsSignal();
				this.dragEnded = dragEnded.asReadonly();

				const dragged = new ArgsSignal<[delta: Vector2]>();
				this.dragged = dragged.asReadonly();

				let isInside = false;
				this.onDisable(() => (isInside = false));
				this.event.subscribe(instance.MouseEnter, () => (isInside = true));
				this.event.subscribe(instance.MouseLeave, () => (isInside = false));

				let isMoving = false;
				this.onDisable(() => (isMoving = false));
				this.event.subInput((ih) => {
					let start: Vector2 | undefined;

					ih.onMouse1Down(() => {
						if (!isInside) return;
						dragStarted.Fire();
						isMoving = true;
						start = UserInputService.GetMouseLocation();
					}, true);
					ih.onMouse1Up(() => {
						if (!isMoving) return;

						isMoving = false;
						dragEnded.Fire();
					}, true);

					ih.onMouseMove((input) => {
						if (!isMoving) return;

						start ??= UserInputService.GetMouseLocation();
						const delta = start.sub(UserInputService.GetMouseLocation());
						dragged.Fire(delta);
					}, true);
				});
			}
		}
		const dragger = this.parent(new Dragger(gui.TextLabel));

		let startPos = 0;
		dragger.dragStarted.Connect(() => (startPos = gui.Size.Y.Offset));
		dragger.dragged.Connect(({ Y: y }) => {
			y = math.clamp(startPos + y, minSize, maxSize);
			gui.Size = new UDim2(gui.Size.X, new UDim(gui.Size.Y.Scale, y));
		});
	}
}

export type ColorWindowDefinition = GuiObject & {
	readonly TextLabel: TextLabel;
	readonly Content: GuiObject & {
		readonly Control: Color4ChooserDefinition;
	};
};
class ColorWindow extends Control<ColorWindowDefinition> {
	constructor(gui: ColorWindowDefinition, value: SubmittableValue<Color4>) {
		super(gui);

		const v = new SubmittableValue<Color4>(value.value);
		this.event.subscribeObservable(v.value, (val) => value.set(val));
		this.event.subscribeObservable(value.value, (val) => v.set(val));
		v.submitted.Connect((v) => value.submit(v));

		this.parent(new Color4Chooser(gui.Content.Control, v, true));
	}
}

export type MaterialWindowDefinition = GuiObject & {
	readonly TextLabel: TextLabel;
	readonly Search: TextBox;
	readonly Content: MaterialChooserDefinition;
};
class MaterialWindow extends ResizableWindow<MaterialWindowDefinition> {
	constructor(
		gui: MaterialWindowDefinition,
		value: SubmittableValue<Enum.Material>,
		color: ReadonlyObservableValue<Color4>,
	) {
		super(gui, 100, 504);

		const search = this.event.observableFromInstanceParam(gui.Search, "Text");
		this.parent(new MaterialChooser(gui.Content, value, color, search));
	}
}

/** Material preview with an edit button */
export class MaterialColorEditControl extends InstanceComponent<MaterialColorEditControlDefinition> {
	static autoCreate(showOnEnable: boolean = false) {
		const template = Interface.getInterface<{
			Tools: { Shared: { Bottom: { Dropups: MaterialColorEditControlDefinition } } };
		}>().Tools.Shared.Bottom.Dropups;
		const dropups = template.Clone();
		dropups.Visible = true;

		return new MaterialColorEditControl(dropups, showOnEnable);
	}

	readonly materialPipette;
	readonly colorPipette;

	readonly materialv;
	readonly colorv;

	constructor(gui: MaterialColorEditControlDefinition, showOnEnable: boolean = false) {
		super(gui);

		const floatingTemplate = Interface.getInterface<{
			readonly Floating: {
				readonly Color: ColorWindowDefinition;
				readonly Material: MaterialWindowDefinition;
			};
		}>().Floating;

		const materialv = SubmittableValue.from<Enum.Material>(Enum.Material.Plastic);
		this.materialv = materialv.asHalfReadonly();

		const colorv = SubmittableValue.from<Color4>({ color: Color3.fromRGB(255, 255, 255), alpha: 1 });
		this.colorv = colorv.asHalfReadonly();

		const materialbtn = this.parent(new TextButtonControl(this.instance.Material));
		this.event.subscribeObservable(
			materialv.value,
			(value) => {
				this.instance.Material.Pipette.Image = Materials.getMaterialTextureAssetId(value);
				MaterialChooser.setColorOfPreview(colorv.get(), this.instance.Material.Pipette);
				materialbtn.text.set(Materials.getMaterialDisplayName(value).upper());
			},
			true,
		);

		const colorbtn = this.parent(new TextButtonControl(this.instance.Color));
		this.event.subscribeObservable(
			colorv.value,
			(value) => colorbtn.text.set("#" + Color4.toHex(value).upper()),
			true,
		);
		this.event.subscribeObservable(
			colorv.value,
			(value) => {
				this.instance.Color.Pipette.BackgroundColor3 = value.color;
				MaterialChooser.setColorOfPreview(value, this.instance.Material.Pipette);

				const imgColor = value.color.ToHSV()[2] > 0.5 ? Colors.black : Colors.white;
				if (this.instance.Color.Pipette.ImageLabel.ImageColor3 !== imgColor) {
					TransformService.cancel(this.instance.Color.Pipette.ImageLabel);
					TransformService.run(this.instance.Color.Pipette.ImageLabel, (tr) =>
						tr.transform(
							this.instance.Color.Pipette.ImageLabel,
							"ImageColor3",
							imgColor,
							Transforms.quadOut02,
						),
					);
				}
			},
			true,
		);

		{
			const gui = floatingTemplate.Color.Clone();
			gui.Parent = colorbtn.instance;
			gui.AnchorPoint = new Vector2(0, 1);
			gui.Position = new UDim2(0, 0, 0, -4);

			const wnd = this.parentDestroyOnly(new ColorWindow(gui, colorv));
			this.onDisable(() => wnd.setVisibleAndEnabled(false));
			if (showOnEnable) {
				this.onEnable(() => wnd.setVisibleAndEnabled(true));
			}
			colorbtn.addButtonAction(() => wnd.setVisibleAndEnabled(!wnd.isInstanceVisible()));
			this.event.subscribeObservable(
				wnd.visibilityComponent().visible,
				(visible) => {
					Transforms.create()
						.transform(colorbtn.instance.Arrow, "Rotation", visible ? 180 : 0, Transforms.quadOut02)
						.run(wnd, true);
				},
				true,
			);
		}
		{
			const gui = floatingTemplate.Material.Clone();
			gui.Parent = materialbtn.instance;
			gui.AnchorPoint = new Vector2(0, 1);
			gui.Position = new UDim2(0, 0, 0, -4);

			const wnd = this.parentDestroyOnly(new MaterialWindow(gui, materialv, colorv.value));
			this.onDisable(() => wnd.setVisibleAndEnabled(false));
			if (showOnEnable) {
				this.onEnable(() => wnd.setVisibleAndEnabled(true));
			}
			materialbtn.addButtonAction(() => wnd.setVisibleAndEnabled(!wnd.isInstanceVisible()));
			this.event.subscribeObservable(
				wnd.visibilityComponent().visible,
				(visible) => {
					Transforms.create()
						.transform(materialbtn.instance.Arrow, "Rotation", visible ? 180 : 0, Transforms.quadOut02)
						.run(wnd, true);
				},
				true,
			);
		}

		this.materialPipette = this.parent(
			BlockPipetteButton.forMaterial(this.instance.Material.Pipette, (m) => {
				materialv.submit(m);
			}),
		);
		this.colorPipette = this.parent(
			BlockPipetteButton.forColor(this.instance.Color.Pipette, (c) => colorv.submit(c)),
		);
	}

	autoSubscribe(material: ObservableValue<Enum.Material>, color: ObservableValue<Color4>) {
		this.event.subscribeObservable(material, (m) => this.materialv.set(m), true, true);
		this.event.subscribe(this.materialv.submitted, (v) => material.set(v));

		this.event.subscribeObservable(color, (m) => this.colorv.set(m), true, true);
		this.event.subscribe(this.colorv.submitted, (v) => color.set(v));
	}
}


import { ContextActionService, UserInputService } from "@rbxts/services";
import { Anim } from "client/gui/Anim";
import { Color4Chooser } from "client/gui/Color4Chooser";
import { ColorVisualizerWithAlpha } from "client/gui/ColorVisualizerWithAlpha";
import { ConfigControlBase } from "client/gui/configControls/ConfigControlBase";
import { Color4TextBox } from "client/gui/controls/Color4TextBox";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import { Observables } from "engine/shared/event/Observables";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { SubmittableValue } from "engine/shared/event/SubmittableValue";
import type { Color4ChooserDefinition as Color4ChooserDefinition } from "client/gui/Color4Chooser";
import type { ColorVisualizerWithAlphaDefinition } from "client/gui/ColorVisualizerWithAlpha";
import type { ConfigControlBaseDefinition } from "client/gui/configControls/ConfigControlBase";
import type { PopupController } from "client/gui/PopupController";

class ColorControl extends Control<ConfigControlColorDefinition["Control"]> {
	readonly value;

	constructor(gui: ConfigControlColorDefinition["Control"], defaultColor: Color4, allowAlpha: boolean) {
		super(gui);

		const v = new SubmittableValue(new ObservableValue<Color4>(defaultColor));
		this.value = v.asHalfReadonly();

		this.parent(new Color4TextBox(gui.RGBA, v, allowAlpha));
		this.parent(new ColorVisualizerWithAlpha(gui.Preview, v.value));

		this.$onInjectAuto((popupController: PopupController) => {
			const scale = (Anim.findScreen(gui)?.FindFirstChild("UIScale") as UIScale | undefined)?.Scale ?? 1;

			this.parent(new Control(gui.EditControl)) //
				.addButtonAction(() => {
					const mousePos = UserInputService.GetMouseLocation().div(scale);

					const template = Interface.getInterface<{
						Floating: {
							Color: GuiObject & { Content: GuiObject & { Control: Color4ChooserDefinition } };
						};
					}>().Floating.Color;
					const colorGui = template.Clone();
					colorGui.Position = new UDim2(0, mousePos.X, 0, mousePos.Y);

					const window = new Control(colorGui);
					const color = window.parent(new Color4Chooser(colorGui.Content.Control, v, allowAlpha));

					const popup = popupController.showPopup(window);

					const fitToScreen = (instance: GuiObject) => {
						const min = instance.AbsolutePosition;
						if (min.X < 0) {
							instance.Position = instance.Position.add(new UDim2(0, -min.X, 0, 0));
						}
						if (min.Y < 0) {
							instance.Position = instance.Position.add(new UDim2(0, 0, 0, -min.Y));
						}

						const screen = instance.FindFirstAncestorWhichIsA("ScreenGui");
						if (!screen) return;
						const scale = screen.FindFirstChild("UIScale") as UIScale | undefined;
						if (!scale) return;

						const screenSize = screen.AbsoluteSize.add(new Vector2(0, -40));
						const max = instance.AbsolutePosition.add(instance.AbsoluteSize);

						if (max.X > screenSize.X) {
							instance.Position = instance.Position.add(
								new UDim2(0, (screenSize.X - max.X) / scale.Scale, 0, 0),
							);
						}
						if (max.Y > screenSize.Y) {
							instance.Position = instance.Position.add(
								new UDim2(0, 0, 0, (screenSize.Y - max.Y) / scale.Scale),
							);
						}
					};
					fitToScreen(colorGui);

					let isInside = false;
					colorGui.MouseEnter.Connect(() => (isInside = true));
					colorGui.MouseLeave.Connect(() => (isInside = false));

					ContextActionService.BindAction(
						"everything",
						() => Enum.ContextActionResult.Sink,
						false,
						Enum.UserInputType.Keyboard,
						Enum.UserInputType.Gamepad1,
					);
					popup.onDestroy(() => ContextActionService.UnbindAction("everything"));

					popup.event.subInput((ih) => {
						task.delay(0, () => {
							ih.onTouchTap(() => {
								const mouse = Interface.mouse;
								const objects = Interface.getPlayerGui().GetGuiObjectsAtPosition(mouse.X, mouse.Y);
								if (objects.contains(colorGui)) return;

								popup.destroy();
							});
						});

						ih.onMouse1Down(() => {
							if (isInside) return;
							popup.destroy();
						}, true);
					});
				});
		});

		this.parent(new Control(gui.UnsetControl)) //
			.addButtonAction(() => v.submit(defaultColor));
	}
}

declare module "client/gui/configControls/ConfigControlsList" {
	export interface ConfigControlTemplateList {
		readonly Color: ConfigControlColorDefinition;
	}
}

export type ConfigControlColorDefinition = ConfigControlBaseDefinition & {
	readonly Control: GuiObject & {
		readonly Preview: ColorVisualizerWithAlphaDefinition;
		readonly RGBA: TextBox;
		readonly EditControl: GuiButton;
		readonly UnsetControl: GuiButton;
	};
};
export class ConfigControlColor extends ConfigControlBase<ConfigControlColorDefinition, Color4> {
	constructor(gui: ConfigControlColorDefinition, name: string, defaultColor: Color4, alpha = false) {
		super(gui, name);

		const control = this.parent(new ColorControl(gui.Control, defaultColor, alpha));

		this.initFromMultiWithDefault(control.value.value, () => defaultColor);
		this.event.subscribe(control.value.submitted, (value) => this.submit(this.multiMap(() => value)));
	}

	initColor(
		observable: ObservableValue<object>,
		colorPath: readonly string[],
		transparencyPath: readonly string[],
		updateType: "value" | "submit" = "submit",
	): this {
		const color = this.event.addObservable(
			Observables.createObservableFromObjectProperty<Color3>(observable, colorPath),
		);
		let alpha = this.event.addObservable(
			Observables.createObservableFromObjectProperty<number>(observable, transparencyPath),
		);
		alpha = this.event.addObservable(
			alpha.fCreateBased(
				(c) => 1 - c,
				(c) => 1 - c,
			),
		);

		const stuff = this.event.addObservable(Observables.createObservableFromMultiple({ color, alpha }));

		return this.initToObservable(stuff, updateType);
	}
}

export class ConfigControlColor3 extends ConfigControlBase<ConfigControlColorDefinition, Color3> {
	constructor(gui: ConfigControlColorDefinition, name: string, defaultColor: Color3) {
		super(gui, name);

		const control = this.parent(new ColorControl(gui.Control, { alpha: 1, color: defaultColor }, false));

		const c3 = this.event.addObservable(
			control.value.value.fCreateBased(
				(c) => c.color,
				(c) => ({ alpha: 1, color: c }),
			),
		);
		this.initFromMultiWithDefault(c3, () => defaultColor);
		this.event.subscribe(control.value.submitted, (value) => this.submit(this.multiMap(() => value.color)));
	}
}


import { ContextActionService, UserInputService } from "@rbxts/services";
import { Anim } from "client/gui/Anim";
import { ConfigControlBase } from "client/gui/configControls/ConfigControlBase";
import { MaterialChooser } from "client/gui/MaterialChooser";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import { Materials } from "engine/shared/data/Materials";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { SubmittableValue } from "engine/shared/event/SubmittableValue";
import type { ConfigControlBaseDefinition } from "client/gui/configControls/ConfigControlBase";
import type { MaterialChooserDefinition } from "client/gui/MaterialChooser";
import type { PopupController } from "client/gui/PopupController";

class MaterialControl extends Control<ConfigControlMaterialDefinition["Control"]> {
	readonly value;

	constructor(gui: ConfigControlMaterialDefinition["Control"], defaultMaterial: Enum.Material) {
		super(gui);

		const v = new SubmittableValue(new ObservableValue<Enum.Material>(defaultMaterial));
		this.value = v.asHalfReadonly();

		const preview = (gui.Preview, v.value);
		v.value.subscribe((m) => (gui.Preview.ImageLabel.Image = Materials.getMaterialTextureAssetId(m)), true);

		this.$onInjectAuto((popupController: PopupController) => {
			const scale = (Anim.findScreen(gui)?.FindFirstChild("UIScale") as UIScale | undefined)?.Scale ?? 1;

			this.parent(new Control(gui.EditControl)) //
				.addButtonAction(() => {
					const mousePos = UserInputService.GetMouseLocation().div(scale);

					const template = Interface.getInterface<{
						Floating: {
							Material: GuiObject & {
								readonly Search: TextBox;
								readonly Content: MaterialChooserDefinition;
							};
						};
					}>().Floating.Material;
					const colorGui = template.Clone();
					colorGui.Position = new UDim2(0, mousePos.X, 0, mousePos.Y);

					const window = new Control(colorGui);
					const msearch = this.event.observableFromInstanceParam(colorGui.Search, "Text");
					const color = window.parent(new MaterialChooser(colorGui.Content, v, undefined, msearch));

					const popup = popupController.showPopup(window);

					let isInside = false;
					colorGui.MouseEnter.Connect(() => (isInside = true));
					colorGui.MouseLeave.Connect(() => (isInside = false));

					ContextActionService.BindAction(
						"everything",
						() => Enum.ContextActionResult.Sink,
						false,
						Enum.UserInputType.Keyboard,
						Enum.UserInputType.Gamepad1,
					);
					popup.onDestroy(() => ContextActionService.UnbindAction("everything"));

					popup.event.subInput((ih) => {
						task.delay(0, () => {
							ih.onTouchTap(() => {
								const mouse = Interface.mouse;
								const objects = Interface.getPlayerGui().GetGuiObjectsAtPosition(mouse.X, mouse.Y);
								if (objects.contains(colorGui)) return;

								popup.destroy();
							});
						});

						ih.onMouse1Down(() => {
							if (isInside) return;
							popup.destroy();
						}, true);
					});
				});
		});

		this.parent(new Control(gui.UnsetControl)) //
			.addButtonAction(() => v.submit(defaultMaterial));
	}
}

declare module "client/gui/configControls/ConfigControlsList" {
	export interface ConfigControlTemplateList {
		readonly Material: ConfigControlMaterialDefinition;
	}
}

export type ConfigControlMaterialDefinition = ConfigControlBaseDefinition & {
	readonly Control: GuiObject & {
		readonly Preview: GuiObject & {
			readonly ImageLabel: ImageLabel;
		};
		readonly EditControl: GuiButton;
		readonly UnsetControl: GuiButton;
	};
};
export class ConfigControlMaterial extends ConfigControlBase<ConfigControlMaterialDefinition, Enum.Material> {
	constructor(gui: ConfigControlMaterialDefinition, name: string, defaultMaterial: Enum.Material) {
		super(gui, name);

		const control = this.parent(new MaterialControl(gui.Control, defaultMaterial));

		this.initFromMultiWithDefault(control.value.value, () => defaultMaterial);
		this.event.subscribe(control.value.submitted, (value) => this.submit(this.multiMap(() => value)));
	}
}


import { Players, UserInputService } from "@rbxts/services";
import { HoveredPartHighlighter } from "client/tools/highlighters/HoveredPartHighlighter";
import { ButtonControl } from "engine/client/gui/Button";
import { Interface } from "engine/client/gui/Interface";
import { Element } from "engine/shared/Element";
import { EventHandler } from "engine/shared/event/EventHandler";
import { SlimFilter } from "engine/shared/event/SlimFilter";
import { SlimSignal } from "engine/shared/event/SlimSignal";
import { PlayerRank } from "engine/shared/PlayerRank";
import { BlockManager } from "shared/building/BlockManager";
import { BuildingManager } from "shared/building/BuildingManager";
import { Colors } from "shared/Colors";
import type { ButtonDefinition } from "engine/client/gui/Button";

export class BlockPipetteButton extends ButtonControl {
	readonly onStart = new SlimSignal();
	readonly onEnd = new SlimSignal();
	readonly onSelect = new SlimSignal<(part: BasePart | BlockModel) => void>();
	readonly filter = new SlimFilter<(part: BasePart | BlockModel) => boolean>();

	constructor(gui: ButtonDefinition) {
		super(gui);

		let stop: (() => void) | undefined;

		const g = (part: BasePart): BasePart | BlockModel | undefined => {
			if (!this.filter.Fire(part)) return;
			return BlockManager.tryGetBlockModelByPart(part) ?? part;
		};

		this.onDisable(() => stop?.());
		this.activated.Connect(() => {
			if (stop) {
				stop?.();
				return;
			}

			const visualFrame = Element.create(
				"Frame",
				{
					Size: new UDim2(1, 0, 1, 0),
					BackgroundColor3: Colors.accentLight,
					Parent: gui,
				},
				{ corner: Element.create("UICorner", { CornerRadius: new UDim(0, 8) }) },
			);

			this.onStart.Fire();

			const visualizer = new HoveredPartHighlighter(g);
			visualizer.enable();

			const eh = new EventHandler();
			stop = () => {
				visualFrame.Destroy();

				visualizer.destroy();
				eh.unsubscribeAll();
				this.onEnd.Fire();
				stop = undefined;
			};

			eh.subscribe(UserInputService.InputEnded, (input, gameProcessed) => {
				if (gameProcessed) return;
				if (Interface.isCursorOnVisibleGui()) return;
				if (input.UserInputType !== Enum.UserInputType.MouseButton1) return;

				const selected = visualizer.highlightedPart.get();
				if (!selected) return stop?.();

				this.onSelect.Fire(selected);
				stop?.();
			});
			eh.subscribe(UserInputService.TouchTap, (positions, gameProcessed) => {
				if (gameProcessed) return;
				if (Interface.isCursorOnVisibleGui()) return;

				let selected: BasePart | BlockModel | undefined = Players.LocalPlayer.GetMouse().Target;
				selected = selected && g(selected);
				if (!selected) return stop?.();

				this.onSelect.Fire(selected);
				stop?.();
			});
		});
	}

	static forMaterial(gui: ButtonDefinition, clicked: (material: Enum.Material) => void) {
		const pipette = new BlockPipetteButton(gui);

		const getMaterial = (part: BasePart | BlockModel) => {
			if (part.IsA("BasePart")) {
				return part.Material;
			}

			return BlockManager.manager.material.get(part);
		};
		pipette.onSelect.Connect((part) => clicked(getMaterial(part)));
		if (!PlayerRank.isAdmin(Players.LocalPlayer)) {
			pipette.filter.add((part) => BuildingManager.AllowedMaterials.includes(getMaterial(part)));
		}

		return pipette;
	}
	static forColor(gui: ButtonDefinition, clicked: (color: Color4) => void) {
		const pipette = new BlockPipetteButton(gui);
		pipette.onSelect.Connect((part) => {
			if (part.IsA("BasePart")) {
				clicked({ color: part.Color, alpha: 1 });
			} else {
				const data = BlockManager.manager.color.get(part);
				clicked(data);
			}
		});

		return pipette;
	}
	static forBlockId(gui: ButtonDefinition, clicked: (id: BlockId) => void) {
		const pipette = new BlockPipetteButton(gui);
		pipette.onSelect.Connect((part) => {
			if (part.IsA("BasePart")) {
				throw "What.";
			}

			const data = BlockManager.manager.id.get(part);
			clicked(data);
		});
		pipette.filter.add((part) => BlockManager.isBlockPart(part));

		return pipette;
	}
}


import { ContextActionService } from "@rbxts/services";
import { SelectButtonPopup, SelectButtonPopupWithCustomString } from "client/gui/popup/SelectButtonPopup";
import { Control } from "engine/client/gui/Control";
import { InputController } from "engine/client/InputController";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { Signal } from "engine/shared/event/Signal";
import { Colors } from "shared/Colors";
import type { PopupController } from "client/gui/PopupController";

export type KeyChooserControlDefinition = TextButton;

type ToStr<NonString extends boolean> = NonString extends false ? KeyCode : KeyCode | string;
/** Control that represents a key */
class _KeyChooserControl<TKC extends boolean> extends Control<KeyChooserControlDefinition> {
	readonly submitted = new Signal<(value: ToStr<TKC>, prev: ToStr<TKC>) => void>();
	readonly value = new ObservableValue<ToStr<TKC>>("P");

	constructor(
		gui: KeyChooserControlDefinition,
		touchChooserCtor: TKC extends true ? typeof SelectButtonPopupWithCustomString : typeof SelectButtonPopup,
	) {
		super(gui);

		const buttonColor = this.gui.BackgroundColor3;
		const buttonColorActive = Colors.lightenPressed(this.gui.BackgroundColor3);

		this.value.subscribe((value) => (this.gui.Text = value === "Unknown" ? "" : value), true);

		this.$onInjectAuto((popupController: PopupController) => {
			this.gui.Activated.Connect(() => {
				if (InputController.inputType.get() === "Touch") {
					const p = new touchChooserCtor(
						(key) => {
							const prev = this.value.get();
							this.value.set(key as ToStr<TKC>);
							this.submitted.Fire(key as ToStr<TKC>, prev);
						},
						() => {},
					);

					popupController.showPopup(p);
				} else {
					this.gui.BackgroundColor3 = buttonColorActive;

					const actionName = "peKeySelection";
					ContextActionService.BindActionAtPriority(
						actionName,
						(name, state, input) => {
							if (actionName === name) {
								if (input.KeyCode === Enum.KeyCode.Escape || input.KeyCode === Enum.KeyCode.Unknown) {
									return Enum.ContextActionResult.Sink;
								}

								ContextActionService.UnbindAction(actionName);

								const prev = this.value.get();
								this.value.set(input.KeyCode.Name);
								this.submitted.Fire(input.KeyCode.Name, prev);
								this.gui.BackgroundColor3 = buttonColor;
							}
						},
						false,
						2000 + 1,
						Enum.UserInputType.Keyboard,
						Enum.UserInputType.Gamepad1,
					);

					this.onDisable(() => ContextActionService.UnbindAction(actionName));
				}
			});
		});
	}
}

export class KeyChooserControl extends _KeyChooserControl<false> {
	constructor(gui: KeyChooserControlDefinition) {
		super(gui, SelectButtonPopup);
	}
}
export class KeyOrStringChooserControl extends _KeyChooserControl<true> {
	constructor(gui: KeyChooserControlDefinition) {
		super(gui, SelectButtonPopupWithCustomString);
	}
}


import { Players, RunService } from "@rbxts/services";
import { ButtonControl } from "engine/client/gui/Button";
import { Control } from "engine/client/gui/Control";
import { ComponentKeyedChildren } from "engine/shared/component/ComponentKeyedChildren";
import { Transforms } from "engine/shared/component/Transforms";
import { ObservableCollectionSet } from "engine/shared/event/ObservableCollection";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { ArgsSignal } from "engine/shared/event/Signal";
import { PlayerRank } from "engine/shared/PlayerRank";

type FakePlayer = {
	readonly UserId: number;
	readonly DisplayName: string;
	readonly Name: string;
};

type PlayerContainerDefinition = Frame & {
	readonly TextButton: TextButton & {
		readonly ImageLabel: ImageLabel;
		readonly Texts: GuiObject & {
			readonly TitleLabel: TextLabel;
			readonly UsernameLabel: TextLabel;
		};
	};
};
class PlayerContainer extends Control<PlayerContainerDefinition> {
	readonly clicked;

	constructor(
		gui: PlayerContainerDefinition,
		readonly player: FakePlayer,
	) {
		super(gui);

		gui.TextButton.Texts.TitleLabel.Text = player.DisplayName;
		gui.TextButton.Texts.UsernameLabel.Text = `@${player.Name}`;
		task.spawn(() => {
			gui.TextButton.ImageLabel.Image = Players.GetUserThumbnailAsync(
				player.UserId,
				Enum.ThumbnailType.HeadShot,
				Enum.ThumbnailSize.Size100x100,
			)[0];
		});

		const button = this.add(new ButtonControl(gui.TextButton));
		this.clicked = button.activated;
	}
}

export type PlayerSelectorColumnControlDefinition = Frame & {
	readonly Left: Frame & {
		readonly Container: PlayerContainerDefinition;
	};
	readonly Right: Frame;
};

export class PlayerSelectorColumnControl extends Control<PlayerSelectorColumnControlDefinition> {
	private readonly playerTemplate;

	private readonly leftControl;
	private readonly rightControl;

	private updating = false;
	readonly publicValue = new ObservableValue<ReadonlySet<number>>(new ReadonlySet<number>());
	private readonly value = new ObservableCollectionSet<number>();
	readonly submitted = new ArgsSignal<[players: ReadonlySet<number>]>();

	constructor(gui: PlayerSelectorColumnControlDefinition) {
		super(gui);

		this.playerTemplate = this.asTemplate(gui.Left.Container, true);
		this.leftControl = this.parent(
			new ComponentKeyedChildren<FakePlayer, PlayerContainer>().withParentInstance(gui.Left),
		);
		this.rightControl = this.parent(
			new ComponentKeyedChildren<FakePlayer, PlayerContainer>().withParentInstance(gui.Right),
		);

		this.onEnable(() => this.updateOnlinePlayers());
		this.publicValue.changed.Connect((v) => {
			if (this.updating) return;
			this.value.set(v);
			this.updateOnlinePlayers();
		});
		this.event.subscribe(Players.PlayerAdded, (player) => this.addPlayer(player));
		this.event.subscribe(Players.PlayerRemoving, (player) => this.removePlayer(player));
	}

	updateOnlinePlayers() {
		this.leftControl.clear();
		this.rightControl.clear();

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

		if (RunService.IsStudio()) {
			const pl: FakePlayer[] = [
				{ DisplayName: "Amongus3", Name: "Sus3", UserId: 123 },
				{ DisplayName: "Amongus4", Name: "Sus4", UserId: 124 },
				{ DisplayName: "Amongus5", Name: "Sus5", UserId: 125 },
			];
			for (const player of pl) {
				this.addPlayer(player);
			}
		}
	}
	private addPlayer(player: FakePlayer) {
		if (player === Players.LocalPlayer) return;
		if (!RunService.IsStudio() && PlayerRank.isAdminById(player.UserId)) return;

		const control = new PlayerContainer(this.playerTemplate(), player);
		const instance = control.instance.TextButton;

		if (!this.value.has(player.UserId)) {
			this.leftControl.add(player, control);
		} else {
			this.rightControl.add(player, control);
		}

		const direction = this.leftControl.get(player) ? -1 : 1;
		Transforms.create()
			.func(() => (instance.Visible = false))
			.transform(instance, "BackgroundTransparency", 1)
			.moveRelative(instance, new UDim2(0, direction * 50, 0, 0))
			.wait(this.leftControl.getAll().size() * 0.05)
			.then()
			.func(() => (instance.Visible = true))
			.transform(instance, "BackgroundTransparency", 0, Transforms.quadOut02)
			.moveRelative(instance, new UDim2(0, direction * -50, 0, 0), Transforms.quadOut02)
			.then()
			.func(() => {
				control.clicked.Connect(() => {
					if (!this.value.has(player.UserId)) {
						this.value.add(player.UserId);
					} else {
						this.value.remove(player.UserId);
					}

					this.removePlayer(player);
					this.addPlayer(player);

					this.updating = true;
					this.publicValue.set(this.value.get());
					this.submitted.Fire(this.value.get());
					this.updating = false;
				});
			})
			.run(instance);
	}
	private removePlayer(player: FakePlayer) {
		const control = this.leftControl.get(player) ?? this.rightControl.get(player);
		if (!control) return;

		control.disable();
		control.instance.Interactable = false;

		const direction = this.leftControl.get(player) ? -1 : 1;
		Transforms.create()
			.transform(control.instance.TextButton.Texts.TitleLabel, "TextTransparency", 1, Transforms.quadOut02)
			.transform(control.instance.TextButton.Texts.UsernameLabel, "TextTransparency", 1, Transforms.quadOut02)
			.transform(control.instance.TextButton.ImageLabel, "ImageTransparency", 1, Transforms.quadOut02)
			.moveRelative(control.instance.TextButton, new UDim2(0, direction * 50, 0, 0), Transforms.quadOut02)
			.transform(control.instance.TextButton, "BackgroundTransparency", 1, Transforms.quadOut02)
			.then()
			.func(() => control.destroy())
			.run({});
	}
}


import { Players, UserInputService } from "@rbxts/services";
import { NumberTextBoxControlNullable } from "client/gui/controls/NumberTextBoxControl";
import { ProgressBarControl } from "client/gui/controls/ProgressBarControl";
import { PartialControl } from "engine/client/gui/PartialControl";
import { EventHandler } from "engine/shared/event/EventHandler";
import { NumberObservableValue } from "engine/shared/event/NumberObservableValue";
import { Signal } from "engine/shared/event/Signal";
import { MathUtils } from "engine/shared/fixes/MathUtils";
import type {
	ProgressBarControlDefinition,
	ProgressBarControlDefinitionParts,
} from "client/gui/controls/ProgressBarControl";

type ToNum<TAllowNull extends boolean> = TAllowNull extends false ? number : number | undefined;
export type SliderControlDefinition = ProgressBarControlDefinition & SliderControlDefinitionParts;
export type SliderControlDefinitionParts = ProgressBarControlDefinitionParts & {
	readonly TextBox?: TextBox;
	readonly Hitbox?: GuiObject;
};

export interface SliderControlConfig {
	readonly min: number;
	readonly max: number;
	readonly step?: number;
	readonly inputStep?: number;
}

/** Control that represents a number via a slider. */
class _SliderControl<TAllowNull extends boolean = false> extends PartialControl<
	SliderControlDefinitionParts,
	GuiObject,
	SliderControlDefinitionParts & { readonly Hitbox: GuiObject }
> {
	private readonly _submitted = new Signal<(value: number) => void>();
	readonly submitted = this._submitted.asReadonly();
	private readonly _moved = new Signal<(value: number) => void>();
	readonly moved = this._moved.asReadonly();
	readonly value;

	private readonly progressBar;
	private readonly inputStep;

	constructor(
		gui: SliderControlDefinition,
		private readonly config: SliderControlConfig,
		parts?: SliderControlDefinitionParts,
	) {
		if (!parts?.Hitbox) {
			parts = { ...parts, Hitbox: gui };
		}

		super(gui, parts);

		const { min, max, step, inputStep } = config;
		this.inputStep = inputStep ?? step ?? 0.01;

		this.progressBar = new ProgressBarControl(this.gui, min, max, step, this.parts);
		this.progressBar.instance.Active = true;
		this.add(this.progressBar);

		this.value = new NumberObservableValue<ToNum<TAllowNull>>(min, min, max, step);
		this.value.subscribe((value) => this.progressBar.value.set(value ?? 0), true);

		this.subscribeMovement();

		if (this.parts.TextBox) {
			const num = new NumberTextBoxControlNullable(this.parts.TextBox, this.value);
			this.event.subscribe(num.submitted, (value) => this._submitted.Fire(value));
			this.add(num);
		}
	}

	private subscribeMovement() {
		const eh = new EventHandler();
		let startpos: number | undefined;

		const moveMouse = () => {
			if (startpos === undefined) return;

			if (this.progressBar.vertical) {
				const y = this.parts.Hitbox.AbsoluteSize.Y - Players.LocalPlayer.GetMouse().Y;

				let value =
					((y - startpos) / this.parts.Hitbox.AbsoluteSize.Y) * this.value.getRange() + this.value.min;
				value = MathUtils.round(value, this.inputStep);

				this.value.set(value);
				this._moved.Fire(math.clamp(value, this.config.min, this.config.max));
			} else {
				const x = Players.LocalPlayer.GetMouse().X;

				let value =
					((x - startpos) / this.parts.Hitbox.AbsoluteSize.X) * this.value.getRange() + this.value.min;
				value = MathUtils.round(value, this.inputStep);

				this.value.set(value);
				this._moved.Fire(math.clamp(value, this.config.min, this.config.max));
			}
		};

		let st = false;
		const unsub = () => {
			st = false;
			startpos = undefined;
			eh.unsubscribeAll();

			const value = this.value.get();
			if (value !== undefined) {
				this._submitted.Fire(value);
			}
		};
		const sub = (signal: RBXScriptSignal<(input: InputObject) => void>) => {
			this.event.subscribe(signal, (input) => {
				if (
					input.UserInputState === Enum.UserInputState.Begin &&
					(input.UserInputType === Enum.UserInputType.MouseButton1 ||
						input.UserInputType === Enum.UserInputType.Touch)
				) {
					startpos = this.progressBar.vertical
						? this.parts.Hitbox.AbsoluteSize.Y -
							this.parts.Hitbox.AbsolutePosition.Y +
							this.parts.Hitbox.AbsoluteSize.Y
						: this.parts.Hitbox.AbsolutePosition.X;
					moveMouse();

					eh.subscribe(Players.LocalPlayer.GetMouse().Move, () => moveMouse());
					eh.subscribe(UserInputService.InputEnded, (input, _) => {
						if (startpos === undefined) return;

						if (
							input.UserInputType === Enum.UserInputType.MouseButton1 ||
							input.UserInputType === Enum.UserInputType.Touch
						) {
							unsub();
						}
					});
				}
			});
		};

		if (this.parts.Knob) {
			this.parts.Knob.SelectionGroup = true;
			this.parts.Knob.SelectionBehaviorLeft = Enum.SelectionBehavior.Stop;
			this.parts.Knob.SelectionBehaviorRight = Enum.SelectionBehavior.Stop;
		}

		const moveGamepad = (posx: boolean) => {
			const step = this.inputStep;
			const value = (this.value.get() ?? 0) + (posx ? step : -step);
			this.value.set(value);
			this._moved.Fire(value);
		};

		if (this.parts.Knob) {
			this.event.subscribe(this.parts.Knob.SelectionGained, () => {
				eh.subscribe(UserInputService.InputBegan, (input) => {
					if (!st) return;

					if (input.UserInputType === Enum.UserInputType.Gamepad1) {
						if (this.progressBar.vertical) {
							if (input.KeyCode === Enum.KeyCode.DPadUp) moveGamepad(true);
							else if (input.KeyCode === Enum.KeyCode.DPadDown) moveGamepad(false);
						} else {
							if (input.KeyCode === Enum.KeyCode.DPadRight) moveGamepad(true);
							else if (input.KeyCode === Enum.KeyCode.DPadLeft) moveGamepad(false);
						}
					}
				});
				eh.subscribe(UserInputService.InputChanged, (input) => {
					if (!st) return;

					if (this.progressBar.vertical) {
						if (input.UserInputType === Enum.UserInputType.Gamepad1 && input.Position.X !== 0) {
							moveGamepad(input.Position.X > 0);
						}
					} else {
						if (input.UserInputType === Enum.UserInputType.Gamepad1 && input.Position.Y !== 0) {
							moveGamepad(input.Position.Y > 0);
						}
					}
				});

				st = true;
			});

			this.event.subscribe(this.parts.Knob.SelectionLost, unsub);
			sub(this.parts.Knob.InputBegan);
		}

		if (this.parts.Filled) {
			sub(this.parts.Filled.InputBegan);
		}

		sub(this.parts.Hitbox.InputBegan);
	}
}

/** Control that represents a number via a slider. */
export class SliderControl extends _SliderControl<false> {}

/** Control that represents a number via a slider, nullable. */
export class SliderControlNullable extends _SliderControl<true> {}


import { ConfigControlBlacklist } from "client/gui/configControls/ConfigControlBlacklist";
import { ConfigControlList } from "client/gui/configControls/ConfigControlsList";
import { Objects } from "engine/shared/fixes/Objects";
import { CustomRemotes } from "shared/Remotes";
import type {
	ConfigControlListDefinition,
	ConfigControlTemplateList,
} from "client/gui/configControls/ConfigControlsList";
import type { ObservableValue } from "engine/shared/event/ObservableValue";
import type { SharedPlot } from "shared/building/SharedPlot";

export class PlayerSettingsBlacklist extends ConfigControlList {
	constructor(gui: ConfigControlListDefinition & ConfigControlTemplateList, value: ObservableValue<PlayerConfig>) {
		super(gui);

		this.$onInjectAuto((plot: SharedPlot) => {
			const isolationMode = this.event.addObservable(plot.isolationMode.fWithDefault(false));
			const blacklistedPlayers = this.event.addObservable(plot.blacklistedPlayers.fWithDefault(Objects.empty));

			const isolation = this.addToggle("Isolation mode") //
				.initToObservable(isolationMode)
				.setDescription("Blacklist everyone");
			isolation.submittedMulti((value) =>
				CustomRemotes.gui.settings.permissions.isolationMode.send(value ?? false),
			);

			const blacklist = this.parent(new ConfigControlBlacklist(this.clone(gui.Blacklist), "Blacklist")) //
				.initToObservable(blacklistedPlayers);
			blacklist.submittedMulti((players) =>
				CustomRemotes.gui.settings.permissions.updateBlacklist.send(players ?? []),
			);

			this.event.subscribeObservable(plot.isolationMode, (value) => blacklist.setVisibleAndEnabled(!value), true);
		});
	}
}


import { ConfigControlList } from "client/gui/configControls/ConfigControlsList";
import { Colors } from "engine/shared/Colors";
import { Observables } from "engine/shared/event/Observables";
import type {
	ConfigControlListDefinition,
	ConfigControlTemplateList,
} from "client/gui/configControls/ConfigControlsList";
import type { ObservableValue } from "engine/shared/event/ObservableValue";

export class PlayerSettingsEnvironment extends ConfigControlList {
	constructor(gui: ConfigControlListDefinition & ConfigControlTemplateList, value: ObservableValue<PlayerConfig>) {
		super(gui);

		this.addCategory("Day cycle");
		{
			this.addToggle("Automatic") //
				.setDescription("Automatic time, synced with all players. 20 minutes per in-game day.")
				.initToObjectPart(value, ["dayCycle", "automatic"]);

			const manual = this.addSlider("Manual", { min: 0, max: 24, inputStep: 0.1 }) //
				.setDescription("Manual time, hours.")
				.initToObjectPart(value, ["dayCycle", "manual"], "value");

			this.event
				.addObservable(value.fReadonlyCreateBased((c) => c.dayCycle))
				.subscribe(({ automatic }) => manual.setVisibleAndEnabled(!automatic), true);
		}

		this.addCategory("Terrain");
		{
			this.addSwitch("Type", [
				["Classic", { description: "Default Roblox terrain" }],
				["Triangle", { description: "Custom triangle part terrain" }],
				["Flat", { description: "Flat terrain" }],
				["Water", { description: "Water only terrain" }],
				["Lava", { description: "Flat terrain with lava" }],
				["Void", { description: "EMPTY NOTHINGNESS" }],
			]) //
				.initToObjectPart(value, ["terrain", "kind"]);

			this.addSlider("Load distance", { min: 1, max: 96, step: 1 }) //
				.initToObjectPart(value, ["terrain", "loadDistance"]);

			const triangleResolution = this.addSlider("Resolution", { min: 1, max: 16, step: 1 }) //
				.initToObjectPart(value, ["terrain", "resolution"]);
			const triangleWater = this.addToggle("Water") //
				.initToObjectPart(value, ["terrain", "water"]);
			const triangleSandBelowSeaLevel = this.addToggle("Sand below sea level") //
				.initToObjectPart(value, ["terrain", "triangleAddSandBelowSeaLevel"]);

			const classicFoliage = this.addToggle("Foliage") //
				.initToObjectPart(value, ["terrain", "foliage"]);

			const terrainSnowOnly = this.addToggle("Snow only") //
				.initToObjectPart(value, ["terrain", "snowOnly"]);

			const terrainOverride = this.addToggle("Override material") //
				.initToObjectPart(value, ["terrain", "override", "enabled"]);

			const terrainOverrideMaterial = this.addMaterial("Material", Enum.Material.Plastic) //
				.initToObservable(
					this.event
						.addObservable(
							Observables.createObservableFromObjectProperty<string>(value, [
								"terrain",
								"override",
								"material",
							]),
						)
						.fCreateBased(
							(c) => Enum.Material[c as never] as Enum.Material,
							(c) => c.Name,
						),
				);

			const terrainOverrideColor = this.addColor("Color", { alpha: 1, color: Colors.white }, false) //
				.initToObjectPart(value, ["terrain", "override", "color"]);

			this.event
				.addObservable(value.fReadonlyCreateBased((c) => c.terrain)) //
				.subscribe(({ kind, snowOnly }) => {
					triangleResolution.setVisibleAndEnabled(kind === "Triangle");
					triangleWater.setVisibleAndEnabled(kind === "Triangle");
					triangleSandBelowSeaLevel.setVisibleAndEnabled(kind === "Triangle" && !snowOnly);

					classicFoliage.setVisibleAndEnabled(kind === "Classic");

					terrainSnowOnly.setVisibleAndEnabled(kind !== "Water" && kind !== "Lava");

					terrainOverride.setVisibleAndEnabled(kind === "Triangle" || kind === "Flat");
					terrainOverrideMaterial.setVisibleAndEnabled(kind === "Triangle" || kind === "Flat");
					terrainOverrideColor.setVisibleAndEnabled(kind === "Triangle" || kind === "Flat");
				}, true);
		}
	}
}


import { GuiService } from "@rbxts/services";
import { SoundController } from "client/controller/SoundController";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import type { ButtonDefinition } from "engine/client/gui/Button";

type AlertPopupDefinition = GuiObject & {
	readonly Content: Frame & {
		readonly Text: TextLabel;
		readonly Buttons: {
			readonly OkButton: TextButton;
		};
	};
	readonly Heading: {
		readonly CloseButton: ButtonDefinition;
	};
};

export class AlertPopup extends Control<AlertPopupDefinition> {
	private readonly okButton;

	constructor(text: string, okFunc?: () => void, delayBeforeInteractable = 3) {
		const gui = Interface.getInterface<{
			Popups: { Crossplatform: { Alert: AlertPopupDefinition } };
		}>().Popups.Crossplatform.Alert.Clone();
		super(gui);

		this.okButton = this.parent(new Control(gui.Content.Buttons.OkButton));

		SoundController.getUISounds().Warning.Play();

		gui.Content.Text.Text = text;

		this.okButton.addButtonAction(() => {
			okFunc?.();
			this.hide();
		});

		const closeButton = this.parent(new Control(gui.Heading.CloseButton).addButtonAction(() => this.hide()));

		this.okButton.setButtonInteractable(false);
		closeButton.setButtonInteractable(false);
		task.delay(delayBeforeInteractable, () => {
			this.okButton.setButtonInteractable(true);
			closeButton.setButtonInteractable(true);
		});

		this.event.onPrepareGamepad(() => (GuiService.SelectedObject = this.okButton.instance));
	}
}


import { GuiService } from "@rbxts/services";
import { SoundController } from "client/controller/SoundController";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import { PartialControl } from "engine/client/gui/PartialControl";

const template = Interface.getInterface<{
	Popups: { Crossplatform: { Confirmation: GuiObject } };
}>().Popups.Crossplatform.Confirmation;
template.Visible = false;

type ConfirmPopupParts = {
	readonly CloseButton: GuiButton;

	readonly TextLabel1: TextLabel;
	readonly TextLabel2: TextLabel;

	readonly AcceptButton: GuiButton;
	readonly CancelButton: GuiButton;
};
export class ConfirmPopup extends PartialControl<ConfirmPopupParts> {
	private readonly confirmButton;
	private readonly cancelButton;
	private readonly closeButton;

	constructor(text: string, ps: string = "", okFunc?: () => void, noFunc?: () => void) {
		const gui = template.Clone();
		super(gui);

		this.confirmButton = this.parent(new Control(this.parts.AcceptButton));
		this.cancelButton = this.parent(new Control(this.parts.CancelButton));
		this.closeButton = this.parent(new Control(this.parts.CloseButton));

		SoundController.getUISounds().Warning.Play();

		this.parts.TextLabel1.Text = text;
		this.parts.TextLabel2.Text = ps;
		this.confirmButton.addButtonAction(() => {
			okFunc?.();
			this.hide();
		});
		this.cancelButton.addButtonAction(() => {
			noFunc?.();
			this.hide();
		});
		this.closeButton.addButtonAction(() => {
			noFunc?.();
			this.hide();
		});

		this.event.onPrepareGamepad(() => (GuiService.SelectedObject = this.cancelButton.instance));
	}
}


import { Base64 } from "@rbxts/crypto";
import { GuiService, Players } from "@rbxts/services";
import { SoundController } from "client/controller/SoundController";
import { ButtonControl } from "engine/client/gui/Button";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import { JSON } from "engine/shared/fixes/Json";
import { GameDefinitions } from "shared/data/GameDefinitions";
import type { PlayerDataStorage } from "client/PlayerDataStorage";
import type { ButtonDefinition } from "engine/client/gui/Button";

const processDataForJsonSerializationForSubmit = (data: unknown): unknown => {
	if (typeIs(data, "Instance")) {
		return {
			__type: "Instance",
			instanceType: data.ClassName,
			name: data.Name,
			attributes: data.GetAttributes(),
			tags: data.GetTags(),
		};
	}

	if (typeIs(data, "table")) {
		const toserialize: Partial<Record<string | number | symbol, unknown>> = {};
		for (const [key, value] of pairs(data)) {
			toserialize[key as keyof typeof toserialize] = processDataForJsonSerializationForSubmit(value);
		}

		return toserialize;
	}

	return data;
};

const serialize = (data: unknown): string => {
	const str = JSON.serialize(data);
	return Base64.Encode(str);
};

type ReportSubmitPopupDefinition = GuiObject & {
	readonly Content: Frame & {
		readonly Text: TextLabel;
		readonly TextBox: TextBox;
		readonly Buttons: {
			readonly OkButton: TextButton;
		};
	};
	readonly Heading: {
		readonly CloseButton: ButtonDefinition;
	};
};
export class ReportSubmitPopup extends Control<ReportSubmitPopupDefinition> {
	constructor(data: unknown, text?: string, okFunc?: () => void) {
		const gui = Interface.getInterface<{
			Popups: { Crossplatform: { ReportSubmit: ReportSubmitPopupDefinition } };
		}>().Popups.Crossplatform.ReportSubmit.Clone();
		super(gui);

		const okButton = this.parent(new Control(gui.Content.Buttons.OkButton));
		okButton.addButtonAction(() => {
			okFunc?.();
			this.hide();
		});
		this.event.onPrepareGamepad(() => (GuiService.SelectedObject = okButton.instance));

		this.parent(new ButtonControl(gui.Heading.CloseButton, () => this.hide()));

		this.onEnable(() => SoundController.getUISounds().Warning.Play());

		gui.Content.Text.Text =
			text ??
			"An error has happened.\nPlease copy the text below and submit a bug report in our community server.";

		this.$onInjectAuto((playerDataStorage: PlayerDataStorage) => {
			if (typeIs(data, "table")) {
				data = {
					...data,
					loadedSlotIdx: playerDataStorage.loadedSlot.get(),
					data: playerDataStorage.data.get(),
					// loadedSlot: this.plot.getBlocks(), // too much text
				};
			}

			try {
				gui.Content.TextBox.Text = serialize({
					uid: Players.LocalPlayer.UserId,
					uname: Players.LocalPlayer.Name,
					udname: Players.LocalPlayer.DisplayName,
					text: text ?? "Never gonna give you up",
					env: GameDefinitions.getEnvironmentInfo(),
					trace: debug.traceback(undefined, 1)?.split("\n"),
					data: processDataForJsonSerializationForSubmit(data),
				});
			} catch {
				try {
					gui.Content.TextBox.Text = serialize({
						uid: Players.LocalPlayer.UserId,
						uname: Players.LocalPlayer.Name,
						udname: Players.LocalPlayer.DisplayName,
						env: GameDefinitions.getEnvironmentInfo(),
						data: processDataForJsonSerializationForSubmit(data),
					});
				} catch {
					try {
						gui.Content.TextBox.Text = serialize({
							uid: Players.LocalPlayer.UserId,
							uname: Players.LocalPlayer.Name,
							udname: Players.LocalPlayer.DisplayName,
							trace: debug.traceback(undefined, 1)?.split("\n"),
						});
					} catch {
						gui.Content.TextBox.Text = serialize("[ERRTOOLONG]");
					}
				}
			}
		});
	}
}


import { GuiService } from "@rbxts/services";
import { TextButtonControl } from "engine/client/gui/Button";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import { TextBoxControl } from "engine/client/gui/TextBoxControl";
import { Signal } from "engine/shared/event/Signal";
import type { ButtonDefinition, TextButtonDefinition } from "engine/client/gui/Button";

type SelectButtonPopupDefinition = GuiObject & {
	readonly Content: {
		readonly Buttons: {
			readonly CancelButton: GuiButton;
		};
		readonly List: GuiObject & {
			readonly ScrollingFrame: ScrollingFrame & {
				readonly ButtonTemplate: TextButtonDefinition;
			};
			readonly AcceptButton: GuiButton;
			readonly CustomTextBox: TextBox;
		};
	};
	readonly Heading: {
		readonly CloseButton: ButtonDefinition;
	};
};

class _SelectButtonPopup<TAllowCustomString extends boolean> extends Control<SelectButtonPopupDefinition> {
	private readonly buttonPressed = new Signal<(key: KeyCode) => void>();
	private readonly cancelButton;
	private readonly closeButton;

	constructor(
		allowCustomString: TAllowCustomString,
		confirmFunc: (key: TAllowCustomString extends true ? string : KeyCode) => void,
		cancelFunc: () => void,
	) {
		const gui = Interface.getInterface<{
			Popups: { MobileButtonSelection: SelectButtonPopupDefinition };
		}>().Popups.MobileButtonSelection.Clone();
		super(gui);

		this.cancelButton = this.parent(new Control(gui.Content.Buttons.CancelButton));
		this.closeButton = this.parent(new Control(gui.Heading.CloseButton));

		const customTextBox = this.parent(new TextBoxControl(gui.Content.List.CustomTextBox));
		const acceptButton = this.parent(new Control(gui.Content.List.AcceptButton));
		if (allowCustomString) {
			acceptButton.addButtonAction(() => {
				this.hide();
				confirmFunc(customTextBox.text.get() as KeyCode);
			});
		} else {
			customTextBox.instance.Interactable = false;
			acceptButton.setButtonInteractable(false);
		}

		const list = new Control(gui.Content.List.ScrollingFrame);
		this.parent(list);

		const template = this.asTemplate(gui.Content.List.ScrollingFrame.ButtonTemplate);

		const keys = Enum.KeyCode.GetEnumItems().filter((value) => {
			// numbers
			if (value.Value >= 48 && value.Value <= 57) return true;

			// a-z
			if (value.Value >= 97 && value.Value <= 122) return true;

			// gamepad
			if (value.Value >= 1000 && value.Value <= 1017) return true;

			return false;
		});

		const keyToName = (key: KeyCode) => {
			if (key === "Zero") return "0";
			if (key === "One") return "1";
			if (key === "Two") return "2";
			if (key === "Three") return "3";
			if (key === "Four") return "4";
			if (key === "Five") return "5";
			if (key === "Six") return "6";
			if (key === "Seven") return "7";
			if (key === "Eight") return "8";
			if (key === "Nine") return "9";

			if (key.find("^Button")[0] !== undefined) {
				return key.sub("Button".size());
			}

			return key;
		};

		for (const key of keys) {
			const btn = new TextButtonControl(template());
			btn.text.set(keyToName(key.Name));
			list.add(btn);

			this.event.subscribe(btn.activated, () => this.buttonPressed.Fire(key.Name));
		}

		this.event.subscribe(this.buttonPressed, (key) => {
			this.hide();
			confirmFunc(key);
		});
		this.cancelButton.addButtonAction(() => {
			this.hide();
			cancelFunc();
		});
		this.closeButton.addButtonAction(() => {
			this.hide();
			cancelFunc();
		});

		this.event.onPrepareGamepad(() => (GuiService.SelectedObject = this.cancelButton.instance));
	}
}

export class SelectButtonPopup extends _SelectButtonPopup<false> {
	constructor(confirmFunc: (key: KeyCode) => void, cancelFunc: () => void) {
		super(false, confirmFunc, cancelFunc);
	}
}
export class SelectButtonPopupWithCustomString extends _SelectButtonPopup<true> {
	constructor(confirmFunc: (key: KeyCode | string) => void, cancelFunc: () => void) {
		super(true, confirmFunc, cancelFunc);
	}
}


import { GuiService } from "@rbxts/services";
import { ButtonControl } from "engine/client/gui/Button";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";

type TextPopupDefinition = GuiObject & {
	readonly Heading: Frame & {
		readonly CloseButton: TextButton;
		readonly TitleLabel: TextLabel;
	};
	readonly Content: {
		readonly Buttons: {
			readonly DoneButton: TextButton;
		};
		readonly Frame: {
			readonly ScrollingFrame: ScrollingFrame & {
				readonly TextBox: TextBox;
			};
		};
	};
};

export class TextPopup extends Control<TextPopupDefinition> {
	private readonly doneButton;
	private readonly closeButton;

	constructor(text: string, ps: string = "", okFunc: (text: string) => void, noFunc: () => void) {
		const gui = Interface.getInterface<{ Popups: { TextInput: TextPopupDefinition } }>().Popups.TextInput.Clone();
		super(gui);

		this.doneButton = this.parent(new ButtonControl(gui.Content.Buttons.DoneButton));
		this.closeButton = this.parent(new ButtonControl(gui.Heading.CloseButton));

		gui.Heading.TitleLabel.Text = text;
		gui.Content.Frame.ScrollingFrame.TextBox.PlaceholderText = ps;
		this.event.subscribe(this.doneButton.activated, () => {
			okFunc(gui.Content.Frame.ScrollingFrame.TextBox.Text);
			this.hide();
		});
		this.event.subscribe(this.closeButton.activated, () => {
			noFunc();
			this.hide();
		});

		this.event.onPrepareGamepad(() => (GuiService.SelectedObject = gui.Content.Frame.ScrollingFrame.TextBox));
	}
}


import { RunService, UserInputService, Workspace } from "@rbxts/services";
import { LoadingController } from "client/controller/LoadingController";
import { LocalPlayerController } from "client/controller/LocalPlayerController";
import { ManualBeacon } from "client/gui/Beacon";
import { CheckBoxControl } from "client/gui/controls/CheckBoxControl";
import { FormattedLabelControl } from "client/gui/controls/FormattedLabelControl";
import { ProgressBarControl } from "client/gui/controls/ProgressBarControl";
import { ConfirmPopup } from "client/gui/popup/ConfirmPopup";
import { TouchModeButtonControl } from "client/gui/ridemode/TouchModeButtonControl";
import { LogControl } from "client/gui/static/LogControl";
import { requestMode } from "client/modes/PlayModeRequest";
import { Action } from "engine/client/Action";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import { InputController } from "engine/client/InputController";
import { LocalPlayer } from "engine/client/LocalPlayer";
import { Colors } from "engine/shared/Colors";
import { Component } from "engine/shared/component/Component";
import { ComponentChild } from "engine/shared/component/ComponentChild";
import { ComponentChildren } from "engine/shared/component/ComponentChildren";
import { ComponentKeyedChildren } from "engine/shared/component/ComponentKeyedChildren";
import { EventHandler } from "engine/shared/event/EventHandler";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { Signal } from "engine/shared/event/Signal";
import { BeaconBlock } from "shared/blocks/blocks/BeaconBlock";
import { RocketBlocks } from "shared/blocks/blocks/RocketEngineBlocks";
import { VehicleSeatBlock } from "shared/blocks/blocks/VehicleSeatBlock";
import { CustomRemotes } from "shared/Remotes";
import type { ClientMachine } from "client/blocks/ClientMachine";
import type { CheckBoxControlDefinition } from "client/gui/controls/CheckBoxControl";
import type { ProgressBarControlDefinition } from "client/gui/controls/ProgressBarControl";
import type { MainScreenLayout } from "client/gui/MainScreenLayout";
import type { PopupController } from "client/gui/PopupController";
import type { RideMode } from "client/modes/ride/RideMode";
import type { PlayerDataStorage } from "client/PlayerDataStorage";
import type { Theme } from "client/Theme";
import type { TextButtonDefinition } from "engine/client/gui/Button";
import type { ReadonlyComponentChildren } from "engine/shared/component/ComponentChildren";
import type { InstanceComponent } from "engine/shared/component/InstanceComponent";
import type { GenericBlockLogic } from "shared/blockLogic/BlockLogic";
import type { BeaconBlockLogic } from "shared/blocks/blocks/BeaconBlock";
import type { RocketBlockLogic } from "shared/blocks/blocks/RocketEngineBlocks";

type RideModeControlsDefinition = GuiObject & {
	readonly Overlay: GuiObject;
	readonly Button: TextButtonDefinition;
};
export class RideModeControls extends Control<RideModeControlsDefinition> {
	readonly onEnterSettingsMode = new Signal<() => void>();
	readonly onQuitSettingsMode = new Signal<() => void>();

	private readonly overlayTemplate;
	private quitSettingsMode?: () => void;

	private readonly keyedChildren;

	constructor(
		gui: RideModeControlsDefinition,
		private readonly playerData: PlayerDataStorage,
	) {
		super(gui, { showOnEnable: true });

		this.keyedChildren = this.parent(
			new ComponentKeyedChildren<string, InstanceComponent<GuiObject>>().withParentInstance(gui),
		);
		this.overlayTemplate = this.asTemplate(this.gui.Overlay);

		this.onDisable(() => {
			if (this.quitSettingsMode) {
				this.toggleSettingsMode();
			}
		});
	}

	resetControl(btn: InstanceComponent<GuiObject>, pos: number) {
		const size = btn.instance.Size;

		let x = 0.95;
		let y = 1 - size.Y.Scale * pos - 0.01 * pos;

		while (y < 0.05) {
			y += 0.95;
			x -= 0.05;
		}

		btn.instance.Position = new UDim2(x, 0, y, 0);
	}
	resetControls() {
		let pos = 0;
		for (const [key, btn] of this.keyedChildren.getAll()) {
			this.resetControl(btn, pos);
			pos++;
		}
	}

	toggleSettingsMode() {
		if (this.quitSettingsMode) {
			this.quitSettingsMode();
			this.quitSettingsMode = undefined;
			return;
		}

		const mode = new Component();

		const overlayInstance = new Instance("Frame");
		mode.onDestroy(() => overlayInstance.Destroy());
		const overlay = mode.parent(new ComponentChildren().withParentInstance(overlayInstance));
		const ehandlers: EventHandler[] = [];
		let inputting = false;

		for (const [_, child] of this.keyedChildren.getAll()) {
			const instance = this.overlayTemplate();
			instance.Position = new UDim2(0, 0, 0, 0);
			instance.Size = new UDim2(1, 0, 1, 0);
			instance.Parent = child.instance;
			child.instance.Active = false;

			instance.InputBegan.Connect((input) => {
				if (inputting) return;

				if (
					input.UserInputType !== Enum.UserInputType.MouseButton1 &&
					input.UserInputType !== Enum.UserInputType.Touch
				)
					return;
				inputting = true;

				let prevpos = input.Position;

				const eh = new EventHandler();
				ehandlers.push(eh);
				eh.subscribe(UserInputService.InputChanged, (input) => {
					if (
						input.UserInputType !== Enum.UserInputType.MouseMovement &&
						input.UserInputType !== Enum.UserInputType.Touch
					)
						return;

					const delta = input.Position.sub(prevpos);
					prevpos = input.Position;
					child.instance.Position = child.instance.Position.add(
						new UDim2(delta.X / this.gui.AbsoluteSize.X, 0, delta.Y / this.gui.AbsoluteSize.Y, 0),
					);
				});

				eh.subscribe(UserInputService.InputEnded, (input) => {
					if (
						input.UserInputType !== Enum.UserInputType.MouseButton1 &&
						input.UserInputType !== Enum.UserInputType.Touch
					)
						return;

					{
						let abs = new Vector2(
							child.instance.Position.X.Scale * this.gui.AbsoluteSize.X,
							child.instance.Position.Y.Scale * this.gui.AbsoluteSize.Y,
						);

						const grid = child.instance.AbsoluteSize.div(4);
						abs = new Vector2(math.round(abs.X / grid.X) * grid.X, math.round(abs.Y / grid.Y) * grid.Y);

						child.instance.Position = new UDim2(
							abs.X / this.gui.AbsoluteSize.X,
							0,
							abs.Y / this.gui.AbsoluteSize.Y,
							0,
						);
					}

					eh.unsubscribeAll();
					ehandlers.remove(ehandlers.indexOf(eh));
					inputting = false;
				});
			});

			const box = new Control(instance);
			overlay.add(box);
		}

		this.onEnterSettingsMode.Fire();
		this.quitSettingsMode = async () => {
			for (const child of overlay.getAll()) {
				if (!(child instanceof Control)) continue;
				child.instance.Active = true;
			}

			this.onQuitSettingsMode.Fire();
			mode.destroy();

			for (const eh of ehandlers) {
				eh.unsubscribeAll();
			}

			const touchControls: Record<string, TouchControlInfo[string]> = {};
			for (const [keycode, child] of this.keyedChildren.getAll()) {
				touchControls[keycode] = {
					pos: [child.instance.Position.X.Scale, child.instance.Position.Y.Scale],
				};
			}

			const result = this.playerData.sendPlayerSlot({
				index: this.playerData.loadedSlot.get() ?? -1,
				touchControls,
				save: false,
			});
			if (!result.success) {
				LogControl.instance.addLine(result.message, Colors.red);
			}
		};
	}

	start(machine: ClientMachine) {
		this.keyedChildren.clear();
		machine.onDestroy(() => this.keyedChildren.clear());
		machine.occupiedByLocalPlayer.subscribe((occupied) => {
			this.setVisibleAndEnabled(occupied, "machineSeatOccupiedByPlayer");
		}, true);

		const inputType = InputController.inputType.get();

		const inputLogics = machine.getLogicInputs();
		const controls = [
			...TouchModeButtonControl.fromBlocks(inputType, inputLogics),
			//
		];

		const controlsInfo = this.playerData.slots.get()[this.playerData.loadedSlot.get() ?? -1]?.touchControls;

		let pos = 0;
		for (const control of controls) {
			this.keyedChildren.add(control.text.get(), control);

			const keycode = control.text.get() as KeyCode;

			const doload =
				controlsInfo !== undefined &&
				controlsInfo[keycode] !== undefined &&
				controlsInfo[keycode].pos[0] >= control.instance.AbsoluteSize.X / this.gui.AbsoluteSize.X &&
				controlsInfo[keycode].pos[0] <= 1 &&
				controlsInfo[keycode].pos[1] >= control.instance.AbsoluteSize.Y / this.gui.AbsoluteSize.Y &&
				controlsInfo[keycode].pos[1] <= 1;

			if (doload) {
				const kc = controlsInfo?.[keycode];
				if (kc) {
					if (kc.pos[0] <= 1) {
						// relative position
						control.instance.Position = new UDim2(kc.pos[0], 0, kc.pos[1], 0);
					} else {
						// pixel position
						control.instance.Position = new UDim2(0, kc.pos[0], 0, kc.pos[1]);
					}
				}
			} else {
				this.resetControl(control, pos);
				pos++;
			}
		}
	}
}

export type RideModeInfoControlDefinition = GuiObject & ProgressBarControlDefinition & { FormattedText: TextLabel };
export class RideModeInfoControl extends Control<RideModeInfoControlDefinition> {
	readonly slider;
	readonly text;

	constructor(gui: RideModeInfoControlDefinition, min: number, max: number, step: number) {
		super(gui);

		this.slider = this.add(new ProgressBarControl(this.gui, min, max, step));
		this.text = this.add(new FormattedLabelControl(this.gui.FormattedText));
	}
}

export type RideModeSceneDefinition = GuiObject & {
	readonly Controls: RideModeControlsDefinition;
	readonly Info: GuiObject & {
		readonly Template: RideModeInfoControlDefinition & { Title: TextLabel };
		readonly TextTemplate: RideModeInfoControlDefinition & { Title: TextLabel };
	};
};

@injectable
export class RideModeScene extends Control<RideModeSceneDefinition> {
	private readonly stopAction;
	private readonly sitAction;

	private readonly controls;
	private readonly info;

	readonly infoTemplate;
	readonly infoTextTemplate;

	private readonly logicVisible = new ObservableValue(false);

	constructor(
		gui: RideModeSceneDefinition,
		@inject readonly mode: RideMode,
		@inject playerData: PlayerDataStorage,
		@inject popupController: PopupController,
		@inject mainScreen: MainScreenLayout,
		@inject theme: Theme,
	) {
		super(gui);

		const controlsEditMode = new ObservableValue(false);
		const notControlsEditMode = controlsEditMode.fReadonlyCreateBased((b) => !b);

		this.stopAction = this.parent(new Action(() => requestMode("build")));
		this.stopAction.subCanExecuteFrom({
			ride_editcontrols: notControlsEditMode,
			ride_isntloading: LoadingController.isNotLoading,
		});
		this.parent(
			mainScreen.top.main.addButton("Stop", { iconId: 15448899035, background: "buttonNegative", width: 200 }),
		) //
			.themeButton(theme, "accent")
			.subscribeToAction(this.stopAction)
			.subscribeVisibilityFrom({ main_enabled: this.enabledState });

		this.sitAction = this.parent(new Action(() => CustomRemotes.modes.ride.teleportOnSeat.send()));
		this.sitAction.subCanExecuteFrom({
			ride_editcontrols: notControlsEditMode,
			ride_isntloading: LoadingController.isNotLoading,
		});
		this.parent(mainScreen.top.main.addButton("Sit", { iconId: 15568720419 })) //
			.subscribeToAction(this.sitAction)
			.subscribeVisibilityFrom({ main_enabled: this.enabledState });

		this.parent(mainScreen.addTopRightButton("EditControls", 18474772799))
			.addButtonAction(() => this.controls.toggleSettingsMode())
			.subscribeVisibilityFrom({
				ride_touchOnly: InputController.isTouch,
				ride_isntloading: LoadingController.isNotLoading,
				ride_enabled: this.enabledState,
			})
			.addValueOverlayChild(
				"BackgroundColor3",
				controlsEditMode.fReadonlyCreateBased((c) => (c ? theme.get("buttonActive") : undefined)),
			);

		this.parent(mainScreen.addTopRightButton("ResetControls", 15654861713))
			.addButtonAction(() =>
				popupController.showPopup(
					new ConfirmPopup("Reset the controls?", "It will be impossible to undo this action", () =>
						this.controls.resetControls(),
					),
				),
			)
			.subscribeVisibilityFrom({
				ride_editcontrols: controlsEditMode,
				ride_isntloading: LoadingController.isNotLoading,
				ride_enabled: this.enabledState,
			});

		this.parent(mainScreen.addTopRightButton("Logic", 18626718502)) //
			.addButtonAction(() => this.logicVisible.toggle())
			.subscribeVisibilityFrom({
				ride_editcontrols: notControlsEditMode,
				ride_isntloading: LoadingController.isNotLoading,
				ride_enabled: this.enabledState,
			});

		//

		this.controls = this.parent(new RideModeControls(this.gui.Controls, playerData));
		this.controls.onEnterSettingsMode.Connect(() => controlsEditMode.set(true));
		this.controls.onQuitSettingsMode.Connect(() => controlsEditMode.set(false));

		this.info = this.parent(new ComponentChildren<Control>().withParentInstance(this.gui.Info));

		this.infoTemplate = this.asTemplate(this.gui.Info.Template);
		this.infoTextTemplate = this.asTemplate(this.gui.Info.TextTemplate);
	}

	private addMeters(machine: Component & { readonly blocks: ReadonlyComponentChildren<GenericBlockLogic> }) {
		this.info.clear();

		const init = (
			title: string,
			textformat: string,
			gui: RideModeInfoControlDefinition & { Title: TextLabel },
			min: number,
			max: number,
			step: number,
			update: (control: RideModeInfoControl) => void,
		) => {
			gui.Title.Text = title.upper();
			gui.FormattedText!.Text = textformat;

			const control = new RideModeInfoControl(gui, min, max, step);
			control.event.subscribe(RunService.Heartbeat, () => update(control));
			this.info.add(control);
		};

		{
			const maxSpdShow = 800;

			init("Speed", "%.1f st/s", this.infoTemplate(), 0, maxSpdShow, 0.1, (control) => {
				const rootPart = LocalPlayer.rootPart.get();
				if (!rootPart) return;

				const spd = rootPart.GetVelocityAtPosition(rootPart.Position).Magnitude;

				control.slider.value.set(spd);
				control.text.value.set(spd);
			});
		}

		{
			const rocketClass = RocketBlocks[0]!.logic!.ctor;

			const rockets = machine.blocks
				.getAll()
				.filter((c) => c instanceof rocketClass) as unknown as readonly RocketBlockLogic[];
			if (rockets.size() !== 0) {
				init("Thrust", "%s %%", this.infoTemplate(), 0, 100, 1, (control) => {
					let avgg = 0;
					let amount = 0;
					for (const rocket of rockets) {
						if (!rocket.isEnabled()) continue;

						avgg += rocket.getThrust();
						amount++;
					}

					control.slider.value.set(amount === 0 ? 0 : avgg / amount);
					control.text.value.set(math.round(control.slider.value.get()));
				});
			}
		}

		{
			const maxAltitude = 420;
			init("Altitude", "%.2f st", this.infoTextTemplate(), 0, maxAltitude, 0.1, (control) => {
				const rootPart = LocalPlayer.rootPart.get();
				if (!rootPart) return;

				const alt = LocalPlayerController.getPlayerRelativeHeight();
				control.slider.value.set(alt);
				control.text.value.set(alt);
			});
		}

		{
			init("Gravity", "%.1f st/s²", this.infoTextTemplate(), 0, 55, 0.1, (control) => {
				const alt = Workspace.Gravity;

				control.text.value.set(alt);
			});
		}

		{
			init("Position", "%s", this.infoTextTemplate(), 0, 1, 1, (control) => {
				const rootPart = LocalPlayer.rootPart.get();
				if (!rootPart) return;

				control.text.value.set(`${math.floor(rootPart.Position.X)} ${math.floor(rootPart.Position.Z)}`);
			});
		}

		{
			const startTime = DateTime.now();
			const pretty = (ms: number) => {
				return `${math.floor(ms / 1000 / 60 / 60)}h ${math.floor(ms / 1000 / 60) % 60}m ${
					math.floor(ms / 1000) % 60
				}s`;
			};

			init("Time", "%s", this.infoTextTemplate(), 0, 1, 1, (control) => {
				control.text.value.set(
					pretty(
						DateTime.fromUnixTimestampMillis(
							DateTime.now().UnixTimestampMillis - startTime.UnixTimestampMillis,
						).UnixTimestampMillis,
					),
				);
			});
		}

		{
			//блок маяка костыль
			const beaconBlockClass = BeaconBlock!.logic!.ctor;

			const beacons = machine.blocks
				.getAll()
				.filter((c) => c instanceof beaconBlockClass) as unknown as readonly BeaconBlockLogic[];

			for (const beacon of beacons) {
				beacon.beaconInstance = new ManualBeacon(beacon.definition.input.text.displayName);
				beacon.updateData(beacon.definition.input.text.displayName, true);
			}
		}
	}

	addMeter(gui: RideModeInfoControlDefinition & { Title: TextLabel }) {
		const control = new RideModeInfoControl(gui, 0, 1, 0);
		this.info.add(control);

		return control;
	}

	private initLogicController(machine: ClientMachine, runLogic: boolean) {
		const component = this.current.set(new Component());

		const template = Interface.getInterface<{
			Floating: {
				LogicDebug: GuiObject & {
					readonly Visualizer: GuiObject & {
						readonly Control: CheckBoxControlDefinition;
					};
					readonly Content: GuiObject & {
						readonly Pause: TextButtonDefinition & {
							readonly ImageLabel: ImageLabel;
						};
						readonly Step: GuiButton;
					};
				};
			};
		}>().Floating.LogicDebug;
		const logicDebugGui = template.Clone();

		const logicDebug = component.parent(new Control(logicDebugGui));
		logicDebug.instance.Parent = template.Parent;

		this.logicVisible.set(!runLogic);
		const sub = this.logicVisible.subscribe((v) => (logicDebugGui.Visible = v), true);
		logicDebug.onDestroy(() => sub.Disconnect());

		const visualizer = component.parent(new ComponentChild(true));
		const vcb = logicDebug.add(new CheckBoxControl(logicDebug.instance.Visualizer.Control));
		vcb.value.subscribe((c) => {
			if (c) {
				visualizer.set(machine.createVisualizer());
			} else {
				visualizer.clear();
			}
		});

		logicDebug.add(new Control(logicDebug.instance.Content.Step).addButtonAction(() => machine.runner.tick()));

		const pauseButton = logicDebug.add(new Control(logicDebug.instance.Content.Pause));
		pauseButton.parent(
			new Control(pauseButton.instance).addButtonAction(() => {
				if (machine.runner.isRunning.get()) {
					machine.runner.stopTicking();
					machine.logicInputs.setEnabled(false);
				} else {
					machine.runner.startTicking();
					machine.logicInputs.setEnabled(true);
				}
			}),
		);

		component.event.subscribeObservable(
			machine.runner.isRunning,
			(running) => {
				pauseButton.setButtonText(running ? "PAUSE" : "RESUME");
				pauseButton.instance.ImageLabel.Image = running
					? "rbxassetid://18627572768"
					: "rbxassetid://15266883073";
			},
			true,
		);
	}

	private readonly current = this.parent(new ComponentChild(true));

	start(machine: ClientMachine, runLogic: boolean) {
		if (InputController.inputType.get() === "Touch") {
			this.controls.start(machine);
		}

		this.sitAction.canExecute.and(
			"canNotSitIfNoSeat",
			machine.blocks.getAll().any((b) => b instanceof VehicleSeatBlock.logic.ctor),
		);

		this.addMeters(machine);
		this.initLogicController(machine, runLogic);
	}
}


import { UserInputService } from "@rbxts/services";
import { Control } from "engine/client/gui/Control";
import { Interface } from "engine/client/gui/Interface";
import { InputController } from "engine/client/InputController";
import { InstanceComponent } from "engine/shared/component/InstanceComponent";
import { Element } from "engine/shared/Element";
import { Keys } from "engine/shared/fixes/Keys";
import type { KeybindRegistration, KeyCombination } from "engine/client/Keybinds";

const tooltipsGui = Interface.getInterface<{ Help: { Controls: TooltipsControlDefinition } }>().Help.Controls;
tooltipsGui.Visible = true;

const keyboardTooltipTemplate = Control.asTemplateWithMemoryLeak(tooltipsGui.KeyboardTemplate);
const gamepadTooltipTemplate = Control.asTemplateWithMemoryLeak(tooltipsGui.GamepadTemplate);

export interface Tooltip {
	readonly keys: readonly KeyCombination[];
	readonly text: string;
}

type TooltipsControlDefinition = GuiObject & {
	GamepadTemplate: GuiObject & {
		ImageLabel: ImageLabel;
		TextLabel: TextLabel;
	};
	KeyboardTemplate: GuiObject & {
		TextLabel: TextLabel;
		Keys: GuiObject & {
			ImageLabel: ImageLabel & {
				KeyLabel: TextLabel;
			};
		};
	};
};

export class TooltipsHolder extends InstanceComponent<GuiObject> {
	static createComponent(category: string): TooltipsHolder {
		const gui = Element.create(
			"Frame",
			{
				Size: new UDim2(1, 0, 0, 0),
				AutomaticSize: Enum.AutomaticSize.Y,
				BackgroundTransparency: 1,
				Name: category,
				Parent: tooltipsGui,
			},
			{
				list: Element.create("UIListLayout", {
					FillDirection: Enum.FillDirection.Vertical,
					SortOrder: Enum.SortOrder.LayoutOrder,
					VerticalAlignment: Enum.VerticalAlignment.Bottom,
				}),
			},
		);

		return new TooltipsHolder(gui);
	}

	private readonly instances: GuiObject[] = [];
	private tooltips: readonly Tooltip[] = [];
	constructor(instance: GuiObject) {
		super(instance);
		this.event.onPrepare(() => this.set(this.tooltips));
		this.onDisable(() => this.clear());
	}

	private createTooltip(tooltip: Tooltip) {
		const button = keyboardTooltipTemplate();
		button.TextLabel.Text = tooltip.text;

		button.Keys.ImageLabel.Visible = false;

		for (const combination of tooltip.keys) {
			for (const k of combination) {
				if (Keys.isKeyGamepad(k)) {
					if (InputController.inputType.get() !== "Gamepad") continue;

					const key = button.Keys.ImageLabel.Clone();
					key.Visible = true;
					key.Parent = button.Keys;
					key.KeyLabel.Text = "";
					key.Image = UserInputService.GetImageForKeyCode(k);
				} else {
					if (InputController.inputType.get() !== "Desktop") continue;

					const key = button.Keys.ImageLabel.Clone();
					key.Visible = true;
					key.Parent = button.Keys;
					key.KeyLabel.Text = Keys.toReadable(k);
				}
			}
		}

		if (!this.isEnabled()) {
			button.Visible = false;
		}

		return button;
	}

	setFromKeybinds(...keybinds: readonly KeybindRegistration[]) {
		this.set(
			keybinds.map(
				(kb): Tooltip => ({
					text: kb.displayPath[kb.displayPath.size() - 1],
					keys: kb.getKeys(),
				}),
			),
		);
	}

	set(tooltips: readonly Tooltip[]) {
		this.tooltips = tooltips;
		this.justSet(tooltips);
	}

	private clear(): void {
		for (const tooltip of this.instances) {
			tooltip.Destroy();
		}
		this.instances.clear();
	}
	private justSet(tooltips: readonly Tooltip[]) {
		this.clear();

		const set = (tooltips: readonly Tooltip[]) => {
			for (const tooltip of tooltips) {
				if (tooltip.keys.size() === 0) continue;

				const instance = this.createTooltip(tooltip);
				instance.Parent = this.instance;
				this.instances.push(instance);
			}
		};

		const filterTooltipsBy = (tooltips: readonly Tooltip[], func: (key: KeyCode) => boolean): Tooltip[] =>
			tooltips.map((t): Tooltip => ({ ...t, keys: t.keys.filter((k) => k.any(func)) }));

		const inputType = InputController.inputType.get();
		if (inputType === "Desktop") {
			set(filterTooltipsBy(tooltips, (k) => !Keys.isKeyGamepad(k)));
		} else if (inputType === "Gamepad") {
			set(filterTooltipsBy(tooltips, (k) => Keys.isKeyGamepad(k)));
		}
	}
}


import { ContentProvider, Players, ReplicatedStorage, RunService, Workspace } from "@rbxts/services";
import { LoadingController } from "client/controller/LoadingController";
import { BSOD } from "client/gui/BSOD";
import { SandboxGame } from "client/SandboxGame";
import { Interface } from "engine/client/gui/Interface";
import { LocalPlayer } from "engine/client/LocalPlayer";
import { Instances } from "engine/shared/fixes/Instances";
import { GameHostBuilder } from "engine/shared/GameHostBuilder";
import { gameInfo } from "shared/GameInfo";

LocalPlayer.character.waitOnceFor(
	(character) => character !== undefined,
	(character) =>
		character.PivotTo(
			Instances.waitForChild<SpawnLocation>(Workspace, "Map", "SpawnLocation").CFrame.add(new Vector3(0, 1, 0)),
		),
);

task.spawn(() => {
	const allSoundIDs = ReplicatedStorage.Assets.GetDescendants().filter((value) => value.IsA("Sound"));
	ContentProvider.PreloadAsync(allSoundIDs);
});

const host = LoadingController.run("Initializing", () => {
	Interface.getInterface<{ Version: TextLabel }>().Version.Text =
		`v${RunService.IsStudio() ? "studio" : game.PlaceVersion}`;

	const builder = new GameHostBuilder(gameInfo);
	try {
		SandboxGame.initialize(builder);
	} catch (err) {
		BSOD.showWithDefaultText(err, "The game has failed to load.");
		throw err;
	}

	const host = LoadingController.run("Initializing services", () => builder.build());
	LoadingController.run("Starting services", () => host.run());

	return host;
});

//testing
//RunService.IsStudio() &&
if (Players.LocalPlayer.Name === "samlovebutter") {
	//&& (false as boolean)
	/*
	task.spawn(() => {
		while (true as boolean) {
			PlasmaProjectile.spawn.send({
				startPosition: new Vector3(369, -16377, 330),
				baseVelocity: new Vector3(
					0 + (math.random() - 0.5) * 10,
					20 + (math.random() - 0.5) * 10,
					-(500 + (math.random() - 0.5) * 10),
				),
				baseDamage: 1,
			});
			task.wait(0.1);
		}
	});

	task.spawn(() => {
		while (true as boolean) {
			BulletProjectile.spawn.send({
				startPosition: new Vector3(379, -16377, 330),
				baseVelocity: new Vector3(
					0 + (math.random() - 0.5) * 10,
					20 + (math.random() - 0.5) * 10,
					-(500 + (math.random() - 0.5) * 10),
				),
				baseDamage: 1,
			});
			task.wait(0.1);
		}
	});

	LaserProjectile.spawn.send({
		startPosition: new Vector3(359, -16377, 330),
		baseVelocity: new Vector3(
			0 + (math.random() - 0.5) * 10,
			20 + (math.random() - 0.5) * 10,
			-(500 + (math.random() - 0.5) * 10),
		),
		baseDamage: 1,
	});

	*/
}


import { GuiService, StarterGui } from "@rbxts/services";
import { BuildingMode } from "client/modes/build/BuildingMode";
import { requestMode } from "client/modes/PlayModeRequest";
import { RideMode } from "client/modes/ride/RideMode";
import { LocalPlayer } from "engine/client/LocalPlayer";
import { HostedService } from "engine/shared/di/HostedService";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { CustomRemotes } from "shared/Remotes";
import type { PopupController } from "client/gui/PopupController";
import type { GameHostBuilder } from "engine/shared/GameHostBuilder";

@injectable
export class PlayModeController extends HostedService {
	readonly playmode = new ObservableValue<PlayModes | undefined>(undefined);
	private readonly modes;

	static initialize(host: GameHostBuilder) {
		host.services.registerSingletonClass(BuildingMode);
		host.services.registerSingletonClass(RideMode);
		host.services.registerService(PlayModeController);
	}
	constructor(@inject build: BuildingMode, @inject ride: RideMode, @inject popupController: PopupController) {
		super();

		GuiService.SetGameplayPausedNotificationEnabled(false);
		StarterGui.SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false);

		CustomRemotes.modes.setOnClient.subscribe((mode) => {
			this.playmode.set(mode);
			return { success: true };
		});

		this.modes = {
			build,
			ride,
		} as const;

		for (const [_, mode] of pairs(this.modes)) {
			this.parentDestroyOnly(mode);
			mode.disable();
		}

		// TODO: !!important!! this commented code disables the current play mode upon opening any popup, which is usually fine but not always
		// for example, the config tool does not like being disabled when it's the one who opened the popup (byte array editor for example)
		// i see no other way of fixing this right now, so this code is disabled
		// i don't know whether it's fine to disable this or not, so testing required

		// this.event.subscribeObservable(popupController.isShown, (shown) => {
		// 	const active = this.playmode.get();
		// 	if (active) this.modes[active].setEnabled(!shown);
		// });

		this.event.subscribeObservablePrev(this.playmode, (mode, prev) => {
			this.callImmediateSetMode(mode, prev);
			this.setMode(mode, prev);
		});
		this.event.subscribe(LocalPlayer.diedEvent, () => this.setMode(undefined, this.playmode.get()));

		this.setMode(this.playmode.get(), undefined);

		this.onEnable(() => {
			spawn(() => requestMode("build"));
			LocalPlayer.spawnEvent.Connect(() => spawn(() => requestMode("build")));
		});
	}

	get(): PlayModes | undefined {
		return this.playmode.get();
	}

	private callImmediateSetMode(mode: PlayModes | undefined, prev: PlayModes | undefined) {
		if (prev) {
			this.modes[prev].onImmediateSwitchToNext(mode);
		}

		if (mode) {
			this.modes[mode].onImmediateSwitchFromPrev(prev);
		}
	}
	private setMode(mode: PlayModes | undefined, prev: PlayModes | undefined) {
		if (mode === prev) return;

		if (prev) {
			this.modes[prev].onSwitchToNext(mode);
			this.modes[prev].disable();
		}
		if (mode) {
			this.modes[mode].onSwitchFromPrev(prev);
			this.modes[mode].enable();
		}
	}
}


import { ReplicatedStorage, Workspace } from "@rbxts/services";
import { Interface } from "engine/client/gui/Interface";
import { Component } from "engine/shared/component/Component";
import { ComponentInstance } from "engine/shared/component/ComponentInstance";
import { ComponentStateContainer } from "engine/shared/component/ComponentStateContainer";
import { Transforms } from "engine/shared/component/Transforms";
import { Element } from "engine/shared/Element";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { BuildingManager } from "shared/building/BuildingManager";
import { SharedPlot } from "shared/building/SharedPlot";
import { Colors } from "shared/Colors";
import type { MainScreenLayout } from "client/gui/MainScreenLayout";
import type { ActionController } from "client/modes/build/ActionController";
import type { PlayerDataStorageRemotes } from "shared/remotes/PlayerDataRemotes";

type CM = readonly [pos: Vector3, mass: number];
const weightedAverage = (values: readonly CM[]) => {
	const sum = values.reduce((acc, [pos, weight]) => acc.add(pos.mul(weight)), Vector3.zero);
	const totalMass = values.reduce((acc, [, weight]) => acc + weight, 0);

	return sum.div(totalMass);
};

@injectable
export class CenterOfMassVisualizer extends Component {
	private readonly viewportFrame: ViewportFrame;

	private renderedBalls: Model[] = [];
	private machineCOM: Model | undefined;

	constructor(
		parent: Instance,
		@inject actionController: ActionController,
		@inject playerRemotes: PlayerDataStorageRemotes,
	) {
		super();

		this.viewportFrame = Element.create("ViewportFrame", {
			Name: "CenterOfMass",
			Size: UDim2.fromScale(1, 1),
			CurrentCamera: Workspace.CurrentCamera,
			Transparency: 1,
			Ambient: Colors.white,
			LightColor: Colors.white,
			ZIndex: -1000,
			Parent: Interface.getInterface(),
		});
		ComponentInstance.init(this, this.viewportFrame);

		const update = () => {
			if (!this.machineCOM) {
				this.machineCOM = ReplicatedStorage.Assets.Helpers.CenterOfMassMachine.Clone();
				this.machineCOM.Parent = this.viewportFrame;
			}

			const blocks = parent.GetChildren() as BlockModel[];
			const pos = this.calculateCentersOfMass(blocks);

			if (pos.size() > this.renderedBalls.size()) {
				for (let i = 0; i < pos.size() - this.renderedBalls.size(); i++)
					this.renderedBalls.push(ReplicatedStorage.Assets.Helpers.CenterOfMassAssembly.Clone());
			}

			if (pos.size() < this.renderedBalls.size()) {
				while (this.renderedBalls.size() > pos.size()) {
					const index = this.renderedBalls.size() - 1;
					this.renderedBalls[index].Destroy();
					this.renderedBalls.remove(index);
				}
			}

			//let machineCOMpost = new Vector3(0, 0, 0);

			for (let i = 0; i < pos.size(); i++) {
				const ball = this.renderedBalls[i];
				ball.Parent = this.viewportFrame;
				ball.PivotTo(new CFrame(pos[i][0]));
				//machineCOMpost = machineCOMpost.add(pos[i]);
			}

			const weightedAverage = (values: readonly CM[]) => {
				const sum = values.reduce((acc, [pos, weight]) => acc.add(pos.mul(weight)), Vector3.zero);
				const totalMass = values.reduce((acc, [, weight]) => acc + weight, 0);

				return sum.div(totalMass);
			};

			//average pos divided by amount of CoMs
			//this.machineCOM?.PivotTo(new CFrame(machineCOMpost.div(pos.size()))); //<---- nesting hell :D
			this.machineCOM?.PivotTo(new CFrame(weightedAverage(pos)));
		};

		const clear = () => {
			for (const b of this.renderedBalls) b.Destroy();
			this.machineCOM?.Destroy();
			this.machineCOM = undefined;

			this.renderedBalls.clear();
		};

		this.event.subscribe(actionController.onRedo, update);
		this.event.subscribe(actionController.onUndo, update);
		this.event.subscribe(playerRemotes.slots.load.sent, clear);
		this.event.subscribe(playerRemotes.slots.load.completed, (v) => (v.success ? update() : undefined));
		this.event.subscribe(SharedPlot.anyChanged, update);

		this.onEnabledStateChange((enabled) => {
			if (enabled) update();
			else clear();
		});
	}

	private calculateCentersOfMass(blocks: readonly BlockModel[]): readonly CM[] {
		const partsInUse: Set<BlockModel> = new Set();
		const ass: CM[] = [];

		// ACTUALLY WRONG; returns the center of every part inside but they're not weighted; but who cares
		const getBlockMass = (block: BlockModel): LuaTuple<[pos: Vector3, mass: number]> => {
			let mass = 0;
			let center = Vector3.zero;
			let amount = 0;

			for (const part of block.GetDescendants()) {
				if (part.IsA("BasePart") && !part.Massless) {
					amount++;

					mass += part.Mass;
					center = center.add(part.Position);
				}
			}

			return $tuple(center.div(amount), mass);
		};
		// ACTUALLY WRONG; returns the center of every part inside but they're not weighted; but who cares
		const getAssemblyMass = (asm: readonly BlockModel[]): LuaTuple<[pos: Vector3, mass: number]> => {
			let mass = 0;
			let center = Vector3.zero;

			for (const b of asm) {
				const [c, m] = getBlockMass(b);
				mass += m;
				center = center.add(c);
			}

			return $tuple(center.div(asm.size()), mass);
		};

		for (const block of blocks) {
			if (partsInUse.has(block)) continue;

			const assembly = BuildingManager.getAssemblyBlocks(block);
			for (const b of assembly) {
				partsInUse.add(b);
			}

			const vectorSum = weightedAverage(assembly.map((b) => [...getBlockMass(b)] as unknown as CM));
			ass.push([vectorSum, getAssemblyMass(assembly)[1]]);
		}

		return ass;
	}
}

@injectable
export class CenterOfMassController extends Component {
	constructor(
		@inject mainScreen: MainScreenLayout,
		@inject plot: SharedPlot,
		@inject actionController: ActionController,
		@inject playerRemotes: PlayerDataStorageRemotes,
	) {
		super();

		const visualizerState = ComponentStateContainer.create(
			this,
			new CenterOfMassVisualizer(plot.instance.WaitForChild("Blocks"), actionController, playerRemotes),
		);

		const enabledByButton = new ObservableValue(false);
		visualizerState.subscribeAndFrom({ enabledByButton });
		const button = this.parentGui(mainScreen.registerTopRightButton("CenterOfMass")) //
			.addButtonAction(() => enabledByButton.set(!enabledByButton.get()));

		this.event.subscribeObservable(
			visualizerState,
			(enabled) =>
				Transforms.create()
					.transform(button.instance, "Transparency", enabled ? 0 : 0.5, Transforms.commonProps.quadOut02)
					.run(button.instance),
			true,
		);
	}
}


import { ReplicatedStorage, Workspace } from "@rbxts/services";
import { Interface } from "engine/client/gui/Interface";
import { Component } from "engine/shared/component/Component";
import { ComponentInstance } from "engine/shared/component/ComponentInstance";
import { ComponentStateContainer } from "engine/shared/component/ComponentStateContainer";
import { Transforms } from "engine/shared/component/Transforms";
import { Element } from "engine/shared/Element";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { BlockManager } from "shared/building/BlockManager";
import { SharedPlot } from "shared/building/SharedPlot";
import { Colors } from "shared/Colors";
import type { MainScreenLayout } from "client/gui/MainScreenLayout";
import type { ActionController } from "client/modes/build/ActionController";
import type { PlayerDataStorageRemotes } from "shared/remotes/PlayerDataRemotes";

@injectable
export class WeldVisualizer extends Component {
	private readonly viewportFrame: ViewportFrame;

	private readonly free: Part[] = [];
	private readonly rendered: Part[] = [];
	private machineCOM: Model | undefined;

	constructor(
		parent: Instance,
		@inject actionController: ActionController,
		@inject playerRemotes: PlayerDataStorageRemotes,
	) {
		super();

		const prefab = Element.create("Part", {
			Anchored: true,
			CanCollide: false,
			CanQuery: false,
			CanTouch: false,
			CastShadow: false,

			Material: Enum.Material.Neon,
			Transparency: 0.2,
			Shape: Enum.PartType.Cylinder,
			Color: Colors.red,
		});

		this.viewportFrame = Element.create("ViewportFrame", {
			Name: "Welds",
			Size: UDim2.fromScale(1, 1),
			CurrentCamera: Workspace.CurrentCamera,
			Transparency: 1,
			Ambient: Colors.white,
			LightColor: Colors.white,
			ZIndex: -1000,
			Parent: Interface.getInterface(),
		});
		ComponentInstance.init(this, this.viewportFrame);

		const startFrame = () => {
			for (const item of this.rendered) {
				this.free.push(item);
				item.Parent = undefined;
			}

			this.rendered.clear();
		};
		const nextWeldInstance = (): Part => {
			const item = this.free.pop() ?? prefab.Clone();

			this.rendered.push(item);
			item.Parent = this.viewportFrame;
			return item;
		};

		const update = () => {
			if (!this.machineCOM) {
				this.machineCOM = ReplicatedStorage.Assets.Helpers.CenterOfMassMachine.Clone();
				this.machineCOM.Parent = this.viewportFrame;
			}

			startFrame();

			const blocks = parent.GetChildren() as BlockModel[];
			for (const block of blocks) {
				for (const weld of block.GetDescendants().filter((c) => c.IsA("WeldConstraint"))) {
					const block1 = BlockManager.tryGetBlockModelByPart(weld.Part0);
					const block2 = BlockManager.tryGetBlockModelByPart(weld.Part1);
					if (!block1 || !block2 || block1 === block2) {
						continue;
					}

					const pos1 = block1.GetPivot().Position;
					const pos2 = block2.GetPivot().Position;

					const cloned = nextWeldInstance();
					cloned.Parent = this.viewportFrame;

					const distance = pos1.sub(pos2).Magnitude;
					cloned.Size = new Vector3(distance - 0.4, 0.07, 0.07);
					cloned.CFrame = new CFrame(pos2, pos1)
						.mul(new CFrame(0, 0, -distance / 2))
						.mul(CFrame.Angles(0, math.rad(90), 0));
				}
			}
		};

		const clear = () => {
			for (const b of this.rendered) {
				b.Destroy();
			}

			this.rendered.clear();
		};

		this.event.subscribe(actionController.onRedo, update);
		this.event.subscribe(actionController.onUndo, update);
		this.event.subscribe(playerRemotes.slots.load.sent, clear);
		this.event.subscribe(playerRemotes.slots.load.completed, (v) => (v.success ? update() : undefined));
		this.event.subscribe(SharedPlot.anyChanged, update);

		this.onEnabledStateChange((enabled) => {
			if (enabled) update();
			else clear();
		});
	}
}

@injectable
export class WeldVisualizerController extends Component {
	constructor(
		@inject mainScreen: MainScreenLayout,
		@inject plot: SharedPlot,
		@inject actionController: ActionController,
		@inject playerRemotes: PlayerDataStorageRemotes,
	) {
		super();

		const visualizerState = ComponentStateContainer.create(
			this,
			new WeldVisualizer(plot.instance.WaitForChild("Blocks"), actionController, playerRemotes),
		);

		const enabledByButton = new ObservableValue(false);
		visualizerState.subscribeAndFrom({ enabledByButton });
		const button = this.parentGui(mainScreen.addTopRightButton("CenterOfMass", 84532983912875)) //
			.addButtonAction(() => enabledByButton.set(!enabledByButton.get()));

		this.event.subscribeObservable(
			visualizerState,
			(enabled) =>
				Transforms.create()
					.transform(button.instance, "Transparency", enabled ? 0 : 0.5, Transforms.commonProps.quadOut02)
					.run(button.instance),
			true,
		);
	}
}