import { Players, RunService } from "@rbxts/services";
import { Component } from "engine/shared/component/Component";
import { Objects } from "engine/shared/fixes/Objects";
import { BlockManager } from "shared/building/BlockManager";
import { Physics } from "shared/Physics";
import { TagUtils } from "shared/utils/TagUtils";
import type { BlockDamageController } from "engine/shared/BlockDamageController";

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

const materialStrength: { readonly [k in Enum.Material["Name"]]: number } = Objects.fromEntries(
	Enum.Material.GetEnumItems().map((material) => {
		const physicalProperties = new PhysicalProperties(material);
		const strongness = math.max(0.5, physicalProperties.Density / 3.5);
		$debug(`Strength of '${material.Name}' set to ${strongness}`);

		return [material.Name, strongness] as const;
	}),
);

const getVolume = (vector: Vector3) => vector.X * vector.Y * vector.Z;

const player = Players.LocalPlayer;
let airModifier = 0;

RunService.Heartbeat.Connect(() => {
	const ch = player?.Character;
	if (!ch) return;
	airModifier = Physics.GetAirDensityModifierOnHeight(Physics.LocalHeight.fromGlobal(ch.GetPivot().Position.Y));
});

@injectable
export class ImpactController extends Component {
	static isImpactAllowed(part: BasePart) {
		if (
			!part.CanTouch ||
			!part.CanCollide ||
			part.IsA("VehicleSeat") ||
			math.max(part.Size.X, part.Size.Y, part.Size.Z) < 0.5
		) {
			return false;
		}
		return true;
	}

	constructor(
		blocks: readonly { readonly instance: BlockModel }[],
		@inject private readonly blockDamageController: BlockDamageController,
	) {
		super();

		task.delay(0.1, () => {
			for (const block of blocks) {
				this.subscribeOnBlock(block);
			}
		});
	}

	subscribeOnBlock(block: { readonly instance: BlockModel }) {
		// init health
		this.blockDamageController.initHealth(block.instance);

		for (const part of block.instance.GetDescendants()) {
			if (!part.IsA("BasePart")) continue;
			if (!ImpactController.isImpactAllowed(part)) continue;

			this.subscribeOnBasePart(part);
		}
	}

	subscribeOnBasePart(part: BasePart) {
		// do nothing for disabled impact
		if (part.HasTag(TagUtils.allTags.IMPACT_UNBREAKABLE)) return;

		// do nothing for parts that's not even in ride mode
		if (!BlockManager.isActiveBlockPart(part)) return;

		// Optimization (do nothing for non-connected blocks)
		if (part.GetJoints().size() === 0) return;

		const block = part.Parent as BlockModel;
		if (!block) return;

		part.Touched.Connect((hit: BasePart | Terrain) => {
			// Optimization (do nothing for non-connected blocks)
			if (part.AssemblyMass === part.Mass) {
				// I kinda see a flaw in that logic but alright
				// - @samlovebutter
				return;
			}

			// Do nothing for non-collidable blocks
			if (!hit.CanCollide) return;

			// Compute magnitudes
			const partSpeed = part.AssemblyLinearVelocity.Magnitude + part.AssemblyAngularVelocity.Magnitude;
			const secondPartSpeed = hit.AssemblyLinearVelocity.Magnitude + hit.AssemblyAngularVelocity.Magnitude;

			const speedDiff = math.abs(partSpeed - secondPartSpeed);

			this.blockDamageController.applyDamage(block, {
				impactDamage: speedDiff,
				heatDamage: 0.1 * airModifier, // 0.1 (10%) is just a chance of ignition
			});
		});
	}
}


import { RunService } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { GameDefinitions } from "shared/data/GameDefinitions";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {},
	output: {
		result: {
			displayName: "Altitude",
			types: ["number"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as AltimeterBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition> {
	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		this.event.subscribe(RunService.PostSimulation, () => {
			const result = this.instance.GetPivot().Y - GameDefinitions.HEIGHT_OFFSET;
			this.output.result.set("number", result);
		});
	}
}

export const AltimeterBlock = {
	...BlockCreation.defaults,
	id: "altimeter",
	displayName: "Altimeter",
	description: "Returns current height",

	logic: { definition, ctor: Logic },
	search: { partialAliases: ["height sensor"] },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {},
	output: {
		result: {
			displayName: "Angle",
			unit: "Radians",
			types: ["vector3"],
		},
		normal: {
			displayName: "Normal",
			unit: "Studs",
			types: ["vector3"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as AngleSensorBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition> {
	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		const initialRotation = this.instance.GetPivot().Rotation;

		this.event.subscribe(RunService.PostSimulation, () => {
			const objSpace = initialRotation.ToObjectSpace(this.instance.GetPivot().Rotation);
			const [x, y, z] = objSpace.ToEulerAnglesYXZ();
			const normal = objSpace.RightVector.mul(-1);
			this.output.result.set("vector3", new Vector3(x, y, z));
			this.output.normal.set("vector3", normal);
		});
	}
}

export const AngleSensorBlock = {
	...BlockCreation.defaults,
	id: "anglesensor",
	displayName: "Angle Sensor",
	description: "Returns its angle",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { Players, RunService, UserInputService, Workspace } from "@rbxts/services";
import { EventHandler } from "engine/shared/event/EventHandler";
import { A2SRemoteEvent, S2CRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { t } from "engine/shared/t";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockSynchronizer } from "shared/blockLogic/BlockSynchronizer";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {
		detachKey: {
			displayName: "Attach/Detach",
			tooltip: "Attach or detach the back mount.",
			types: {
				key: {
					config: "H" as KeyCode,
				},
			},
			connectorHidden: true,
		},

		detachBool: {
			displayName: "Attach/Detach",
			tooltip: "Attach or detach the back mount.",
			types: {
				bool: {
					config: false,
				},
			},
			configHidden: true,
		},

		connectToRootPart: {
			displayName: "RootPart attachment",
			tooltip: "Make back mount attached to your RootPart instead of your actual back.",
			types: {
				bool: {
					config: true,
				},
			},
			connectorHidden: true,
		},

		shared: {
			displayName: "Shared",
			tooltip: "Allows other players to wear your back mount. It doesn't work as it used to.",
			types: {
				bool: {
					config: false,
				},
			},
			connectorHidden: true,
		},
	},
	output: {
		mounted: {
			displayName: "Occupied",
			tooltip: "Returns true if player is mounted",
			types: ["bool"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type BackMountModel = BlockModel & {
	ProximityPrompt: ProximityPrompt;
	mainPart: BasePart;
	PlayerWeldConstraint: Motor6D;
};

// declaring constants here
const MAX_PROMPT_VISIBILITY_DISTANCE = 5;
const MAX_PROMPT_VISIBILITY_DISTANCE_EQUIPPED = 15;

const owners = new Map<BackMountModel, Player | undefined>();
const updateWeld = (caller: Player, owner: Player, block: BackMountModel, connectToRootPart: boolean) => {
	const weldOwner = owners.get(block);

	if (weldOwner === undefined) {
		Logic.events.weldMountUpdate.send({
			block,
			weldedState: true,
			owner,
			connectToRootPart,
		});
		return;
	}

	if (weldOwner === caller) {
		Logic.events.weldMountUpdate.send({
			block,
			weldedState: false,
			owner,
			connectToRootPart,
		});
		return;
	}
};

const ownerSideInit = ({ block, key, owner, connectToRootPart }: proximityInferedType, pp: ProximityPrompt) => {
	// set activation key
	const k = Enum.KeyCode[key as unknown as never];
	const isUnknownKeybind = k === Enum.KeyCode.Unknown;

	const player = Players.LocalPlayer;
	const mainPart = block.FindFirstChild("mainPart") as BasePart;
	if (!mainPart) return;

	// remote client event handler
	const handler = new EventHandler();

	// subscribe to block being destroyed
	handler.subscribe(block.DescendantRemoving, () => handler.unsubscribeAll());
	handler.subscribe(pp.Triggered, () => updateWeld(player, owner, block, connectToRootPart));

	// subscribe to keypress
	handler.subscribe(UserInputService.InputBegan, (input, gameProccessed) => {
		if (gameProccessed) return;
		if (isUnknownKeybind) return;
		if (input.KeyCode !== k) return;

		updateWeld(player, owner, block, connectToRootPart);
	});

	// some checks so the prompt disappears when player wearing
	handler.subscribe(RunService.Heartbeat, () => {
		const weldOwner = owners.get(block);
		if (weldOwner !== player) return;

		const camera = Workspace.CurrentCamera;
		if (!camera) return;

		const distance = camera.CFrame.Position.sub(mainPart.Position).Magnitude;
		pp.MaxActivationDistance = distance > MAX_PROMPT_VISIBILITY_DISTANCE_EQUIPPED ? 0 : distance;
	});
};

const otherClientSideInit = (
	{ block, key, isPublic, owner, connectToRootPart }: proximityInferedType,
	pp: ProximityPrompt,
) => {
	// set activation key
	const k = Enum.KeyCode[key as unknown as never];
	const isUnknownKeybind = k === Enum.KeyCode.Unknown;

	const player = Players.LocalPlayer;
	const mainPart = block.FindFirstChild("mainPart") as BasePart;
	if (!mainPart) return;

	// remote client event handler
	const handler = new EventHandler();

	// subscribe to block being destroyed
	handler.subscribe(block.DescendantRemoving, () => handler.unsubscribeAll());
	handler.subscribe(pp.Triggered, () => updateWeld(player, owner, block, connectToRootPart));

	// subscribe to keypress
	handler.subscribe(UserInputService.InputBegan, (input, gameProccessed) => {
		if (gameProccessed) return;
		if (isUnknownKeybind) return;
		if (input.KeyCode !== k) return;

		// make it only available to unweld on the same key
		if (owners.get(block) !== player) return;

		updateWeld(player, owner, block, connectToRootPart);
	});

	// some checks so the prompt disappears when player wearing
	handler.subscribe(RunService.Heartbeat, () => {
		if (!isPublic) return;
		const weldOwner = owners.get(block);
		// these two checks are placed here ON PURPOSE
		// allows the owner of the block to unequip the block off other players
		if (weldOwner === undefined) {
			pp.MaxActivationDistance = MAX_PROMPT_VISIBILITY_DISTANCE_EQUIPPED;
			return;
		}

		if (weldOwner !== player) {
			pp.MaxActivationDistance = 0;
			return;
		}

		const camera = Workspace.CurrentCamera;
		if (!camera) return;

		const distance = camera.CFrame.Position.sub(mainPart.Position).Magnitude;
		pp.MaxActivationDistance = distance > MAX_PROMPT_VISIBILITY_DISTANCE_EQUIPPED ? 0 : distance;
	});
};

const updateProximity = (data: proximityInferedType) => {
	const block = data.block;
	const key = data.key;
	const pp = block.FindFirstChild("ProximityPrompt") as typeof block.ProximityPrompt;
	if (!pp) return;

	// set activation key
	const k = Enum.KeyCode[key as unknown as never];
	const isUnknownKeybind = k === Enum.KeyCode.Unknown;

	block.DescendantRemoving.Connect(() => owners.delete(block));

	if (!isUnknownKeybind) {
		pp.KeyboardKeyCode = k;
		pp.GamepadKeyCode = k;
	} else pp.Enabled = false;

	if (data.owner === Players.LocalPlayer) {
		pp.Enabled = true;
		pp.MaxActivationDistance = MAX_PROMPT_VISIBILITY_DISTANCE;
		ownerSideInit(data, pp);
	} else {
		pp.Enabled = data.isPublic;
		pp.MaxActivationDistance = data.isPublic ? MAX_PROMPT_VISIBILITY_DISTANCE : 0;
		otherClientSideInit(data, pp);
	}
};

const proximityEventType = t.interface({
	block: t.instance("Model").nominal("blockModel").as<BackMountModel>(),
	connectToRootPart: t.boolean,
	owner: t.any.as<Player>(),
	isPublic: t.boolean,
	key: t.string,
});

type proximityInferedType = t.Infer<typeof proximityEventType>;

type weldTypeEvent = {
	readonly block: BackMountModel;
	readonly weldedState: boolean;
	readonly owner: Player;
	readonly connectToRootPart: boolean;
};

type logicUpdateEvent = {
	readonly block: BackMountModel;
	readonly weldedTo: Player | undefined;
};

export type { Logic as BackMountBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition, BackMountModel> {
	static readonly events = {
		updateLogic: new S2CRemoteEvent<logicUpdateEvent>("backmount_logic", "RemoteEvent"),
		weldMountUpdate: new A2SRemoteEvent<weldTypeEvent>("backmount_weld", "RemoteEvent"),
		updateProximity: new BlockSynchronizer<proximityInferedType>(
			"backmount_proximity",
			proximityEventType,
			updateProximity,
		),
	} as const;

	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		// update pressable key
		this.onk(["detachKey", "shared", "connectToRootPart"], ({ detachKey, shared, connectToRootPart }) => {
			Logic.events.updateProximity.send({
				block: this.instance,
				key: detachKey,
				isPublic: shared,
				owner: Players.LocalPlayer,
				connectToRootPart,
			});
		});

		// call weld stuff on detach bool
		this.onk(["detachBool", "connectToRootPart"], ({ detachBoolChanged, detachBool, connectToRootPart }) => {
			if (!detachBoolChanged) return;
			Logic.events.weldMountUpdate.send({
				block: this.instance,
				weldedState: detachBool,
				owner: Players.LocalPlayer,
				connectToRootPart,
			});
		});

		if (RunService.IsClient()) {
			this.event.subscribe(Logic.events.updateLogic.invoked, ({ block, weldedTo }) => {
				if (block !== this.instance) return;
				this.output.mounted.set("bool", !!weldedTo);
			});
		}
	}
}

// add handler to make it constantly fill the map
Logic.events.updateLogic.invoked.Connect(({ block, weldedTo }) => {
	owners.set(block, weldedTo);
	const pp = block.FindFirstChild("ProximityPrompt") as typeof block.ProximityPrompt;
	if (!pp) return;
	pp.ActionText = weldedTo ? "Detach" : "Attach";
	pp.MaxActivationDistance = !weldedTo ? MAX_PROMPT_VISIBILITY_DISTANCE : 0;
});

export const BackMountBlock = {
	...BlockCreation.defaults,
	id: "backmount",
	displayName: "Back Mount",
	description: "A mountable backpack. You can weld stuff to it and wear it.",
	limit: 15,

	search: {
		partialAliases: ["body", "backpack"],
	},

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { A2SRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { Colors } from "shared/Colors";
import { GameDefinitions } from "shared/data/GameDefinitions";
import type { ManualBeacon } from "client/gui/Beacon";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: ["enabled", "text", "showUpDistance", "markerColor", "position"],
	input: {
		enabled: {
			displayName: "Enabled",
			tooltip: "Enable/disable beacon visibility.",
			unit: "state",
			types: {
				bool: {
					config: true,
				},
			},
		},
		text: {
			displayName: "Text",
			tooltip: "The text that will appear under the beacon's marker.",
			types: {
				string: {
					config: "New Beacon",
				},
			},
		},
		showUpDistance: {
			displayName: "Show Up Distance",
			tooltip: "The distance at which you can see the marker.",
			unit: "meters",
			types: {
				number: {
					config: 0,
				},
			},
		},
		markerColor: {
			displayName: "Marker Color",
			tooltip: "The color of the marker.",
			types: {
				color: {
					config: Colors.green,
				},
			},
		},
		position: {
			displayName: "Position",
			tooltip: "Position of the marker. Leave at [0, 0, 0] to use the block position.",
			types: {
				vector3: {
					config: Vector3.zero,
				},
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

type beaconBlock = BlockModel & {
	LED: BasePart;
};

interface UpdateData {
	readonly block: beaconBlock;
	readonly color: Color3;
}

export type { Logic as BeaconBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition> {
	static readonly events = {
		update: new A2SRemoteEvent<UpdateData>("beacon_update"),
	} as const;

	static updateLedColor(data: UpdateData) {
		if (!data.block) return;
		data.block.LED.Color = data.color;
	}

	beaconInstance: ManualBeacon | undefined;
	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		let timePassed = 0;
		const maxTimeInSeconds = 1;
		const startColor = Colors.black;

		const en = this.initializeInputCache("enabled");
		const mc = this.initializeInputCache("markerColor");
		const dst = this.initializeInputCache("showUpDistance");
		const beacon: beaconBlock = this.instance as beaconBlock;

		const updateColor = (color: Color3) => {
			const data: UpdateData = {
				block: beacon,
				color: color,
			};

			Logic.updateLedColor(data);
			Logic.events.update.send(data);
		};

		this.event.subscribe(RunService.Heartbeat, (seconds) => {
			//апдейт минимальной дистанции срабатывания маяка
			if (this.beaconInstance !== undefined) {
				this.beaconInstance.showUpDistance = dst.get();
			}

			// ладно, я знаю как сделать хитрожопо, но потом хуй прочитаешь
			// да и тем более это было не оптимизировано
			if (!en.get()) {
				timePassed = 0;
				return updateColor(startColor);
			}

			timePassed = (timePassed + seconds) % maxTimeInSeconds;
			const progress = 1 - timePassed / maxTimeInSeconds;

			if (!progress) return;

			updateColor(startColor.Lerp(mc.get(), progress));
		});

		this.onk(["text", "enabled"], ({ text, enabled }) => {
			this.updateData(text, enabled);
		});

		this.onk(["markerColor"], ({ markerColor }) => {
			if (this.beaconInstance === undefined) return;
			this.beaconInstance.billboard.Title.TextColor3 = markerColor;
			this.beaconInstance.billboard.ImageLabel.ImageColor3 = markerColor;
			this.beaconInstance.billboard.Distance.TextColor3 = markerColor;
		});

		const positionCache = this.initializeInputCache("position");
		this.onTicc(() => {
			if (!this.beaconInstance) return;

			let position = positionCache.tryGet();
			if (!position || position === Vector3.zero) {
				position = this.instance.GetPivot().Position;
			} else {
				position = position.add(new Vector3(0, GameDefinitions.HEIGHT_OFFSET, 0));
			}

			this.beaconInstance.position = position;
		});

		this.onDisable(() => this.beaconInstance?.destroy());
	}

	//ВЫЗЫВАЕТСЯ ИЗ ВНЕ (RideModeScene.ts)! НЕ МЕНЯТЬ!
	updateData(text: string, enabled: boolean) {
		if (this.beaconInstance === undefined) return;
		this.beaconInstance.billboard.Title.Text = text;
		this.beaconInstance.setEnabled(enabled);
	}
}

export const BeaconBlock = {
	...BlockCreation.defaults,
	id: "beacon",
	displayName: "Beacon",
	description: "Switchable marker. Only you can see it. Shows itself if its position input is [0, 0, 0]",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {},
	output: {
		result: {
			displayName: "Axle angle",
			unit: "Radians",
			types: ["number"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type BlockDefinition = BlockModel & {
	readonly Union: BasePart;
	readonly Part: BasePart;
};

export type { Logic as AngleSensorBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition, BlockDefinition> {
	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		const base = this.instance.Union;
		const axle = this.instance.Part;
		const initial = base.GetPivot().ToObjectSpace(axle.GetPivot()).ToEulerAnglesXYZ()[0];

		this.event.subscribe(RunService.PostSimulation, () => {
			const [x] = base.GetPivot().ToObjectSpace(axle.GetPivot()).ToEulerAnglesXYZ();
			this.output.result.set("number", ((x - initial + math.pi) % (math.pi * 2)) - math.pi);
		});
	}
}

export const BearingShaftBlock = {
	...BlockCreation.defaults,
	id: "bearingshaft",
	displayName: "Bearing Shaft",
	description: "A free spinning shaft with a bearing",
	search: {
		partialAliases: ["angle"],
	},

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { Players, RunService } from "@rbxts/services";
import { EventHandler } from "engine/shared/event/EventHandler";
import { A2OCRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { Instances } from "engine/shared/fixes/Instances";
import { t } from "engine/shared/t";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockSynchronizer } from "shared/blockLogic/BlockSynchronizer";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { PlayerDataStorage } from "client/PlayerDataStorage";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: ["text", "switchMode", "sharedMode", "lampColor", "buttonColor"],
	input: {
		text: {
			displayName: "Text",
			tooltip: "Text on the button.",
			types: {
				string: {
					config: "CLICK ME!",
				},
			},
			connectorHidden: true,
		},
		sharedMode: {
			displayName: "Shared",
			tooltip: "Allows other players to press the button if enabled.",
			types: {
				bool: {
					config: false,
				},
			},
			connectorHidden: true,
		},
		switchMode: {
			displayName: "Switch Mode",
			tooltip: "Button will act as a switch if enabled.",
			types: {
				bool: {
					config: false,
				},
			},
			connectorHidden: true,
		},
		buttonColor: {
			displayName: "Button Color",
			types: {
				color: {
					config: Color3.fromRGB(196, 40, 28),
				},
			},
			connectorHidden: true,
		},
		lampColor: {
			displayName: "Lamp Color",
			types: {
				color: {
					config: Color3.fromRGB(0xff, 0xff, 0xff),
				},
			},
		},
	},
	output: {
		result: {
			displayName: "Pressed",
			types: ["bool"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type buttonType = BlockModel & {
	Button: BasePart & {
		SurfaceGui: {
			TextLabel: TextLabel;
		};
	};
	Base: UnionOperation & {
		AlignPosition: AlignPosition;
		PressedPosition: Attachment;
		DepressedPosition: Attachment;
		ClickDetector: ClickDetector;
	};
	LED: BasePart;
};
const baseLEDColor = Color3.fromRGB(17, 17, 17);
const updateTextDataType = t.interface({
	block: t.instance("Model").as<buttonType>(),
	buttonColor: t.color,
	text: t.string,
});
const updateStateDataType = t.interface({
	block: t.instance("Model").as<buttonType>(),
	LEDcolor: t.color,
	buttonState: t.boolean,
});
const initButtonType = t.interface({
	block: t.instance("Model").as<buttonType>(),
	owner: t.instance("Player"),
});

type updateTextData = t.Infer<typeof updateTextDataType>;
type updateStateData = t.Infer<typeof updateStateDataType>;
type initButton = t.Infer<typeof initButtonType>;

const updateButtonText = ({ block, buttonColor, text }: updateTextData) => {
	block.Button.SurfaceGui.TextLabel.Text = text;
	block.Button.Color = buttonColor;
};

const updateButtonState = ({ block, LEDcolor, buttonState }: updateStateData) => {
	block.LED.Color = buttonState ? LEDcolor : baseLEDColor;
	block.Base.AlignPosition.Attachment1 = buttonState ? block.Base.PressedPosition : block.Base.DepressedPosition;
};

const init = ({ block, owner }: initButton) => {
	// handler just in case I decide to change some things
	const handler = new EventHandler();

	block.Base.ClickDetector.MaxActivationDistance = math.huge;
	const onClickEvent = () => clickEvent.send(owner, block);
	handler.subscribe(block.Base.ClickDetector.MouseClick, onClickEvent);
	handler.subscribe(block.DescendantRemoving, () => handler.unsubscribeAll());
};

const events = {
	updateText: new BlockSynchronizer("b_button_data_update_text", updateTextDataType, updateButtonText),
	updateState: new BlockSynchronizer("b_button_data_update_state", updateStateDataType, updateButtonState),
	init: new BlockSynchronizer("b_button_init", initButtonType, init),
} as const;
events.updateText.sendBackToOwner = true;

const clickEvent = new A2OCRemoteEvent<buttonType>("b_button_click", "RemoteEvent");

export type { Logic as ButtonBlockLogic };
@injectable
class Logic extends InstanceBlockLogic<typeof definition, buttonType> {
	constructor(block: InstanceBlockLogicArgs, @tryInject playerDataStorage?: PlayerDataStorage) {
		super(definition, block);

		const inst = this.instance;
		if (!inst) return;
		const button = inst.Button;

		const led = inst.LED;

		const cachedLEDColor = this.initializeInputCache("lampColor");
		const cachedSwitchMode = this.initializeInputCache("switchMode");
		const cachedSharedMode = this.initializeInputCache("sharedMode");

		let UpdateOnNextTick = false;
		let isPressed = false;

		const upd = () => {
			events.updateState.sendOrBurn(
				{
					block: inst,
					buttonState: isPressed,
					LEDcolor: cachedLEDColor.get(),
				},
				this,
			);
		};

		const pressButton = (playerWhoClicked: Player) => {
			if (!cachedSharedMode.get() && Players.LocalPlayer !== playerWhoClicked) return;

			const isSwitch = cachedSwitchMode.get();

			if (isSwitch) {
				this.output.result.set("bool", (isPressed = !isPressed));
				upd();
				return;
			}

			this.output.result.set("bool", (isPressed = true));
			const triggerLamp = playerDataStorage?.config.get().graphics.logicEffects === true;
			if (triggerLamp) {
				led.Color = cachedLEDColor.get();
				upd();
			}
			UpdateOnNextTick = true;
		};

		// we should probably add server support but noone cares
		if (RunService.IsClient()) {
			this.event.subscribe(clickEvent.invoked, (block, whoPressed) => {
				if (this.instance !== block) return;
				if (!whoPressed) return;
				pressButton(whoPressed);
			});
		}

		this.onEnable(() => this.output.result.set("bool", false));

		this.onk(["sharedMode"], ({ sharedMode }) => {
			if (!sharedMode) init({ block: inst, owner: Players.LocalPlayer });
			else events.init.sendOrBurn({ block: inst, owner: Players.LocalPlayer }, this);
		});

		this.onk(["buttonColor", "text"], ({ buttonColor, text }) => {
			button.Color = buttonColor;
			button.SurfaceGui.TextLabel.Text = text;

			events.updateText.sendOrBurn(
				{
					block: inst,
					text: button.SurfaceGui.TextLabel.Text,
					buttonColor: buttonColor,
				},
				this,
			);
		});

		this.onAlwaysInputs(({ switchMode }) => {
			if (UpdateOnNextTick) return (UpdateOnNextTick = false);
			if (!switchMode && isPressed) {
				this.output.result.set("bool", (isPressed = false));
				upd();
			}
		});
	}
}

const immediate = BlockCreation.immediate(definition, (block: buttonType, config) => {
	const btn = Instances.waitForChild(block, "Button");
	Instances.waitForChild(btn, "SurfaceGui", "TextLabel");

	events.updateText.send({
		block,
		buttonColor: BlockCreation.defaultIfWiredUnset(
			config?.buttonColor,
			definition.input.buttonColor.types.color.config,
		),
		text: BlockCreation.defaultIfWiredUnset(config?.text, definition.input.text.types.string.config),
	});
});

export const ButtonBlock = {
	...BlockCreation.defaults,
	id: "button",
	displayName: "Button",
	description: "Returns true when the button is clicked or tapped. Can be activated by other players if configured.",

	logic: { definition, ctor: Logic, events, immediate },
} as const satisfies BlockBuilder;


import { Lighting, Players, RunService, UserInputService, Workspace } from "@rbxts/services";
import { Component } from "engine/shared/component/Component";
import { Element } from "engine/shared/Element";
import { ObservableValue } from "engine/shared/event/ObservableValue";
import { A2SRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { Objects } from "engine/shared/fixes/Objects";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { Colors } from "shared/Colors";
import type { PlayerDataStorage } from "client/PlayerDataStorage";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: ["enabled", "controllable", "fov", "saturation", "brightness", "contrast", "tint"],
	input: {
		enabled: {
			displayName: "Enabled",
			types: {
				bool: {
					config: false,
					control: {
						config: {
							enabled: true,
							key: "F",
							reversed: false,
							switch: true,
						},
						canBeReversed: true,
						canBeSwitch: true,
					},
				},
			},
		},
		controllable: {
			displayName: "Controllable",
			connectorHidden: true,
			types: { bool: { config: false } },
		},
		fov: {
			displayName: "Field Of View",
			types: {
				number: {
					config: 70,
					clamp: {
						showAsSlider: true,
						min: 1,
						max: 120,
					},
				},
			},
		},
		saturation: {
			displayName: "Saturation",
			types: {
				number: {
					config: 0,
					clamp: {
						showAsSlider: true,
						min: -1,
						max: 1,
					},
				},
			},
		},
		brightness: {
			displayName: "Brightness",
			types: {
				number: {
					config: 0,
					clamp: {
						showAsSlider: true,
						min: -1,
						max: 1,
					},
				},
			},
		},
		contrast: {
			displayName: "Contrast",
			types: {
				number: {
					config: 0,
					clamp: {
						showAsSlider: true,
						min: -1,
						max: 1,
					},
				},
			},
		},
		tint: {
			displayName: "Tint",
			types: {
				color: {
					config: Colors.white,
				},
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

const defaultCamera = Workspace.CurrentCamera;
if (RunService.IsClient() && defaultCamera) {
	// Upon setting the Workspace.CurrentCamera, all cameras that are children of Workspace get deleted, so we create a nested folder for the camera
	defaultCamera.Parent = Element.create("Folder", { Name: "Camera", Parent: Workspace });
}

const enabledCameras = new Set<Logic>();

export type { Logic as CameraBlockLogic };
@injectable
class Logic extends InstanceBlockLogic<typeof definition> {
	static readonly events = {
		update: new A2SRemoteEvent<{ readonly block: BlockModel; readonly enabled: boolean }>("b_camera_update"),
	} as const;

	constructor(block: InstanceBlockLogicArgs, @inject playerData: PlayerDataStorage) {
		super(definition, block);

		if (!RunService.IsClient()) return;
		if (!defaultCamera) return;

		const target = this.instance.WaitForChild("Target") as BasePart;
		const camera = Element.create("Camera", {
			CFrame: target.CFrame,
			CameraSubject: target,
			CameraType: Enum.CameraType.Scriptable,
			Parent: this.instance,
		});

		const effect = Element.create("ColorCorrectionEffect", {
			Enabled: false,
			Name: `ColorCorrectionCamera_${block.instance}`,
			Parent: Lighting,
		});
		this.onDisable(() => effect.Destroy());

		this.onk(["fov"], ({ fov }) => (camera.FieldOfView = fov));
		this.onk(["brightness"], ({ brightness }) => (effect.Brightness = brightness));
		this.onk(["contrast"], ({ contrast }) => (effect.Contrast = contrast));
		this.onk(["saturation"], ({ saturation }) => (effect.Saturation = saturation));
		this.onk(["tint"], ({ tint }) => (effect.TintColor = tint));

		const ticker = new Component();
		ticker.onDisable(() => disable());
		ticker.event.subscribe(RunService.RenderStepped, () => (camera.CFrame = target.CFrame));
		this.onDisable(() => ticker.disable());
		this.onDescendantDestroyed(() => ticker.destroy());

		this.event.readonlyObservableFromInstanceParam(Workspace, "CurrentCamera").subscribe((wcamera) => {
			const thisCamera = wcamera === camera;

			ticker.setEnabled(thisCamera);
			effect.Enabled = thisCamera;
		});

		const fovCache = this.initializeInputCache("fov");
		const controllableCache = this.initializeInputCache("controllable");

		let cameraSub: SignalConnection | undefined = undefined;
		this.onDisable(() => cameraSub?.Disconnect());

		const disable = () => {
			cameraSub?.Disconnect();
			cameraModeSub?.Disconnect();
			isFirstPerson.set(false);

			if (controllableCache.tryGet()) {
				defaultCamera!.CameraSubject = Players.LocalPlayer.Character?.FindFirstChild("Humanoid") as
					| Humanoid
					| undefined;
			} else {
				enabledCameras.delete(this);
				Workspace.CurrentCamera =
					(Objects.firstKey(enabledCameras)?.instance.FindFirstChild("Camera") as Camera | undefined) ??
					defaultCamera;
			}
			defaultCamera.FieldOfView = playerData.config.get().betterCamera.fov ?? 70;
		};

		let cameraModeSub: SignalConnection | undefined;
		this.onDisable(() => cameraModeSub?.Disconnect());

		const localTargetPos = this.instance.GetPivot().PointToObjectSpace(target.Position);
		const isFirstPerson = new ObservableValue<boolean>(false);
		isFirstPerson.subscribe((fp) => {
			if (fp) {
				target.Position = this.instance
					.GetPivot()
					.PointToWorldSpace(localTargetPos.add(new Vector3(0, 0.6, 0)));
			} else {
				target.Position = this.instance.GetPivot().PointToWorldSpace(localTargetPos);
			}
		});

		this.onk(["enabled"], ({ enabled }) => {
			if (enabled) {
				if (controllableCache.tryGet()) {
					cameraSub = defaultCamera?.GetPropertyChangedSignal("CameraSubject").Connect(() => {
						const subject = defaultCamera!.CameraSubject;
						if (subject?.Name !== "Target") {
							defaultCamera!.CameraSubject = target;
						}
					});
					cameraModeSub = this.event.loop(0, () =>
						isFirstPerson.set(UserInputService.MouseBehavior === Enum.MouseBehavior.LockCenter),
					);
					defaultCamera!.FieldOfView = fovCache.tryGet() ?? definition.input.fov.types.number.config;
					defaultCamera!.CameraSubject = target;
				} else {
					enabledCameras.add(this);
					Workspace.CurrentCamera = camera;
					camera.FieldOfView = fovCache.tryGet() ?? definition.input.fov.types.number.config;
				}
			} else {
				disable();
			}

			Logic.events.update.send({ block: this.instance, enabled });
			Logic.update(this.instance, enabled);
		});

		this.onDisable(disable);
	}

	static update(block: BlockModel, enabled: boolean) {
		const led = block.FindFirstChild("LED") as BasePart | undefined;
		if (!led) return;

		led.Color = enabled ? Colors.green : Colors.red;
	}
}

export const CameraBlock = {
	...BlockCreation.defaults,
	id: "camera",
	displayName: "Camera",
	description: "Does camera things",

	logic: { definition, ctor: Logic },
	mirror: {
		behaviour: "offset180",
	},
} as const satisfies BlockBuilder;


import { Players } from "@rbxts/services";
import { TextChatService } from "@rbxts/services";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicArgs, BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder, BlockCategoryPath, BlockModelSource } from "shared/blocks/Block";

const autoModel = (prefab: BlockCreation.Model.PrefabName, text: string, category: BlockCategoryPath) => {
	return {
		model: BlockCreation.Model.fAutoCreated(prefab, text),
		category: () => category,
	} satisfies BlockModelSource;
};
const definition = {
	input: {},
	output: {
		playerMessage: {
			displayName: "Message",
			types: ["string"],
		},
		player: {
			displayName: "Player",
			types: ["string"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as KeySensorBlockLogic };
class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs) {
		super(definition, block);

		// TextLabels have "ContentText" which shows EXACTLY the text being rendered without any of the RichText tags such as "<b>"
		// By using ContentText we can filter out RichText tags and ONLY get the actual message the player sent
		const stupid = new Instance("TextLabel");
		stupid.RichText = true;

		TextChatService.MessageReceived.Connect((msg) => {
			const id = msg.TextSource?.UserId;
			if (!id) return;
			const plr = Players.GetPlayerByUserId(id);
			if (!plr) return;

			// filter out undesirable chars from richtext chat
			// this is a warning to those who will try AND FAIL
			// to extract text with no rich text in it
			// BE WARNED
			// lua is a pile of garbage and you CAN'T have nice things here

			// WHY THE JZXHIGZFAGFUIAWGDAjk DOES THIS WORK AIUGHFYIAWGFIGWARGAIWRGTOIl YOU ROBLOX
			stupid.Text = msg.Text;

			// the regex doesn't want to work
			// const [noTags, _1] = msg.Text.gsub("<[^>]*>", "");
			// const [noTransformedTags, _] = noTags.gsub("%$lt;[^/%$]+[^%$]*%$gt;", "");
			this.output.player.set("string", plr.Name);
			this.output.playerMessage.set("string", stupid.ContentText);
		});
	}
}

export const ChatSensorBlock = {
	...BlockCreation.defaults,
	id: "chatsensor",
	displayName: "Chat Sensor",
	description: "Returns the most recent message, and the player who sent it.",
	search: { partialAliases: ["speak", "message", "command", "cmd"] },
	modelSource: autoModel("DoubleGenericLogicBlockPrefab", "CHAT SENSOR", BlockCreation.Categories.sensor),

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { A2SRemoteEvent, S2CRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { InstanceBlockLogic as InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {
		disconnect: {
			displayName: "Disconnect",
			types: {
				bool: {
					config: false,
					control: {
						config: {
							enabled: true,
							key: "F",
							reversed: false,
							switch: false,
						},
						canBeReversed: false,
						canBeSwitch: false,
					},
				},
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

export type disconnectorBlock = BlockModel & {
	BottomPart: Part;
	TopPart: Part;
};

export type { Logic as DisconnectBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition, disconnectorBlock> {
	static readonly events = {
		disconnect: new A2SRemoteEvent<{ readonly block: disconnectorBlock }>("b_disconnectblock_disconnect"),
		disconnect2c: new S2CRemoteEvent<{ readonly block: disconnectorBlock }>("b_disconnectblock_disconnect2c"),
	} as const;

	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		this.onk(["disconnect"], ({ disconnect }) => {
			if (!disconnect) return;

			const newPart = (parent: BasePart) => {
				const part = new Instance("Part");
				part.Name = "deleted";
				part.CFrame = parent.CFrame;
				part.AssemblyLinearVelocity = parent.AssemblyLinearVelocity;
				part.AssemblyAngularVelocity = parent.AssemblyAngularVelocity;
				part.Size = Vector3.zero;
				part.RootPriority = 127;
				part.Parent = parent;

				const weld = new Instance("WeldConstraint");
				weld.Part0 = part;
				weld.Part1 = parent;
				weld.Parent = part;
			};
			newPart(block.instance.FindFirstChild("BottomPart") as BasePart);
			newPart(block.instance.FindFirstChild("TopPart") as BasePart);

			Logic.events.disconnect.send({ block: this.instance });
			this.disable();
		});
	}

	static disconnect(block: BlockModel) {
		(block.FindFirstChild("Ejector") as Part | undefined)?.Destroy();
	}
}

if (RunService.IsClient()) {
	Logic.events.disconnect2c.invoked.Connect(({ block }) => {
		block.FindFirstChild("BottomPart")?.FindFirstChild("deleted")?.Destroy();
		block.FindFirstChild("TopPart")?.FindFirstChild("deleted")?.Destroy();
	});
}

export const DisconnectBlock = {
	...BlockCreation.defaults,
	id: "disconnectblock",
	displayName: "Disconnector",
	description: "Detaches connected parts",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { ReplicatedStorage } from "@rbxts/services";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicArgs, BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const vLuau = require(ReplicatedStorage.Modules.vLuau) as {
	luau_execute: (code: string, env: unknown) => LuaTuple<[start: () => void, close: () => void]>;
};

const baseEnv = { ...math };
delete (baseEnv as Partial<typeof baseEnv>).randomseed;

const safeEnv = setmetatable(
	{},
	{
		__index: baseEnv as never,
		__newindex: (_, key, value) => {
			if (baseEnv[key as never] !== undefined) {
				error("Attempt to overwrite protected key: " + tostring(key), 2);
			}
			rawset(baseEnv, key, value);
		},
	},
);

const inputVars = ["a", "b", "c", "d", "e", "f", "g", "h"];
const definition = {
	inputOrder: ["expression", "input1", "input2", "input3", "input4", "input5", "input6", "input7", "input8"],
	input: {
		expression: {
			displayName: "Expression",
			tooltip: "The expression in string format",
			types: {
				string: {
					config: "a + (b - c)",
				},
			},
		},

		input1: {
			displayName: inputVars[0],
			types: {
				number: { config: 0 },
			},
		},
		input2: {
			displayName: inputVars[1],
			types: {
				number: { config: 1 },
			},
		},
		input3: {
			displayName: inputVars[2],
			types: {
				number: { config: 2 },
			},
		},
		input4: {
			displayName: inputVars[3],
			types: {
				number: { config: 3 },
			},
		},
		input5: {
			displayName: inputVars[4],
			types: {
				number: { config: 4 },
			},
		},
		input6: {
			displayName: inputVars[5],
			types: {
				number: { config: 5 },
			},
		},
		input7: {
			displayName: inputVars[6],
			types: {
				number: { config: 6 },
			},
		},
		input8: {
			displayName: inputVars[7],
			types: {
				number: { config: 7 },
			},
		},
	},
	output: {
		result: {
			displayName: "Result",
			types: ["number"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs) {
		super(definition, block);

		let func:
			| ((a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number)
			| undefined;
		this.onkFirstInputs(["expression"], ({ expression }) => {
			if (expression.trim().contains("\n")) {
				this.disableAndBurn();
				return;
			}

			expression = `
				return function(${inputVars.join(", ")})
					return ${expression}
				end
			`;

			try {
				const [bytecode] = vLuau.luau_execute(expression, safeEnv);
				func = bytecode() as unknown as typeof func;
			} catch (err) {
				this.disableAndBurn();
				return;
			}
		});

		this.onRecalcInputs(({ input1, input2, input3, input4, input5, input6, input7, input8 }) => {
			if (!func) return;

			const res = func(input1, input2, input3, input4, input5, input6, input7, input8);
			this.output.result.set("number", res);
		});
	}
}

export const FunctionBlock = {
	...BlockCreation.defaults,
	id: "functionblock",
	displayName: "Function Block",
	description: "Solves the given expression using the provided variables.",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { GameDefinitions } from "shared/data/GameDefinitions";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {},
	output: {
		result: {
			displayName: "Global Position",
			unit: "Coordinates",
			types: ["vector3"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as GPSSensorBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition> {
	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		const offset = new Vector3(0, -GameDefinitions.HEIGHT_OFFSET, 0);
		this.event.subscribe(RunService.PostSimulation, () => {
			this.output.result.set("vector3", offset.add(block.instance.GetPivot().Position));
		});
	}
}

export const GPSSensorBlock = {
	...BlockCreation.defaults,
	id: "gpssensor",
	displayName: "GPS",
	description: "Returns its global position",

	logic: { definition, ctor: Logic },
	modelSource: {
		model: BlockCreation.Model.fAutoCreated("DoubleGenericLogicBlockPrefab", "GPS"),
		category: () => BlockCreation.Categories.sensor,
	},
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { Physics } from "shared/Physics";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {},
	output: {
		result: {
			displayName: "Acceleration (stud/s²)",
			types: ["number"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as GravitySensorBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition> {
	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		this.event.subscribe(RunService.PostSimulation, () => {
			this.output.result.set(
				"number",
				Physics.GetGravityOnHeight(Physics.LocalHeight.fromGlobal(this.instance.GetPivot().Y)),
			);
		});
	}
}

export const GravitySensorBlock = {
	...BlockCreation.defaults,
	id: "gravitysensor",
	displayName: "Gravity Sensor",
	description: "Returns the current gravity acceleration in stud/s²",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { BlockManager } from "shared/building/BlockManager";
import { GameEnvironment } from "shared/data/GameEnvironment";
import { Physics } from "shared/Physics";
import type { BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {
		density: {
			displayName: "Density",
			unit: "m/V",
			types: {
				number: {
					config: 0.17,
					clamp: {
						showAsSlider: true,
						min: 0.05,
						max: 2,
						step: 0.01,
					},
				},
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

type HeliumModel = BlockModel & {
	readonly Part: BasePart & {
		readonly VectorForce: VectorForce;
	};
};

export type { Logic as HeliumBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition, HeliumModel> {
	private readonly part;
	private readonly vectorForce;
	private airDensityConstant = 1.2 / GameEnvironment.EarthAirDensity;

	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		this.part = this.instance.Part;
		this.vectorForce = this.part.VectorForce;

		const blockScale = BlockManager.manager.scale.get(this.instance) ?? Vector3.one;
		const scale = blockScale.X * blockScale.Y * blockScale.Z;

		const densityCache = this.initializeInputCache("density");
		const f = () => {
			const density = densityCache.tryGet();
			if (!density) return;

			//tick works when block works
			const height = Physics.LocalHeight.fromGlobal(this.part.GetPivot().Y);
			const counterforce = Physics.GetGravityOnHeight(height) * this.part.Mass;
			this.vectorForce.Force = new Vector3(
				0,
				((Physics.GetAirDensityOnHeight(height) * this.airDensityConstant) / density) * counterforce * scale,
				0,
			);
		};

		this.event.subscribe(RunService.PostSimulation, f);
	}
}

export const HeliumBlock = {
	...BlockCreation.defaults,
	id: "heliumblock",
	displayName: "Helium",
	description: "I still have no idea how did they manage to pump helium into soap",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { BlockManager } from "shared/building/BlockManager";
import { Physics } from "shared/Physics";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuildersWithoutIdAndDefaults, BlockLogicInfo } from "shared/blocks/Block";
import type { SoundEffect } from "shared/effects/SoundEffect";

const definition = {
	inputOrder: ["thrust", "strength"],
	input: {
		thrust: {
			displayName: "Thrust",
			unit: "Percentage",
			types: {
				number: {
					config: 0,
					clamp: {
						showAsSlider: false,
						min: 0,
						max: 100,
					},
					control: {
						config: {
							enabled: true,
							startValue: 0,
							mode: {
								type: "smooth",
								instant: {
									mode: "onRelease",
								},
								smooth: {
									speed: 20,
									mode: "stopOnRelease",
								},
							},
							keys: [
								{ key: "W", value: 100 },
								{ key: "S", value: 0 },
							],
						},
					},
				},
			},
		},
		strength: {
			displayName: "Strength",
			unit: "Percentage",
			types: {
				number: {
					config: 100,
					clamp: {
						showAsSlider: true,
						max: 100,
						min: 0,
					},
				},
			},
		},
	},
	output: {
		maxpower: {
			displayName: "Force",
			unit: "Rowtons",
			types: ["number"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type JetModel = BlockModel & {
	readonly TurbineShaft: MeshPart & {
		readonly Working: Sound;
		readonly Idle: Sound;
		readonly Start: Sound;
		readonly Shut: Sound;
		readonly HingeConstraint: HingeConstraint;
	};
	readonly TurbineBody: (UnionOperation | Part | MeshPart) & {
		readonly VectorForce: VectorForce;
		readonly BladeLocation: Attachment;
	};
	readonly ColBox: Part;
};

export type { Logic as JetBlockLogic };

@injectable
class Logic extends InstanceBlockLogic<typeof definition, JetModel> {
	// Instances
	private readonly vectorForce;

	// Math
	private readonly basePower = 30_000;
	private readonly maxPower;

	constructor(
		block: InstanceBlockLogicArgs,
		@inject private readonly soundEffect: SoundEffect,
	) {
		super(definition, block);

		// const
		const maxSoundVolume = 0.5;

		// vals
		const thrust = this.initializeInputCache("thrust");

		// Instances
		const colbox = this.instance.ColBox;
		const shaft = this.instance.TurbineShaft;
		const body = this.instance.TurbineBody;
		// const hinge = shaft.HingeConstraint;

		this.vectorForce = body.VectorForce;

		// Sounds
		const wSound = shaft.Working;
		const iSound = shaft.Idle;
		const stSound = shaft.Start;
		const shSound = shaft.Shut;
		const soundStageArray = [stSound, shSound];

		// Math
		let multiplier = (colbox.Size.X * colbox.Size.Y * colbox.Size.Z) / 8;

		// The strength depends on the material
		const material = BlockManager.manager.material.get(this.instance);
		multiplier *= math.max(1, new PhysicalProperties(material).Density / 2);

		// Max power
		this.maxPower = this.basePower * multiplier;
		this.output.maxpower.set("number", this.maxPower);

		let playing: Sound = iSound;
		const stopOtherSoundAndPlayNewOne = (sound: Sound) => {
			if (playing === sound) return;

			this.soundEffect.send(this.instance.PrimaryPart!, {
				sound: playing,
				isPlaying: false,
				volume: playing.Volume,
			});

			this.soundEffect.send(this.instance.PrimaryPart!, {
				sound: sound,
				isPlaying: true,
				volume: playing.Volume,
			});

			playing = sound;
		};

		const magicThreshold = 0.2;
		const updateSound = (volume: number, currentThrust: number, previousThrust: number) => {
			const changed = currentThrust !== previousThrust;
			// update volume
			for (const s of soundStageArray) s.Volume = volume;

			if (!changed) return;
			this.soundEffect.send(this.instance.PrimaryPart!, {
				sound: iSound,
				isPlaying: true,
				volume: volume,
			});

			if (currentThrust > previousThrust) return stopOtherSoundAndPlayNewOne(stSound);
			return stopOtherSoundAndPlayNewOne(shSound);
		};

		const updateForce = (modifier: number) => {
			const gravModifier = Physics.GetAirDensityModifierOnHeight(
				Physics.LocalHeight.fromGlobal(this.instance.GetPivot().Y),
			);

			this.vectorForce.Force = new Vector3(
				this.maxPower * modifier * math.clamp(gravModifier - magicThreshold, 0, 1),
			);
		};

		let lastThrust = 0;
		let thrustPercent = 0;
		let strengthPercent = 0;
		this.onAlwaysInputs(({ thrust, strength }) => {
			//nan check
			if (typeIs(thrust, "number") && thrust !== thrust) return;

			//the code
			thrustPercent = thrust / 100;
			strengthPercent = strength / 100;

			updateForce(thrustPercent * strengthPercent);
			updateSound(thrustPercent * maxSoundVolume, thrust, lastThrust);

			lastThrust = thrust;
		});

		let rotationAccumulator = 0;
		this.event.subscribe(RunService.RenderStepped, (dt) => {
			rotationAccumulator += math.deg((thrust.get() ?? 0) * dt) % 360;
			const orn = body.BladeLocation.Orientation;
			body.BladeLocation.Orientation = new Vector3(orn.X, orn.Y, rotationAccumulator);
		});

		this.onDisable(() => {
			updateForce(0);
			updateSound(0, 0, 0);
			playing.Stop();
		});
	}
}

const search = { partialAliases: ["turbine", "engine", "military", "civil", "engine", "afterburner"] };
const logic: BlockLogicInfo = { definition, ctor: Logic };
const list: BlockBuildersWithoutIdAndDefaults = {
	jetenginecivil: {
		displayName: "Civil Jet Engine",
		description: "Engines your jet or whatever",
		logic,
		limit: 50,
		search,
	},
	jetenginemilitaryold: {
		displayName: "Military Jet Engine (Old Model)",
		description: "Long live military jet engine! ",
		logic,
		limit: 50,
		search,
	},
	jetenginemilitary: {
		displayName: "Military Jet Engine",
		description: "Engines your jet or whatever (military grade)",
		logic,
		limit: 50,
		search,
	},
};
export const JetEngineBlocks = BlockCreation.arrayFromObject(list);


import { ContextActionService, RunService } from "@rbxts/services";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { BlockManager } from "shared/building/BlockManager";
import type { BlockLogicArgs, BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	outputOrder: ["text", "name", "id", "pressed"],
	input: {},
	output: {
		text: {
			displayName: "Key text",
			types: ["string"],
		},
		name: {
			displayName: "Key name",
			types: ["string"],
		},
		id: {
			displayName: "Key id",
			types: ["number"],
		},
		pressed: {
			displayName: "Pressed",
			types: ["bool"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as KeySensorBlockLogic };
class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs) {
		super(definition, block);

		if (!RunService.IsClient()) return;

		const keyToStr = (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 === "KeypadZero") return "0";
			if (key === "KeypadOne") return "1";
			if (key === "KeypadTwo") return "2";
			if (key === "KeypadThree") return "3";
			if (key === "KeypadFour") return "4";
			if (key === "KeypadFive") return "5";
			if (key === "KeypadSix") return "6";
			if (key === "KeypadSeven") return "7";
			if (key === "KeypadEight") return "8";
			if (key === "KeypadNine") return "9";

			return key;
		};

		const actname = "kbl_" + BlockManager.manager.uuid.get(block.instance!);
		ContextActionService.BindActionAtPriority(
			actname,
			(_, state, input) => {
				if (input.UserInputType !== Enum.UserInputType.Keyboard) {
					return;
				}

				let pressed;
				if (state === Enum.UserInputState.Begin) {
					pressed = true;
				} else if (state === Enum.UserInputState.End) {
					pressed = false;
				} else {
					return Enum.ContextActionResult.Pass;
				}

				this.output.pressed.set("bool", pressed);
				this.output.text.set("string", keyToStr(input.KeyCode.Name));
				this.output.id.set("number", input.KeyCode.Value);
				this.output.name.set("string", input.KeyCode.Name);

				return Enum.ContextActionResult.Pass;
			},
			false,
			123456798,
			Enum.UserInputType.Keyboard,
		);

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

export const KeyboardBlock = {
	...BlockCreation.defaults,
	id: "keyboardsensor",
	displayName: "Keyboard Sensor",
	description: "Returns a pressed (or released) keyboard key",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { Workspace } from "@rbxts/services";
import { InstanceBlockLogic as InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

// the max distance the raycast can travel
const absoluteMaxDistance = 36000;

// the number of instances to make to cover the full distance
let beamInstanceCount = math.ceil(absoluteMaxDistance / 2048);
const rayMaxBounces = beamInstanceCount;

// tag for if a part reflects the laser
// DO NOT CHANGE
const mirrorTag = "Mirror_Reflective";

// if plots doesn't exist then theres a much bigger issue
const workspacePlots = Workspace.WaitForChild("Plots");

beamInstanceCount = math.max(beamInstanceCount, rayMaxBounces);
const definition = {
	input: {
		alwaysEnabled: {
			displayName: "Laser always enabled",
			types: {
				bool: {
					config: false,
				},
			},
		},
		maxDistance: {
			displayName: "Max distance",
			types: {
				number: {
					config: 2048,
					clamp: {
						showAsSlider: true,
						min: 0.1,
						max: absoluteMaxDistance,
					},
				},
			},
		},
		rayTransparency: {
			displayName: "Transparency",
			types: {
				number: {
					config: 0.9,
					clamp: {
						showAsSlider: true,
						min: 0,
						max: 1,
					},
				},
			},
		},
		rayColor: {
			displayName: "Ray color",
			types: {
				color: {
					config: Color3.fromRGB(255, 255, 255),
				},
			},
		},
		dotColor: {
			displayName: "Dot color",
			types: {
				color: {
					config: Color3.fromRGB(255, 255, 255),
				},
			},
			connectorHidden: true,
		},
		enableReflections: {
			displayName: "Enable Reflections",
			tooltip: "If reflections of the laser should be enabled",
			types: {
				bool: {
					config: true, // on by default
				},
			},
			connectorHidden: true,
		},
	},
	output: {
		distance: {
			displayName: "Distance",
			types: ["number"],
		},
		targetColor: {
			displayName: "Target Color",
			types: ["vector3"],
			tooltip: "Black color (0, 0, 0) by default and if nothing found",
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type LaserModel = BlockModel & {
	Ray: BasePart;
	Dot: BasePart;
};

// returns if a block can reflect a laser
function isReflective(block: Instance): boolean {
	const part = block as Part;

	// must be a placed part
	if (!block.IsDescendantOf(workspacePlots)) return false;

	if (block.HasTag(mirrorTag)) return true;

	// glass material
	return part.Material === Enum.Material.Glass; // && (part.Transparency <= 0.35 || part.Transparency === 0.3);
}

function reflect(incomingVector: Vector3, normalVector: Vector3) {
	return incomingVector.sub(normalVector.mul(2 * incomingVector.Dot(normalVector)));
}

export class LaserBlockLogic extends InstanceBlockLogic<typeof definition, LaserModel> {
	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		const dotSize = 0.3;

		const ray = this.instance.Ray;
		ray.Transparency = 0.5;
		const dot = this.instance.Dot;
		const rayBeams: BasePart[] = [ray];
		dot.Size = Vector3.one.mul(dotSize);

		let nextBeam = 0;

		// it was getting to cluttered
		const laserFolder = new Instance("Folder");
		laserFolder.Name = "laserFolder";
		laserFolder.Parent = this.instance;

		/*
		// laser normal debug
		const db = new Instance("Part");
		db.Size = new Vector3(0.5, 0.5, 2);
		db.CanCollide = false;
		db.CanQuery = false;
		db.CanTouch = false;
		db.Transparency = 0.5;

		function moveDisplay(disp: Part, pos: Vector3, normal: Vector3) {
			disp.CFrame = new CFrame(pos, pos.add(normal)).add(normal.mul(disp.Size.Z / 2));
			disp.Parent = laserFolder;
		}

		const db_normals: Part[] = [];
		for (let i=0; i<30; i++) {
			db_normals.push(db.Clone());
		}*/

		// makes a beam between 2 positions using the rayBeams
		function createBeamBetween(origin: Vector3, target: Vector3) {
			const totalDist = origin.sub(target).Magnitude;
			const direction = target.sub(origin).Unit;

			for (let i = 0; i < totalDist; i += 2048) {
				if (rayBeams.size() <= nextBeam) return;

				const thisDist = math.min(2048, totalDist - i);
				const ray = rayBeams[nextBeam++];
				const position = origin.add(direction.mul(i + thisDist / 2));

				ray.Size = new Vector3(thisDist, 0.1, 0.1);
				ray.CFrame = CFrame.lookAlong(position, direction).mul(CFrame.Angles(0, math.rad(90), 0));
				if (ray.Parent !== laserFolder) {
					ray.Parent = laserFolder;
				}
			}
		}

		// amount of instances needed to cover the absolute max distance minus original ray
		for (let i = 1; i <= beamInstanceCount; i++) {
			const rayClone = ray.Clone();
			rayClone.Name += i;
			rayClone.CanCollide = false;
			rayClone.CanQuery = false;
			rayBeams.push(rayClone);
		}

		this.onDisable(() => {
			for (const ray of rayBeams) {
				ray.Destroy();
			}
		});

		this.onk(["rayColor"], ({ rayColor }) => {
			for (const r of rayBeams) {
				r.Color = rayColor;
			}
		});
		this.onk(["dotColor"], ({ dotColor }) => {
			dot.Color = dotColor;
		});

		this.onAlwaysInputs(({ maxDistance, alwaysEnabled, rayTransparency, enableReflections }) => {
			const thisPivot = this.instance.GetPivot();
			const raycastOrigin = thisPivot.Position;
			const raycastDirection = thisPivot.UpVector;

			let newOrigin = raycastOrigin;
			let newDirection = raycastDirection;
			const newParams = new RaycastParams();
			newParams.FilterDescendantsInstances = [this.instance];
			newParams.FilterType = Enum.RaycastFilterType.Exclude;

			let availDistance = math.min(maxDistance, absoluteMaxDistance);
			let totalDistance = 0;
			let lastResult: RaycastResult | undefined = undefined;

			const cachedBeams: Array<[Vector3, Vector3]> = [];

			let laserBounces = 0;
			nextBeam = 0;
			while (availDistance > 0) {
				const rayDir = newDirection.mul(availDistance);
				const raycastResult = Workspace.Raycast(newOrigin.add(newDirection.mul(0.001)), rayDir, newParams);
				if (raycastResult) {
					const ray_hit = raycastResult.Position;
					const ray_block = raycastResult.Instance;
					const lightVector = ray_hit.sub(newOrigin).Unit;
					const reflected = reflect(lightVector, raycastResult.Normal);

					const distance = newOrigin.sub(ray_hit).Magnitude;

					// [debug] display bounces
					// moveDisplay(db_normals[laserBounces], ray_hit, reflected);

					// store beams for later
					cachedBeams.push([newOrigin, ray_hit]);

					lastResult = raycastResult;
					totalDistance += distance;

					// detect if should continue casting (if it reflects)
					if (enableReflections && isReflective(ray_block)) {
						// set new origin & direction
						newOrigin = ray_hit;
						newDirection = reflected;
						if (laserBounces === 0) {
							// clear original exclude on first bounce
							newParams.FilterDescendantsInstances = [];
						}
						availDistance -= distance;
					} else {
						break;
					}
				} else {
					// ray did not hit
					const endpos = newOrigin.add(rayDir);
					if (availDistance > 0 && (laserBounces !== 0 || alwaysEnabled)) {
						// create beam still with max dist
						cachedBeams.push([newOrigin, endpos]);
					}
					newOrigin = endpos;
					lastResult = undefined;
					break;
				}
				laserBounces++;
				if (laserBounces >= rayMaxBounces) {
					totalDistance = -1;
					break;
				}
			}

			// create the actual beams
			for (const [origin, endpos] of cachedBeams) {
				createBeamBetween(origin, endpos);
			}

			// remove parent of any unused beams
			const beamCount = rayBeams.size();
			for (let i = nextBeam; i < beamCount; i++) {
				const beam = rayBeams[i];
				if (beam.Parent !== undefined) beam.Parent = undefined;
			}

			const endpos = lastResult?.Position || newOrigin;

			const color = lastResult?.Instance.Color;
			this.output.targetColor.set(
				"vector3",
				color ? new Vector3(color.R, color.G, color.B).mul(255) : Vector3.zero,
			);

			this.output.distance.set("number", lastResult?.Distance ? totalDistance : -1);

			if (lastResult?.Distance !== undefined || alwaysEnabled) {
				for (const r of rayBeams) {
					r.Transparency = rayTransparency;
				}
				dot.Transparency = rayTransparency;
				dot.CFrame = CFrame.lookAlong(endpos, newDirection);
			} else {
				for (const r of rayBeams) {
					r.Transparency = 1;
				}
				dot.Transparency = 1;
			}
		});
	}
}

export const LaserBlock = {
	...BlockCreation.defaults,
	id: "laser",
	displayName: "Laser pointer",
	description: "shoot beem boom target!",
	logic: { definition, ctor: LaserBlockLogic },
	search: { partialAliases: ["sensor", "beam"] },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { A2SRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { InstanceBlockLogic as InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: ["posx", "posy", "color", "update", "reset", "suspendDraw"],
	input: {
		posx: {
			displayName: "Position X",
			types: {
				number: {
					config: 0,
					clamp: {
						showAsSlider: true,
						min: 0,
						max: 7,
						step: 1,
					},
				},
			},
			configHidden: true,
		},
		posy: {
			displayName: "Position Y",
			types: {
				number: {
					config: 0,
					clamp: {
						showAsSlider: true,
						min: 0,
						max: 7,
						step: 1,
					},
				},
			},
			configHidden: true,
		},
		color: {
			displayName: "Color",
			types: {
				vector3: {
					config: new Vector3(0, 0, 0),
				},
				color: {
					config: new Color3(0, 0, 0),
				},
			},
			configHidden: true,
		},
		update: {
			displayName: "Update",
			types: {
				bool: {
					config: false,
				},
			},
			configHidden: true,
		},
		reset: {
			displayName: "Reset",
			types: {
				bool: {
					config: false,
				},
			},
			configHidden: true,
		},
		suspendDraw: {
			displayName: "Suspend drawing",
			tooltip: "If true, buffer pixel changes, and when this input is false, draw them all at once",
			types: {
				bool: {
					config: false,
				},
			},
			configHidden: true,
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

// using array to save space when sending network events
type cachedChange = [frame: Frame, color: Color3];

export type { Logic as LedDisplayBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition> {
	static readonly events = {
		prepare: new A2SRemoteEvent<{
			readonly block: BlockModel;
			readonly baseColor: Color3;
		}>("leddisplay_prepare", "RemoteEvent"), // TODO: fix this shit crap
		update: new A2SRemoteEvent<{
			readonly block: BlockModel;
			readonly changes: readonly cachedChange[];
		}>("leddisplay_update", "RemoteEvent"),
		fill: new A2SRemoteEvent<{
			readonly block: BlockModel;
			readonly color: Color3;
			readonly frames: Frame[][];
		}>("leddisplay_fill", "RemoteEvent"),
	} as const;

	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		let cachedChanges = new Map<Frame, cachedChange>();
		let suspendBuffer = new Map<Frame, cachedChange>();

		const baseColor = this.definition.input.color.types.color.config;

		Logic.events.prepare.send({ block: block.instance, baseColor: baseColor });
		const gui = block.instance.WaitForChild("Screen").WaitForChild("SurfaceGui");

		const display: Frame[][] = new Array(8);
		for (let x = 0; x < 8; x++) {
			display[x] = new Array(8);
			for (let y = 0; y < 8; y++) {
				display[x][y] = gui.WaitForChild(`x${x}y${y}`) as Frame;
			}
		}

		this.event.subscribe(RunService.Heartbeat, () => {
			if (cachedChanges.isEmpty()) return;

			Logic.events.update.send({
				block: block.instance,
				changes: cachedChanges.values(),
			});
			cachedChanges.clear();
		});

		this.on(({ posx, posy, color, update, suspendDraw }) => {
			if (!update) return;

			if (typeIs(color, "Vector3")) {
				color = Color3.fromRGB(color.X, color.Y, color.Z);
			}

			const target = suspendDraw ? suspendBuffer : cachedChanges;

			const frame = display[posx][posy];
			target.set(frame, [frame, color]);
		});

		this.onk(["suspendDraw"], ({ suspendDraw }) => {
			if (suspendDraw) return;
			if (suspendBuffer.isEmpty()) return;

			cachedChanges = suspendBuffer;
			suspendBuffer = new Map();
		});

		this.onk(["reset"], ({ reset }) => {
			if (!reset) return;

			Logic.events.fill.send({
				block: block.instance,
				color: baseColor,
				frames: display,
			});
		});
	}
}

export const LedDisplayBlock = {
	...BlockCreation.defaults,
	id: "leddisplay",
	displayName: "Display",
	description: "Simple 8x8 pixel display. Wonder what can you do with it..",
	limit: 36,

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { ReplicatedStorage } from "@rbxts/services";
import { Colors } from "engine/shared/Colors";
import { Objects } from "engine/shared/fixes/Objects";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { LogControl } from "client/gui/static/LogControl";
import type { BlockLogicArgs, BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { BlockLogicTypes } from "shared/blockLogic/BlockLogicTypes";
import type { BlockBuilder } from "shared/blocks/Block";

const vLuau = require(ReplicatedStorage.Modules.vLuau) as {
	luau_execute: (code: string, env: unknown) => LuaTuple<[start: () => void, close: () => void]>;
};

const definitionPart = {
	types: {
		number: { config: 0 },
		bool: { config: false },
		string: { config: "" },
		vector3: { config: Vector3.zero },
		color: { config: new Color3(0, 0, 0) },
	},
	configHidden: true,
};

const definition = {
	inputOrder: ["code", "input1", "input2", "input3", "input4", "input5", "input6", "input7", "input8"],
	outputOrder: ["output1", "output2", "output3", "output4", "output5", "output6", "output7", "output8"],
	input: {
		code: {
			displayName: "Code",
			types: {
				code: {
					config: `-- Read your inputs using "getInput(index)"
-- Write values to outputs using "setOutput(index, value)"
-- You are limited to 8 kilobytes of code. If you're short, use minifers.

onTick(function(deltaTime, tick)
    -- The code here is executed every tick.
    -- deltaTime shows how much time has elapsed since the previous tick. tick shows the current tick number.
    -- Remember that it makes no sense to change the same output several times here.

    -- Key Sensor -> Screen example
    local keyPressed = getInput(1) -- Key sensor
    if keyPressed then
        setOutput(1, "Key pressed") -- Screen
    else
        setOutput(1, "Key is not pressed") -- Screen
    end
end)

print("Hello, OverEngineered!")`,
					lengthLimit: 8192,
				},
			},
			tooltip: "Lua code to run.",
			connectorHidden: true,
		},
		input1: {
			displayName: "Input 1",
			...definitionPart,
		},
		input2: {
			displayName: "Input 2",
			...definitionPart,
		},
		input3: {
			displayName: "Input 3",
			...definitionPart,
		},
		input4: {
			displayName: "Input 4",
			...definitionPart,
		},
		input5: {
			displayName: "Input 5",
			...definitionPart,
		},
		input6: {
			displayName: "Input 6",
			...definitionPart,
		},
		input7: {
			displayName: "Input 7",
			...definitionPart,
		},
		input8: {
			displayName: "Input 8",
			...definitionPart,
		},
	},
	output: {
		output1: {
			displayName: "Output 1",
			types: Objects.keys(definitionPart.types),
		},
		output2: {
			displayName: "Output 2",
			types: Objects.keys(definitionPart.types),
		},
		output3: {
			displayName: "Output 3",
			types: Objects.keys(definitionPart.types),
		},
		output4: {
			displayName: "Output 4",
			types: Objects.keys(definitionPart.types),
		},
		output5: {
			displayName: "Output 5",
			types: Objects.keys(definitionPart.types),
		},
		output6: {
			displayName: "Output 6",
			types: Objects.keys(definitionPart.types),
		},
		output7: {
			displayName: "Output 7",
			types: Objects.keys(definitionPart.types),
		},
		output8: {
			displayName: "Output 8",
			types: Objects.keys(definitionPart.types),
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as LuaCircuitBlockLogic };
@injectable
class Logic extends BlockLogic<typeof definition> {
	private close: () => void = undefined!;
	private greenLED: BasePart = undefined!;
	private redLED: BasePart = undefined!;

	constructor(block: BlockLogicArgs, @tryInject logControl?: LogControl) {
		super(definition, block);

		this.greenLED = block.instance?.FindFirstChild("GreenLED") as BasePart;
		this.redLED = block.instance?.FindFirstChild("RedLED") as BasePart;

		this.greenLED.Material = Enum.Material.Neon;
		this.greenLED.Color = Colors.green;

		const inputCaches = {
			[1]: this.initializeInputCache("input1"),
			[2]: this.initializeInputCache("input2"),
			[3]: this.initializeInputCache("input3"),
			[4]: this.initializeInputCache("input4"),
			[5]: this.initializeInputCache("input5"),
			[6]: this.initializeInputCache("input6"),
			[7]: this.initializeInputCache("input7"),
			[8]: this.initializeInputCache("input8"),
		};

		const showErr = (err: unknown) => {
			log(`Runtime error: ${tostring(err)}`, "error");
			blinkRedLEDLoop();
		};

		const log = function (text: string, level: "info" | "warn" | "error"): void {
			if (level === "warn") {
				warn("[Lua Circuit]", text);
				logControl?.addLine(text, Colors.yellow);
			} else if (level === "error") {
				warn("[Lua Circuit]", text);
				logControl?.addLine(text, Colors.red);
			} else {
				print("[Lua Circuit]", text);
				logControl?.addLine(text);
			}
		};

		const tasklib = {
			wait: (duration?: number) => {
				const start = time();
				duration = math.max(duration ?? 0, 0);

				const endTime = start + duration;
				let current = start;

				while (endTime >= current) {
					coroutine.yield();
					current = time();
				}

				return current - start;
			},
			waitTicks: (duration?: number) => {
				const start = time();
				duration = math.max(duration ?? 1, 1);

				for (let i = 0; i < duration; i++) {
					coroutine.yield();
				}

				return time() - start;
			},
		};

		const validOutputTypes: { readonly [k in string]?: keyof BlockLogicTypes.Primitives } = {
			number: "number",
			Vector3: "vector3",
			Color3: "color",
			string: "string",
			boolean: "bool",
		};

		const baseEnv = {
			print: (...args: unknown[]) => {
				for (let i = 0; i < args.size(); i++) {
					args[i] ??= "nil";
				}

				log((args as defined[]).join(" "), "info");
			},
			warn: (...args: unknown[]) => {
				for (let i = 0; i < args.size(); i++) {
					args[i] ??= "nil";
				}

				log((args as defined[]).join(" "), "warn");
			},
			error: (...args: unknown[]) => {
				for (let i = 0; i < args.size(); i++) {
					args[i] ??= "nil";
				}

				log((args as defined[]).join(" "), "error");
			},
			task: tasklib,
			table,
			assert: (condition: boolean, message?: string | undefined) => assert(condition, message),
			pcall,
			xpcall,
			tostring,
			tonumber,
			pairs,
			ipairs,
			type,
			typeof: (obj: unknown) => typeOf(obj),
			math,
			string,
			bit32,
			Vector2,
			Vector3,
			CFrame,
			Color3,
			DateTime,
			time,
			buffer,
			utf8,
			next,
			select,

			onTick: (func: (dt: number, tick: number) => void): void => {
				this.onTicc((ctx) => {
					try {
						const c = coroutine.create(() => func(ctx.dt, ctx.tick));
						const [success, data] = coroutine.resume(c);
						if (!success) throw data;

						coroutines.push(c);
					} catch (err) {
						showErr(err);
						this.close();
					}
				});
			},

			getInput: (input: number): string | number | boolean | Vector3 | Color3 | undefined => {
				if (input < 1 || input > 8) {
					error("Output index must be between 1 and 8", 2);
				}

				return inputCaches[input as 1].tryGet();
			},
			setOutput: (output: number, value: unknown): void => {
				if (output < 1 || output > 8) {
					error("Output index must be between 1 and 8", 2);
				}

				const retType = validOutputTypes[typeOf(value)];
				if (!retType) {
					error(`Invalid object type ${typeOf(value)}`, 2);
				}

				this.output[`output${output}` as "output1"].set(retType as never, value as never);
			},
		};

		const safeEnv = setmetatable(
			{},
			{
				__index: baseEnv as never,
				__newindex: (_, key, value) => {
					if (baseEnv[key as never] !== undefined) {
						error("Attempt to overwrite protected key: " + tostring(key), 2);
					}
					rawset(baseEnv, key, value);
				},
			},
		);

		const blinkRedLEDLoop = () => {
			this.event.loop(0.1, () => {
				this.redLED.Color = this.redLED.Color === Colors.red ? new Color3(91, 93, 105) : Colors.red;
				this.redLED.Material =
					this.redLED.Material === Enum.Material.Neon ? Enum.Material.SmoothPlastic : Enum.Material.Neon;
			});
		};

		const coroutines: thread[] = [];
		const removedCoroutines: thread[] = [];
		this.onTicc((ctx) => {
			for (const t of coroutines) {
				if (coroutine.status(t) === "dead") {
					removedCoroutines.push(t);
					continue;
				}

				const [success, data] = coroutine.resume(t, ctx.dt, ctx.tick);
				if (!success) {
					showErr(data);
				}
			}
			for (const removed of removedCoroutines) {
				coroutines.remove(coroutines.indexOf(removed));
			}
			removedCoroutines.clear();
		});

		this.onkFirstInputs(["code"], ({ code }) => {
			let bytecode: () => void;

			try {
				[bytecode, this.close] = vLuau.luau_execute(code, safeEnv);
			} catch (err) {
				log(`Compilation error: ${tostring(err)}`, "error");
				blinkRedLEDLoop();

				return;
			}

			try {
				coroutines.push(coroutine.create(bytecode));
			} catch (err) {
				showErr(err);
			}
		});
	}
}

export const LuaCircuitBlock = {
	...BlockCreation.defaults,
	id: "luacircuit",
	displayName: "Lua Circuit",
	description: "Allows you to run Lua code to program your buildings. If the code is too large, use a minifier.",
	limit: 1,
	requiredFeatures: ["lua_circuit"],

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { BlockManager } from "shared/building/BlockManager";
import { PartUtils } from "shared/utils/PartUtils";
import type { BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: ["strength", "invertPolarity", "distance"],
	input: {
		strength: {
			displayName: "Strength",
			types: {
				number: {
					config: 100,
					clamp: {
						showAsSlider: true,
						min: 0,
						max: 100,
					},
				},
			},
		},
		invertPolarity: {
			displayName: "Invert polarity",
			types: {
				bool: {
					config: false,
					control: {
						config: {
							enabled: true,
							key: "G",
							switch: false,
							reversed: false,
						},
						canBeSwitch: true,
						canBeReversed: true,
					},
				},
			},
		},
		distance: {
			displayName: "Distance multiplier",
			types: {
				number: { config: 1 },
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

const magicNumber = 60 * 1; // dt
const magnets: Logic[] = [];
const forcesApplied: Map<Logic, Vector3> = new Map<Logic, Vector3>();

const calculateForce = (block1: Logic, block2: Logic, distMult: number): Vector3 | undefined => {
	const pos1 = block1.part.Position;
	const pos2 = block2.part.Position;
	const difference = pos1.sub(pos2);
	const distance = difference.Magnitude / distMult;
	if (distance > 10) return;

	const invSqrt = 1 / (1 + math.sqrt(distance));
	const isAttracted = block1.polarity === block2.polarity;
	const result = difference.Unit.mul(isAttracted ? invSqrt : -invSqrt);

	//distance between
	//inverse square root
	//make it minus value if
	//multiply by normalized value?
	//???
	//profit!!

	return result;
};

RunService.PostSimulation.Connect((dt) => {
	if (magnets.size() === 0) return;

	forcesApplied.clear();

	// Getting AssemblyRootPart is slow and it's happening several times per magnet, so we cache it
	const assemblies = magnets.mapToMap((m) => $tuple(m, m.part.AssemblyRootPart));

	for (let i = 0; i < magnets.size(); i++) {
		const magneti = magnets[i];
		const strength1 = magneti.getStrength() * magicNumber * dt;
		if (strength1 === 0) continue;

		for (let j = i + 1; j < magnets.size(); j++) {
			const magnetj = magnets[j];
			if (assemblies.get(magneti) === assemblies.get(magnetj)) {
				continue;
			}

			const calculatedForce = calculateForce(
				magneti,
				magnetj,
				magneti.distanceMultiplier * magnetj.distanceMultiplier,
			);
			if (!calculatedForce) continue;

			const strength2 = magnetj.getStrength() * magicNumber * dt;
			if (strength2 === 0) continue;

			const appliedForce = calculatedForce.mul(strength1).add(calculatedForce.mul(strength2));
			forcesApplied.set(magneti, (forcesApplied.get(magneti) ?? Vector3.zero).add(appliedForce));
			forcesApplied.set(magnetj, (forcesApplied.get(magnetj) ?? Vector3.zero).add(appliedForce.mul(-1)));
		}

		const finalForce = forcesApplied.get(magneti);
		if (finalForce) {
			magneti.part.ApplyImpulseAtPosition(finalForce, magneti.part.Position);
		}
	}
});

const partColor1 = Color3.fromRGB(128, 128, 240);
const partColor2 = Color3.fromRGB(240, 128, 128);

export type { Logic as MagnetBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition> {
	polarity = false;
	readonly part;
	private strength = 0;

	readonly scale: number;
	distanceMultiplier: number = 1;

	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);
		this.part = this.instance.WaitForChild("Part") as BasePart;

		const blockScale = BlockManager.manager.scale.get(block.instance) ?? Vector3.one;
		this.scale = blockScale.X * blockScale.Y * blockScale.Z;

		this.onk(["strength"], ({ strength }) => (this.strength = strength));
		this.onk(["distance"], ({ distance }) => (this.distanceMultiplier = distance));
		this.onk(["invertPolarity"], ({ invertPolarity }) => {
			this.polarity = invertPolarity;
			PartUtils.applyToAllDescendantsOfType(
				"BasePart",
				this.instance,
				(part) => (part.Color = invertPolarity ? partColor1 : partColor2),
			);
		});

		this.onEnable(() => magnets.push(this));
		forcesApplied.set(this, Vector3.zero);
	}

	getStrength(): number {
		return this.strength * this.scale;
	}

	destroy() {
		magnets.remove(magnets.indexOf(this));
		forcesApplied.delete(this);
		super.destroy();
	}
}

export const MagnetBlock = {
	...BlockCreation.defaults,
	id: "magnet",
	displayName: "Magnet",
	description: "Block that attracts to different polarity magnets, repels from same. Only for your blocks, though",

	search: {
		partialAliases: ["attractor"],
	},
	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { t } from "engine/shared/t";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockSynchronizer } from "shared/blockLogic/BlockSynchronizer";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { BlockManager } from "shared/building/BlockManager";
import { RemoteEvents } from "shared/RemoteEvents";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: ["rotationSpeed", "clutch_release", "max_torque", "cframe"],
	input: {
		rotationSpeed: {
			displayName: "Angular Speed",
			unit: "radians/second",
			types: {
				number: {
					config: 0,
					clamp: {
						showAsSlider: false,
						min: -150,
						max: 150,
					},
					control: {
						config: {
							enabled: true,
							startValue: 0,
							mode: {
								type: "instant",
								instant: {
									mode: "onRelease",
								},
								smooth: {
									speed: 60,
									mode: "stopOnRelease",
								},
							},
							keys: [
								{ key: "R", value: 15 },
								{ key: "F", value: -15 },
							],
						},
					},
				},
			},
		},
		clutch_release: {
			displayName: "Clutch release",
			types: {
				bool: {
					config: false,
					control: {
						config: {
							enabled: false,
							key: "Y",
							switch: false,
							reversed: false,
						},
						canBeSwitch: true,
						canBeReversed: true,
					},
				},
			},
		},
		max_torque: {
			displayName: "Max Torque",
			tooltip:
				"The maximum torque that Motor can apply when trying to reach its desired Angular Speed. Does not affect the operation of infinite torque version.",
			unit: "RMU stud²/s²",
			types: {
				number: {
					config: 200,
					clamp: {
						max: 600,
						min: 0,
						showAsSlider: true,
					},
				},
			},
		},
		cframe: {
			displayName: "Infinite torque",
			tooltip: "May break something, use with caution.",
			types: {
				bool: {
					config: false,
				},
			},
			connectorHidden: true,
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

type MotorBlock = BlockModel & {
	readonly Base: Part & {
		readonly HingeConstraint: HingeConstraint;
		readonly Weld: Weld;
	};
	readonly Attach: Part;
};

const updateEventType = t.interface({
	block: t.instance("Model").nominal("blockModel").as<MotorBlock>(),
	rotationSpeed: t.number,
	currentCFrame: t.cframe,
});
type CFrameUpdateData = t.Infer<typeof updateEventType>;

const cframe_update_signals: Map<Weld, RBXScriptConnection> = new Map(); // TODO: possibly fix this shit somehow
const cframe_update = ({ block, rotationSpeed, currentCFrame }: CFrameUpdateData) => {
	if (!block.Base.Weld.Enabled) {
		block.Base.Weld.Enabled = true;

		const blockScale = BlockManager.manager.scale.get(block) ?? Vector3.one;
		const rotationWeld = block.Base.Weld;

		rotationWeld.Enabled = true;
		block.Base.HingeConstraint.Enabled = false;

		rotationWeld.C1 = new CFrame(
			new Vector3(-(block.Base.CFrame.ToObjectSpace(block.Attach.CFrame).Position.X * blockScale.Y), 0, 0),
		);
	}

	const weld = block.Base.Weld;
	weld.C0 = currentCFrame;
	cframe_update_signals.get(weld)?.Disconnect();
	cframe_update_signals.delete(weld);

	if (rotationSpeed !== 0) {
		cframe_update_signals.set(
			weld,
			RunService.Heartbeat.Connect((deltaTime) => {
				if (!weld) {
					cframe_update_signals.get(weld)?.Disconnect();
					cframe_update_signals.delete(weld);
				}

				weld.C0 = weld.C0.mul(CFrame.Angles(-rotationSpeed * deltaTime, 0, 0));
			}),
		);
	}
};

const events = {
	cframe_update: new BlockSynchronizer("motor_cframe_update", updateEventType, cframe_update),
} as const;

export type { Logic as MotorBlockLogic };
export class Logic extends InstanceBlockLogic<typeof definition, MotorBlock> {
	private readonly hingeConstraint;
	private readonly rotationWeld;

	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		this.hingeConstraint = this.instance.Base.HingeConstraint;
		this.rotationWeld = this.instance.Base.Weld;

		const blockScale = BlockManager.manager.scale.get(this.instance) ?? Vector3.one;
		const scale = blockScale.X * blockScale.Y * blockScale.Z;

		this.onk(["rotationSpeed"], ({ rotationSpeed }) => {
			if (this.rotationWeld.Enabled) {
				events.cframe_update.send({
					rotationSpeed,
					currentCFrame: this.rotationWeld.C0,
					block: this.instance,
				} as CFrameUpdateData);
			} else {
				this.hingeConstraint.AngularVelocity = rotationSpeed;
			}
		});

		this.onk(["max_torque"], ({ max_torque }) => {
			if (this.rotationWeld.Enabled) {
				return;
			}

			this.hingeConstraint.MotorMaxTorque = max_torque * 1_000_000 * math.max(1, scale);
		});

		this.onk(["cframe"], ({ cframe }) => {
			if (cframe) {
				events.cframe_update.send({
					rotationSpeed: 0,
					currentCFrame: this.rotationWeld.C0,
					block: this.instance,
				} as CFrameUpdateData);
			}

			// Security check to prevent issues
			if (!cframe) {
				this.onTicc(() => {
					const base = this.instance.FindFirstChild("Base") as BasePart | undefined;
					const attach = this.instance.FindFirstChild("Attach") as BasePart | undefined;
					if (!attach || !base) {
						this.disableAndBurn();
						return;
					}

					if (attach.Position.sub(base.Position).Magnitude > 3 * blockScale.Y) {
						RemoteEvents.ImpactBreak.send([base]);

						this.disable();
					}
				});
			}
		});

		this.onk(["clutch_release"], ({ clutch_release }) => {
			if (this.rotationWeld.Enabled && clutch_release) {
				this.disableAndBurn();
			}

			this.hingeConstraint.ActuatorType = Enum.ActuatorType[clutch_release ? "None" : "Motor"];
		});

		this.onDisable(() => {
			if (this.instance.FindFirstChild("Base")?.FindFirstChild("HingeConstraint")) {
				this.hingeConstraint.AngularVelocity = 0;
			}
		});
	}
}

export const MotorBlock = {
	...BlockCreation.defaults,
	id: "motorblock",
	displayName: "Motor",
	description: "Rotates attached blocks. For unpowered rotation, use the Hinge block.",

	logic: { definition, ctor: Logic, events },
} as const satisfies BlockBuilder;


import { Players, RunService, UserInputService, Workspace } from "@rbxts/services";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { GameDefinitions } from "shared/data/GameDefinitions";
import type { BlockLogicArgs, BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	outputOrder: ["position", "angle", "direction", "angle3d", "position3d"],
	input: {},
	output: {
		position: {
			displayName: "Position",
			unit: "Vector2 0-1",
			types: ["vector3"],
		},
		angle: {
			displayName: "Angle around the center",
			unit: "Degrees",
			types: ["number"],
		},
		direction: {
			displayName: "3D Direction",
			unit: "Vector3 unit",
			types: ["vector3"],
		},
		angle3d: {
			displayName: "3D Angle of direction",
			unit: "Radians",
			types: ["vector3"],
		},
		position3d: {
			displayName: "3D Position",
			unit: "Vector3 Global position",
			types: ["vector3"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as MouseSensorBlockLogic };
class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs) {
		super(definition, block);

		let wheel = 0;

		if (RunService.IsClient()) {
			this.event.subscribe(UserInputService.InputChanged, (input) => {
				if (input.UserInputType === Enum.UserInputType.MouseWheel) {
					wheel = input.Position.Z;
				}
			});
		}

		this.event.subscribe(RunService.PostSimulation, () => {
			const mousePos = UserInputService.GetMouseLocation();
			const relaPos = mousePos.div(Workspace.CurrentCamera!.ViewportSize);

			this.output.position.set("vector3", new Vector3(relaPos.X, relaPos.Y, wheel));
			wheel = 0;

			this.output.angle.set("number", math.deg(math.atan2(-(relaPos.Y - 0.5), relaPos.X - 0.5)));

			const camera = Workspace.CurrentCamera;
			if (camera) {
				const ray = camera.ViewportPointToRay(mousePos.X, mousePos.Y);
				const [x, y, z] = CFrame.lookAt(Vector3.zero, ray.Direction).ToOrientation();

				this.output.direction.set("vector3", ray.Direction);
				this.output.angle3d.set("vector3", new Vector3(x, y, z));
				this.output.position3d.set(
					"vector3",
					Players.LocalPlayer.GetMouse()!.Hit.Position.sub(new Vector3(0, GameDefinitions.HEIGHT_OFFSET, 0)),
				);
			}
		});
	}
}

export const MouseSensorBlock = {
	...BlockCreation.defaults,
	id: "mousesensor",
	displayName: "Mouse Sensor",
	description: "Returns some data about the mouse cursor",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { RunService, Workspace } from "@rbxts/services";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { GameDefinitions } from "shared/data/GameDefinitions";
import type { BlockLogicArgs, BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	outputOrder: ["position", "direction", "up"],
	input: {},
	output: {
		position: {
			displayName: "Position",
			types: ["vector3"],
		},
		direction: {
			displayName: "Direction",
			types: ["vector3"],
		},
		up: {
			displayName: "Up vector",
			types: ["vector3"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as OwnerCameraLocatorBlockLogic };
class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs) {
		super(definition, block);

		this.event.subscribe(RunService.PostSimulation, () => {
			const camera = Workspace.CurrentCamera;
			if (!camera) return;

			this.output.position.set(
				"vector3",
				camera.CFrame.Position.sub(new Vector3(0, GameDefinitions.HEIGHT_OFFSET, 0)),
			);
			this.output.direction.set("vector3", camera.CFrame.LookVector);
			this.output.up.set("vector3", camera.CFrame.UpVector);
		});
	}
}

export const OwnerCameraLocatorBlock = {
	...BlockCreation.defaults,
	id: "ownercameralocator",
	displayName: "Owner Camera Locator",
	description: "Returns owner camera position and direction",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { Players, RunService } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	outputOrder: ["linear", "angular"],
	input: {},
	output: {
		linear: {
			displayName: "Offset",
			unit: "Studs, relative",
			types: ["vector3"],
		},
		angular: {
			displayName: "Angular offset",
			unit: "Radians, relative",
			types: ["vector3"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as OwnerLocatorBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition> {
	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block);

		this.event.subscribe(RunService.PostSimulation, () => {
			if (!this.instance.PrimaryPart) return;

			const owner = Players.LocalPlayer;
			const playerPart = owner.Character?.FindFirstChild("HumanoidRootPart") as Part | undefined;
			if (!playerPart) return;

			const localPosition = this.instance
				.GetPivot()
				.mul(CFrame.Angles(-math.pi / 2, 0, math.pi / 2))
				.PointToObjectSpace(playerPart.Position);

			const xa = Vector3.yAxis.Angle(localPosition.mul(new Vector3(0, 1, 1)), Vector3.xAxis);
			const ya = Vector3.zAxis.Angle(localPosition.mul(new Vector3(1, 0, 1)), Vector3.yAxis);
			const za = Vector3.xAxis.Angle(localPosition.mul(new Vector3(1, 1, 0)), Vector3.zAxis);

			this.output.angular.set("vector3", new Vector3(math.deg(xa), math.deg(ya), math.deg(za)));

			this.output.linear.set("vector3", localPosition);
		});
	}
}

export const OwnerLocatorBlock = {
	...BlockCreation.defaults,
	id: "ownerlocator",
	displayName: "Owner Locator",
	description: "IT WILL FIND YOU",

	logic: { definition, ctor: Logic },
} as const satisfies BlockBuilder;


import { Players } from "@rbxts/services";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import type { BlockLogicArgs, BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {},
	output: {
		ping: {
			displayName: "Ping",
			types: ["number"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as PingSensorLogic };
class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs) {
		super(definition, block);

		this.onTicc(() => {
			this.output.ping.set("number", Players.LocalPlayer.GetNetworkPing() * 2);
		});
	}
}

export const PingSensor = {
	...BlockCreation.defaults,
	id: "pingsensor",
	displayName: "Ping",
	description: "Outputs your latency",

	logic: { definition, ctor: Logic },
	modelSource: {
		model: BlockCreation.Model.fAutoCreated("ConstLogicBlockPrefab", "PING"),
		category: () => BlockCreation.Categories.other,
	},
} as const satisfies BlockBuilder;