import { Workspace } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { BlockManager } from "shared/building/BlockManager";
import { Colors } from "shared/Colors";
import { Sound } from "shared/Sound";
import { VectorUtils } from "shared/utils/VectorUtils";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";
import type { ParticleEffect } from "shared/effects/ParticleEffect";
import type { SoundEffect } from "shared/effects/SoundEffect";

const definition = {
	inputOrder: ["direction", "trailLength", "trailColor"],
	input: {
		direction: {
			displayName: "Direction",
			tooltip:
				"Each vector axis represents the direction and force of the each engine. Each axis is clamped between -100 and 100.",
			unit: "Vector3 unit",
			types: {
				vector3: {
					config: Vector3.zero,
				},
			},
		},
		trailLength: {
			displayName: "Trail length",
			tooltip: "The length of the burst trail.",
			types: {
				number: {
					config: 1,
					clamp: {
						showAsSlider: true,
						min: 1,
						max: 5,
					},
				},
			},
		},
		trailColor: {
			displayName: "Trail color",
			types: {
				color: {
					config: Color3.fromRGB(255, 255, 255),
				},
			},
			connectorHidden: true,
		},
	},
	output: {
		maxpower: {
			displayName: "Max Power (Newtons)",
			tooltip: "A constant. Shows how much force each engine can output.",
			types: ["number"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type Emitter = Part & {
	readonly Fire: ParticleEmitter;
};
type Engine = Instance & {
	readonly VectorForce: VectorForce;
	readonly Sound: Sound;
};
type RCSEngineModel = BlockModel & {
	readonly Engine1Emitter: Emitter;
	readonly Engine2Emitter: Emitter;
	readonly Engine3Emitter: Emitter;
	readonly Engine4Emitter: Emitter;
	readonly Engine5Emitter: Emitter;
	readonly Engine1: Engine;
	readonly Engine2: Engine;
	readonly Engine3: Engine;
	readonly Engine4: Engine;
	readonly Engine5: Engine;
	readonly ColBox: Part;
};

type singleEngineConfiguration = {
	readonly engine: Engine;
	readonly particleEmitter: Emitter;
	soundEmitter: Sound;
	vectorForce: VectorForce;
};

export type { Logic as RCSEngineBlockLogic };

@injectable
class Logic extends InstanceBlockLogic<typeof definition, RCSEngineModel> {
	// Instances
	private readonly engineData: readonly singleEngineConfiguration[] = [
		{
			engine: this.instance.Engine1,
			particleEmitter: this.instance.Engine1Emitter,
			soundEmitter: undefined!,
			vectorForce: undefined!,
		},
		{
			engine: this.instance.Engine2,
			particleEmitter: this.instance.Engine2Emitter,
			soundEmitter: undefined!,
			vectorForce: undefined!,
		},
		{
			engine: this.instance.Engine3,
			particleEmitter: this.instance.Engine3Emitter,
			soundEmitter: undefined!,
			vectorForce: undefined!,
		},
		{
			engine: this.instance.Engine4,
			particleEmitter: this.instance.Engine4Emitter,
			soundEmitter: undefined!,
			vectorForce: undefined!,
		},
		{
			engine: this.instance.Engine5,
			particleEmitter: this.instance.Engine5Emitter,
			soundEmitter: undefined!,
			vectorForce: undefined!,
		},
	];

	// Math
	private readonly basePower = 500;
	private readonly maxPower;

	// Const
	private readonly maxSoundVolume = 0.25;
	private readonly maxParticlesAcceleration = 120;

	private thrust: Vector3 = Vector3.zero;

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

		for (const d of this.engineData) {
			d.vectorForce = d.engine.VectorForce;
			d.soundEmitter = d.engine.Sound;
		}

		// The strength depends on the material

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

		const material = BlockManager.manager.material.get(this.instance);
		const multiplier = math.max(1, math.round(new PhysicalProperties(material).Density / 2)) * scale;

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

		const trailColorCache = this.initializeInputCache("trailColor");

		const setEngineThrust = (engine: singleEngineConfiguration, thrustPercentage: number) => {
			if (!engine.particleEmitter.Fire) return;
			// Force
			engine.vectorForce.Force = new Vector3(this.maxPower * thrustPercentage);

			// Particles
			const visualize = thrustPercentage !== 0;
			const newParticleEmitterAcceleration = engine.particleEmitter
				.GetPivot()
				.RightVector.mul(this.maxParticlesAcceleration * thrustPercentage);

			const particleEmmiterHasDifference =
				engine.particleEmitter.Fire.Enabled !== visualize ||
				engine.particleEmitter.Fire.Acceleration.sub(newParticleEmitterAcceleration).Abs().Magnitude > 1;

			engine.particleEmitter.Fire.Enabled = visualize;
			engine.particleEmitter.Fire.Acceleration = newParticleEmitterAcceleration;

			// Sound
			const newVolume =
				Sound.getWorldVolume(this.instance.GetPivot().Y) *
				(this.maxSoundVolume * math.abs(thrustPercentage)) *
				math.sqrt(scale);

			const volumeHasDifference =
				visualize !== engine.soundEmitter.Playing || math.abs(engine.soundEmitter.Volume - newVolume) > 0.005;
			engine.soundEmitter.Playing = visualize;
			engine.soundEmitter.Volume = newVolume;

			if (volumeHasDifference) {
				this.soundEffect.send(this.instance.PrimaryPart!, {
					sound: engine.soundEmitter,
					isPlaying: engine.soundEmitter.Playing,
					volume: engine.soundEmitter.Volume / 2,
				});
			}
			if (particleEmmiterHasDifference) {
				const trailColor = trailColorCache.tryGet();

				this.particleEffect.send(this.instance.PrimaryPart!, {
					particle: engine.particleEmitter?.Fire,
					isEnabled: engine.particleEmitter?.Fire.Enabled,
					acceleration: engine.particleEmitter?.Fire.Acceleration,
					color: trailColor ?? Colors.white,
				});
			}
		};

		const update = () => {
			if (!this.isEnabled()) return;
			const thrustPercent = VectorUtils.apply(this.thrust, (v) => math.clamp(v, -100, 100) / 100);
			setEngineThrust(this.engineData[0], -math.max(thrustPercent.Y, 0));

			setEngineThrust(this.engineData[1], -math.abs(math.max(thrustPercent.X, 0)));
			setEngineThrust(this.engineData[2], -math.abs(math.min(thrustPercent.X, 0)));

			setEngineThrust(this.engineData[4], -math.abs(math.max(thrustPercent.Z, 0)));
			setEngineThrust(this.engineData[3], -math.abs(math.min(thrustPercent.Z, 0)));
		};

		this.event.subscribe(Workspace.GetPropertyChangedSignal("Gravity"), update);
		this.onk(["direction"], ({ direction }) => {
			//nan check
			if (typeIs(direction.X, "number") && direction.X !== direction.X) return;
			if (typeIs(direction.Y, "number") && direction.Y !== direction.Y) return;
			if (typeIs(direction.Z, "number") && direction.Z !== direction.Z) return;

			//the code
			this.thrust = direction;
			update();
		});

		this.onk(["trailLength"], ({ trailLength }) => {
			const val = new NumberRange(trailLength * 0.15);
			for (const engine of this.engineData) {
				engine.particleEmitter.Fire.Lifetime = val;
			}
		});

		this.onk(["trailColor"], ({ trailColor }) => {
			const val = new ColorSequence(trailColor);
			for (const engine of this.engineData) {
				engine.particleEmitter.Fire.Color = val;
			}
		});

		this.onEnable(() => {
			const scale = math.sqrt(BlockManager.manager.scale.get(this.instance)?.findMin() ?? 1);
			for (const emitter of this.engineData.map((d) => d.particleEmitter.Fire)) {
				emitter.Size = new NumberSequence(
					emitter.Size.Keypoints.map((k) => new NumberSequenceKeypoint(k.Time, k.Value * scale, k.Envelope)),
				);

				this.particleEffect.send(this.instance.PrimaryPart!, {
					particle: emitter,
					isEnabled: false,
					acceleration: emitter.Acceleration,
					scale,
				});
			}
		});

		this.onDisable(() => update());
	}
}

export const RCSEngineBlock = {
	...BlockCreation.defaults,
	id: "rcsengine",
	displayName: "RCS Engine",
	description: "Small rockets used to reorient a spacecraft, input vector correlates to each axis",
	limit: 50,
	mirror: {
		behaviour: "offset180",
	},
	search: {
		aliases: ["rcs"],
	},

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


import { Players, RunService } from "@rbxts/services";
import { t } from "engine/shared/t";
import { InstanceBlockLogic as InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockSynchronizer } from "shared/blockLogic/BlockSynchronizer";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { SharedPlots } from "shared/building/SharedPlots";
import { CustomRemotes } from "shared/Remotes";
import { TagUtils } from "shared/utils/TagUtils";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: ["maxDistance", "detectionSize", "visibility", "detectSelf", "relativePositioning", "minimalDistance"],
	input: {
		maxDistance: {
			displayName: "Max Distance",
			tooltip: "Returns coordinates relative to this block instead of relative to global grid",
			unit: "Studs",
			types: {
				number: {
					config: 100,
					clamp: {
						showAsSlider: true,
						min: 1,
						max: 2048,
					},
				},
			},
		},
		detectionSize: {
			displayName: "Detection Area Size",
			types: {
				number: {
					config: 1,
					clamp: {
						showAsSlider: true,
						min: 1,
						max: 5,
					},
				},
			},
		},
		visibility: {
			displayName: "Detection Area Visibility",
			types: {
				bool: {
					config: false,
				},
			},
		},
		detectSelf: {
			displayName: "Detect Self",
			types: {
				bool: {
					config: true,
				},
			},
			connectorHidden: true,
		},
		relativePositioning: {
			displayName: "Object-Relative Output",
			types: {
				bool: {
					config: true,
				},
			},
		},
		minimalDistance: {
			displayName: "Minimal Detection Distance",
			types: {
				number: {
					config: 5,
					clamp: {
						showAsSlider: true,
						min: 0,
						max: 2048,
					},
				},
			},
		},
	},
	output: {
		distance: {
			displayName: "Offset",
			types: ["vector3"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

const definitionRadial = {
	...definition,
	input: {
		...definition.input,
		maxDistance: {
			...definition.input.maxDistance,
			types: {
				number: {
					...definition.input.maxDistance.types.number,
					clamp: {
						...definition.input.maxDistance.types.number.clamp,
						max: definition.input.maxDistance.types.number.clamp.max * 0.5,
					},
				},
			},
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type radarBlock = BlockModel & {
	RadarView: BasePart | UnionOperation | MeshPart;
};

if (RunService.IsClient()) {
	const p = Players.LocalPlayer;
	CustomRemotes.modes.set.sent.Connect(({ mode }) => {
		if (mode === "ride") {
			const blocks = SharedPlots.instance.getPlotComponentByOwnerID(p.UserId).getBlocks();

			for (const b of blocks) {
				if (!b.PrimaryPart) continue;
				ownDetectablesSet.add(b.PrimaryPart);
			}
			return;
		}

		ownDetectablesSet.clear();
	});
}

const updateEventType = t.interface({
	block: t.instance("Model").nominal("blockModel").as<radarBlock>(),
	size: t.vector3,
	offset: t.cframe,
	position: t.vector3,
});

type UpdateData = t.Infer<typeof updateEventType>;

const events = {
	updateSize: new BlockSynchronizer(
		"b_radar_size_update",
		updateEventType, //useless comment just to calm down the text formatter
		({ block, size, offset, position }: UpdateData) => {
			block.RadarView.Size = size;
			block.RadarView.Position = position;
			block.RadarView.PivotOffset = offset;
		},
	),
} as const;

const ownDetectablesSet = new Set<BasePart>();

export type { Logic as RadarSectionBlockLogic };
@injectable
class Logic extends InstanceBlockLogic<typeof definition, radarBlock> {
	constructor(
		def: typeof definition,
		block: InstanceBlockLogicArgs,
		sizeAndOffsetCalculator: (
			view: MeshPart | UnionOperation | BasePart,
			detectionSize: number,
			maxDistance: number,
		) => void,
	) {
		super(def, block);

		let updateTask: thread;
		let minDistance = 0;
		const view = this.instance.FindFirstChild("RadarView") as MeshPart | UnionOperation | BasePart;
		const metalPlate = this.instance.FindFirstChild("MetalBase") as BasePart;

		this.onk(["visibility"], ({ visibility }) => (view.Transparency = visibility ? 0.8 : 1));
		this.onk(["relativePositioning"], ({ relativePositioning }) => (this.isRelativePosition = relativePositioning));
		this.onk(["detectionSize", "maxDistance"], ({ detectionSize, maxDistance }) => {
			const pivo = metalPlate.GetPivot();
			view.Position = pivo.PointToWorldSpace(Vector3.xAxis.mul(maxDistance / 2 + 0.5));
			view.Size = new Vector3(view.Size.X, maxDistance, view.Size.Z);

			sizeAndOffsetCalculator(view, detectionSize, maxDistance);
			this.triggerDistanceListUpdate = true;
		});

		this.onAlwaysInputs(({ minimalDistance }) => (minDistance = minimalDistance));

		const selfDetect = this.initializeInputCache("detectSelf");
		this.event.subscribe(view.Touched, (part) => {
			//just to NOT detect radar view things
			// probably pointless check
			// since it detects only colboxes anyway
			// ...or does it?
			// Hey, VSausage. Michael here.
			// Can the radar detect non-colboxes?
			if (part.HasTag(TagUtils.allTags.SPECIAL_RADARVIEW)) return;

			//just to NOT detect own blocks
			if (!selfDetect.get() && ownDetectablesSet.has(part)) return;

			if (!minDistance) return;

			this.allTouchedBlocks.add(part);
			this.triggerDistanceListUpdate = true;
		});

		this.event.subscribe(view.TouchEnded, (part) => {
			this.allTouchedBlocks.delete(part);
			if (this.triggerDistanceListUpdate) return;
			this.triggerDistanceListUpdate = part === this.closestDetectedPart;
		});

		const setView = () => {
			view.AssemblyLinearVelocity = Vector3.zero;
			view.AssemblyAngularVelocity = Vector3.zero;
			view.PivotTo(this.instance.PrimaryPart!.CFrame);
		};

		// For actual contacts
		this.event.subscribe(RunService.Stepped, () => {
			if (this.closestDetectedPart?.Parent === undefined || this.triggerDistanceListUpdate) {
				this.triggerDistanceListUpdate = false;

				this.closestDetectedPart = this.findClosestPart(minDistance);

				if (updateTask) task.cancel(updateTask);
				updateTask = task.delay(5, () => (this.triggerDistanceListUpdate = true));
			}
			this.output.distance.set(
				"vector3",
				this.closestDetectedPart ? this.getDistanceTo(this.closestDetectedPart) : Vector3.zero,
			);
			setView();
		});
		// For rendering (so people don't think it lags)
		this.event.subscribe(RunService.PreRender, setView);

		this.onDisable(() => {
			if (view) view.Transparency = 1;
			this.allTouchedBlocks.clear();
		});
	}

	private isRelativePosition = false;
	private triggerDistanceListUpdate: boolean = false;
	private closestDetectedPart: BasePart | undefined = undefined;
	private readonly allTouchedBlocks: Set<BasePart> = new Set<BasePart>();

	private getDistanceTo = (part: BasePart) => {
		if (this.instance === undefined) return Vector3.zero;
		if (part === undefined) return Vector3.zero;
		if (this.isRelativePosition) return this.instance.GetPivot().ToObjectSpace(part.GetPivot()).Position;
		return part.GetPivot().Position.sub(this.instance.GetPivot().Position);
	};

	private findClosestPart(minDist: number) {
		let smallestDistance: Vector3 | undefined;
		let closestPart: BasePart | undefined;

		for (const bp of this.allTouchedBlocks) {
			const d = this.getDistanceTo(bp);

			if (d.Magnitude < minDist) continue;
			if (smallestDistance === undefined) {
				[smallestDistance, closestPart] = [d, bp];
				continue;
			}

			if (d.Magnitude > smallestDistance.Magnitude) continue;
			[smallestDistance, closestPart] = [d, bp];
		}
		return closestPart;
	}
}

class RadialRadarLogic extends Logic {
	constructor(block: InstanceBlockLogicArgs) {
		const zeroOffset = new CFrame(0, 0, 0);
		super(definitionRadial, block, (view, detectionSize, maxDistance) => {
			events.updateSize.send({
				block: this.instance,
				size: new Vector3(maxDistance, maxDistance, maxDistance),
				offset: zeroOffset,
				position: this.instance.GetPivot().Position,
			});
		});
	}
}

class RadarSectionLogic extends Logic {
	constructor(block: InstanceBlockLogicArgs) {
		super(definition, block, (view, detectionSize, maxDistance) => {
			const halvedMaxDist = maxDistance / 2;
			const ds = detectionSize * (detectionSize - math.sqrt(halvedMaxDist / (maxDistance + halvedMaxDist))) * 10;
			events.updateSize.send({
				block: this.instance,
				size: new Vector3(ds, view.Size.Y, ds),
				offset: new CFrame(0, -view.Size.Y / 2 - 0.4, 0),
				position: view.Position,
			});
		});
	}
}

export const RadarBlocks = [
	{
		...BlockCreation.defaults,
		id: "radarsection",
		displayName: "Radar Section",
		description: "Returns the position of the closest object in it's field of view",

		logic: { definition, ctor: RadarSectionLogic },
	},
	{
		...BlockCreation.defaults,
		id: "radialradar",
		displayName: "Radial Radar",
		description: "Returns the position of the closest object in it's spherical field of view",

		logic: { definition: definitionRadial, ctor: RadialRadarLogic },
	},
] as const satisfies BlockBuilder[];


import { Players, RunService } from "@rbxts/services";
import { InstanceBlockLogic as InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { SharedPlots } from "shared/building/SharedPlots";
import { CustomRemotes } from "shared/Remotes";
import { TagUtils } from "shared/utils/TagUtils";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {
		relativePositioning: {
			displayName: "Object-Relative Output",
			tooltip: "Returns coordinates relative to this block instead of relative to global grid",
			types: {
				bool: {
					config: true,
				},
			},
		},
	},
	output: {
		targetsAmount: {
			displayName: "Locked Targets Amount",
			types: ["number"],
		},
		target1: {
			displayName: "Target 1 Offset",
			types: ["vector3"],
		},
		target2: {
			displayName: "Target 2 Offset",
			types: ["vector3"],
		},
		target3: {
			displayName: "Target 3 Offset",
			types: ["vector3"],
		},
		target4: {
			displayName: "Target 4 Offset",
			types: ["vector3"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

const ownDetectablesSet = new Set<BasePart>();

if (RunService.IsClient()) {
	const p = Players.LocalPlayer;
	CustomRemotes.modes.set.sent.Connect(({ mode }) => {
		if (mode === "ride") {
			const blocks = SharedPlots.instance.getPlotComponentByOwnerID(p.UserId).getBlocks();

			for (const b of blocks) {
				if (!b.PrimaryPart) continue;
				ownDetectablesSet.add(b.PrimaryPart);
			}
			return;
		}

		ownDetectablesSet.clear();
	});
}

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

		const detectedSet = new Set<BasePart | UnionOperation | MeshPart>();
		const outputs = [this.output.target1, this.output.target2, this.output.target3, this.output.target4];

		const getPosition = (part: BasePart | UnionOperation | MeshPart, isRelative: boolean): Vector3 => {
			if (isRelative) this.instance.GetPivot().ToObjectSpace(part.GetPivot()).Position;
			return part.GetPivot().Position.sub(this.instance.GetPivot().Position);
		};

		const isRelative = this.initializeInputCache("relativePositioning");
		this.event.subscribe(RunService.Stepped, () => {
			const len = detectedSet.size();
			const arr = detectedSet.toArray();

			for (let i = 0; i < outputs.size(); i++) {
				const pp = (arr[i]?.Parent as Model)?.PrimaryPart;
				if (!pp) continue;

				outputs[i].set("vector3", i > len ? Vector3.zero : getPosition(pp, isRelative.get()));
			}

			this.output.targetsAmount.set("number", len);
		});

		this.event.subscribe(block.instance.PrimaryPart!.Touched, (part) => {
			if (!part.HasTag(TagUtils.allTags.SPECIAL_RADARVIEW)) return;
			detectedSet.add(part);
		});

		this.event.subscribe(block.instance.PrimaryPart!.TouchEnded, (part) => {
			if (!part.HasTag(TagUtils.allTags.SPECIAL_RADARVIEW)) return;
			detectedSet.delete(part);
		});
	}
}

export const RadarWarningReceiver = {
	...BlockCreation.defaults,
	id: "radarwarningreceiver",
	displayName: "Radar Warning Receiver",
	description: "Returns the positions of the radars that can be tracking the block.",
	search: {
		aliases: ["rwr"],
	},
	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: {
		linear: {
			displayName: "Linear Velocity",
			types: ["vector3"],
		},
		angular: {
			displayName: "Angular Velocity",
			types: ["vector3"],
		},
		linearAcceleration: {
			displayName: "Linear Acceleration",
			types: ["vector3"],
		},
		angularAcceleration: {
			displayName: "Angular Acceleration",
			types: ["vector3"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

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

		const getLocalPos = (pos: Vector3) => this.instance.GetPivot().Rotation.ToObjectSpace(new CFrame(pos)).Position;

		const localVelocity = {
			linear: Vector3.zero,
			angular: Vector3.zero,
		};

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

			const l1 = getLocalPos(this.instance.PrimaryPart.AssemblyLinearVelocity);
			const l2 = getLocalPos(this.instance.PrimaryPart.AssemblyAngularVelocity);

			this.output.linearAcceleration.set("vector3", l1.sub(localVelocity.linear));
			this.output.angularAcceleration.set("vector3", l2.sub(localVelocity.angular));

			this.output.linear.set("vector3", (localVelocity.linear = l1));
			this.output.angular.set("vector3", (localVelocity.angular = l2));
		});
	}
}

export const SpeedometerBlock = {
	...BlockCreation.defaults,
	id: "speedometer",
	displayName: "Speedometer",
	description:
		"Returns the current velocity and acceleration. Yes I know, there should've been a separate accelerometer block. It was.",

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


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

const definition = {
	input: {},
	output: {
		occupied: {
			displayName: "Occupied",
			types: ["bool"],
		},
		occupant: {
			displayName: "Occupant Name",
			types: ["string"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type VehicleSeatModel = BlockModel & {
	readonly VehicleSeat: VehicleSeat;
};

export type { Logic as VehicleSeatBlockLogic };

@injectable
class Logic extends InstanceBlockLogic<typeof definition, VehicleSeatModel> {
	readonly vehicleSeat;

	constructor(block: InstanceBlockLogicArgs, @inject machine: SharedMachine) {
		super(definition, block);

		this.vehicleSeat = this.instance.VehicleSeat;
		this.event.subscribeObservable(
			this.event.readonlyObservableFromInstanceParam(this.vehicleSeat, "Occupant"),
			(occupant) => {
				this.output.occupied.set("bool", occupant !== undefined);
				if (!occupant) {
					this.output.occupant.unset();
					return;
				}
				const player = Players.GetPlayerFromCharacter(occupant.Parent as Model);
				if (player) this.output.occupant.set("string", player.Name);
			},
			true,
		);

		if (RunService.IsClient()) {
			this.vehicleSeat
				.GetPropertyChangedSignal("Occupant")
				.Connect(() =>
					machine.occupiedByLocalPlayer.set(
						this.vehicleSeat.Occupant?.Parent === Players.LocalPlayer.Character,
					),
				);
		}
	}

	getDebugInfo(ctx: BlockLogicTickContext): readonly string[] {
		const char = this.vehicleSeat.Occupant?.Parent;
		const player = char && Players.GetPlayerFromCharacter(char);

		return [...super.getDebugInfo(ctx), `Occupied by ${player?.Name}`];
	}
}

export const VehicleSeatBlock = {
	...BlockCreation.defaults,
	id: "vehicleseat",
	displayName: "Driver seat",
	description: "A seat for your vehicle. Allows you to control your contraption",
	limit: 1,
	search: { partialAliases: ["vehicle"] },

	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 { CannonBases } from "shared/blocks/blocks/Weaponry/Cannon/CannonBases";
import { Colors } from "shared/Colors";
import { ShellProjectile } from "shared/weaponProjectiles/ShellProjectileLogic";
import { WeaponModule } from "shared/weaponProjectiles/WeaponModuleSystem";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {
		projectileColor: {
			displayName: "Projectile Color",
			types: {
				color: {
					config: Colors.pink,
				},
			},
		},
		fireTrigger: {
			displayName: "Fire",
			types: {
				bool: {
					config: false,
					control: {
						config: {
							enabled: true,
							key: "F",
							switch: false,
							reversed: false,
						},
						canBeReversed: false,
						canBeSwitch: false,
					},
				},
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

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

		// get outputs and the module
		const relativeOuts = new Map<BasePart, CFrame>();
		const module = WeaponModule.allModules[this.instance.Name];

		// disable markers
		module.parentCollection.setMarkersVisibility(false);

		// get relative directions
		for (const p of module.parentCollection.calculatedOutputs)
			for (const o of p.outputs)
				relativeOuts.set(o.markerInstance, this.instance.GetPivot().ToObjectSpace(o.markerInstance.CFrame));

		//update marker positions
		this.event.subscribe(RunService.Heartbeat, () => {
			const pivo = this.instance.GetPivot();
			for (const e of module.parentCollection.calculatedOutputs) {
				for (const o of e.outputs) {
					o.markerInstance.PivotTo(pivo.ToWorldSpace(relativeOuts.get(o.markerInstance)!));
				}
			}
		});

		// fire on button press
		this.onk(["fireTrigger", "projectileColor"], ({ fireTrigger, projectileColor }) => {
			if (!fireTrigger) return;
			for (const e of module.parentCollection.calculatedOutputs) {
				const mainpart = (e.module.instance as BlockModel & { MainPart: BasePart & { Sound: Sound } }).MainPart;
				const sound = mainpart.FindFirstChild("Sound") as Sound & {
					pitch: PitchShiftSoundEffect;
				};

				if (sound) sound.pitch.Octave = math.random(1000, 1200) / 10000;
				for (const o of e.outputs) {
					sound?.Play();
					const direction = o.markerInstance.GetPivot().RightVector.mul(-1);
					mainpart.ApplyImpulse(direction.mul(-100));
					ShellProjectile.spawnProjectile.send({
						startPosition: o.markerInstance.Position.add(direction),
						baseVelocity: direction, //e.module.instance.PrimaryPart!.AssemblyLinearVelocity.add(direction),
						baseDamage: 0,
						modifier: e.modifier,
						// color: projectileColor,
					});
				}
			}
		});
	}
}

export const CannonBreech = {
	...BlockCreation.defaults,
	id: "cannonbreech",
	displayName: "Cannon Breech",
	description: "",

	weaponConfig: {
		type: "CORE",
		modifier: {
			speedModifier: {
				value: 1,
			},
		},
		markers: {
			output1: {
				emitsProjectiles: false,
				allowedBlockIds: [...CannonBases.map((v) => v.id)],
			},
		},
	},

	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 { Colors } from "shared/Colors";
import { LaserProjectile } from "shared/weaponProjectiles/LaserProjectileLogic";
import { WeaponModule } from "shared/weaponProjectiles/WeaponModuleSystem";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {
		projectileColor: {
			displayName: "Projectile Color",
			types: {
				color: {
					config: Colors.pink,
				},
			},
		},
		fireTrigger: {
			displayName: "Fire",
			types: {
				bool: {
					config: false,
					control: {
						config: {
							enabled: true,
							key: "F",
							switch: false,
							reversed: false,
						},
						canBeReversed: false,
						canBeSwitch: false,
					},
				},
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

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

		// get outputs and the module
		const relativeOuts = new Map<BasePart, CFrame>();
		const module = WeaponModule.allModules[this.instance.Name];

		const mainpart = (this.instance as BlockModel & { MainPart: BasePart & { Sound: Sound } }).MainPart;
		const sound = mainpart.FindFirstChild("Sound") as Sound & {
			pitch: PitchShiftSoundEffect;
		};

		this.onFirstInputs(({ projectileColor }) => {
			(this.instance.FindFirstChild("Lens") as BasePart).Color = projectileColor;
		});

		const destroyProjectile = () => {
			for (const e of module.parentCollection.calculatedOutputs) {
				for (const o of e.outputs) {
					LaserProjectile.destroyProjectile.send({
						originPart: o.markerInstance,
					});
				}
			}
		};

		// disable markers
		module.parentCollection.setMarkersVisibility(false);

		// get relative directions
		for (const p of module.parentCollection.calculatedOutputs)
			for (const o of p.outputs)
				relativeOuts.set(o.markerInstance, this.instance.GetPivot().ToObjectSpace(o.markerInstance.CFrame));

		//update marker positions
		this.event.subscribe(RunService.Heartbeat, () => {
			const pivo = this.instance.GetPivot();
			for (const e of module.parentCollection.calculatedOutputs) {
				for (const o of e.outputs) {
					o.markerInstance.PivotTo(pivo.ToWorldSpace(relativeOuts.get(o.markerInstance)!));
				}
			}
		});

		// fire on button press
		this.onk(["fireTrigger", "projectileColor"], ({ fireTrigger, projectileColor }) => {
			if (!fireTrigger) {
				sound?.Stop();
				destroyProjectile();
				return;
			}

			for (const e of module.parentCollection.calculatedOutputs) {
				if (sound) sound.pitch.Octave = math.random(1000, 1200) / 10000;
				for (const o of e.outputs) {
					sound?.Play();
					LaserProjectile.spawnProjectile.send({
						originPart: o.markerInstance,
						baseDamage: 1,
						modifier: e.modifier,
						color: projectileColor,
						owner: Players.LocalPlayer,
					});
				}
			}
		});
	}
}

export const LaserEmitterBlock = {
	...BlockCreation.defaults,
	id: "laseremitter",
	displayName: "Laser Emitter",
	description: "",

	weaponConfig: {
		type: "CORE",
		modifier: {
			speedModifier: {
				value: 10,
			},
		},
		markers: {
			inputMarker: {
				allowedBlockIds: [],
			},
			marker1: {
				emitsProjectiles: true,
				allowedBlockIds: ["laserlens"],
			},
		},
	},

	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 { MachineGunAmmoBlocks } from "shared/blocks/blocks/Weaponry/Machinegun/MachineGunAmmoBlocks";
import { MachineGunBarrels } from "shared/blocks/blocks/Weaponry/Machinegun/MachineGunBarrels";
import { MachineGunMuzzleBrakes } from "shared/blocks/blocks/Weaponry/Machinegun/MachineGunMuzzleBrakes";
import { Colors } from "shared/Colors";
import { BulletProjectile } from "shared/weaponProjectiles/BulletProjectileLogic";
import { WeaponModule } from "shared/weaponProjectiles/WeaponModuleSystem";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {
		projectileColor: {
			displayName: "Projectile Color",
			types: {
				color: {
					config: Colors.pink,
				},
			},
		},
		fireTrigger: {
			displayName: "Fire",
			types: {
				bool: {
					config: false,
					control: {
						config: {
							enabled: true,
							key: "F",
							switch: false,
							reversed: false,
						},
						canBeReversed: false,
						canBeSwitch: false,
					},
				},
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

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

		// get outputs and the module
		const relativeOuts = new Map<BasePart, CFrame>();
		const module = WeaponModule.allModules[this.instance.Name];

		// disable markers
		module.parentCollection.setMarkersVisibility(false);

		// get relative directions
		for (const p of module.parentCollection.calculatedOutputs)
			for (const o of p.outputs)
				relativeOuts.set(o.markerInstance, this.instance.GetPivot().ToObjectSpace(o.markerInstance.CFrame));

		//update marker positions
		this.event.subscribe(RunService.Heartbeat, () => {
			const pivo = this.instance.GetPivot();
			for (const e of module.parentCollection.calculatedOutputs) {
				for (const o of e.outputs) {
					o.markerInstance.PivotTo(pivo.ToWorldSpace(relativeOuts.get(o.markerInstance)!));
				}
			}
		});

		// fire on button press
		this.onk(["fireTrigger", "projectileColor"], ({ fireTrigger, projectileColor }) => {
			if (!fireTrigger) return;
			for (const e of module.parentCollection.calculatedOutputs) {
				const mainpart = (e.module.instance as BlockModel & { MainPart: BasePart & { Sound: Sound } }).MainPart;
				const sound = mainpart.FindFirstChild("Sound") as Sound & {
					pitch: PitchShiftSoundEffect;
				};

				if (sound) sound.pitch.Octave = math.random(1000, 1200) / 10000;
				for (const o of e.outputs) {
					sound?.Play();
					const direction = o.markerInstance.GetPivot().RightVector.mul(-1);
					mainpart.ApplyImpulse(direction.mul(-100));
					BulletProjectile.spawnProjectile.send({
						startPosition: o.markerInstance.Position.add(direction),
						baseVelocity: direction, //e.module.instance.PrimaryPart!.AssemblyLinearVelocity.add(direction),
						baseDamage: 0,
						modifier: e.modifier,
						// color: projectileColor,
					});
				}
			}
		});
	}
}

export const MachineGunLoader = {
	...BlockCreation.defaults,
	id: "mgloader",
	displayName: "Machine Gun Loader",
	description: "",

	weaponConfig: {
		type: "CORE",
		modifier: {
			speedModifier: {
				value: 1000,
			},
		},
		markers: {
			output1: {
				emitsProjectiles: true,
				allowedBlockIds: [...MachineGunBarrels, ...MachineGunMuzzleBrakes, ...MachineGunAmmoBlocks].map(
					(v) => v.id,
				),
			},
			upgradeMarker: {},
		},
	},

	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 { Colors } from "shared/Colors";
import { PlasmaProjectile } from "shared/weaponProjectiles/PlasmaProjectileLogic";
import { WeaponModule } from "shared/weaponProjectiles/WeaponModuleSystem";
import type { BlockLogicFullBothDefinitions, InstanceBlockLogicArgs } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	input: {
		projectileColor: {
			displayName: "Projectile Color",
			types: {
				color: {
					config: Colors.pink,
				},
			},
		},
		fireTrigger: {
			displayName: "Fire",
			types: {
				bool: {
					config: false,
					control: {
						config: {
							enabled: true,
							key: "F",
							switch: false,
							reversed: false,
						},
						canBeReversed: false,
						canBeSwitch: false,
					},
				},
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

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

		// get outputs and the module
		const relativeOuts = new Map<BasePart, CFrame>();
		const module = WeaponModule.allModules[this.instance.Name];

		// disable markers
		module.parentCollection.setMarkersVisibility(false);

		// get relative directions
		for (const p of module.parentCollection.calculatedOutputs)
			for (const o of p.outputs)
				relativeOuts.set(o.markerInstance, this.instance.GetPivot().ToObjectSpace(o.markerInstance.CFrame));

		//update marker positions
		this.event.subscribe(RunService.Heartbeat, () => {
			const pivo = this.instance.GetPivot();
			for (const e of module.parentCollection.calculatedOutputs) {
				for (const o of e.outputs) {
					o.markerInstance.PivotTo(pivo.ToWorldSpace(relativeOuts.get(o.markerInstance)!));
				}
			}
		});

		// fire on button press
		this.onk(["fireTrigger", "projectileColor"], ({ fireTrigger, projectileColor }) => {
			if (!fireTrigger) return;
			for (const e of module.parentCollection.calculatedOutputs) {
				const mainpart = (e.module.instance as BlockModel & { MainPart: BasePart & { Sound: Sound } }).MainPart;
				const sound = mainpart.FindFirstChild("Sound") as Sound & {
					pitch: PitchShiftSoundEffect;
				};

				if (sound) sound.pitch.Octave = math.random(1000, 1200) / 10000;
				for (const o of e.outputs) {
					sound?.Play();
					const direction = o.markerInstance.GetPivot().RightVector.mul(-1);
					mainpart.ApplyImpulse(direction.mul(-100));
					PlasmaProjectile.spawnProjectile.send({
						startPosition: o.markerInstance.Position.add(direction),
						baseVelocity: e.module.instance.PrimaryPart!.AssemblyLinearVelocity.add(direction),
						baseDamage: 100,
						modifier: e.modifier,
						color: projectileColor,
					});
				}
			}
		});
	}
}

export const PlasmaGunBlock = {
	...BlockCreation.defaults,
	id: "plasmagun",
	displayName: "Plasma Gun",
	description: "",

	weaponConfig: {
		type: "CORE",
		modifier: {
			speedModifier: {
				value: 10,
			},
		},
		markers: {
			output1: {
				emitsProjectiles: true,
				allowedBlockIds: ["plasmagunbarrel", "plasmaseparatormuzzle", "plasmashotgunmuzzle"],
			},
		},
	},

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


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

const definition = {
	input: {},
	output: {
		occupied: {
			displayName: "Occupied",
			types: ["bool"],
		},
		occupant: {
			displayName: "Occupant Name",
			types: ["string"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type PassengerSeatModel = BlockModel & {
	readonly VehicleSeat: VehicleSeat;
};

export type { Logic as PassengerSeatBlockLogic };
class Logic extends InstanceBlockLogic<typeof definition, PassengerSeatModel> {
	readonly vehicleSeat;

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

		this.vehicleSeat = this.instance.VehicleSeat;

		const update = () => {
			const occupant = this.vehicleSeat.Occupant;
			this.output.occupied.set("bool", occupant !== undefined);
			if (!occupant) {
				this.output.occupant.unset();
				return;
			}
			const player = Players.GetPlayerFromCharacter(occupant.Parent as Model);
			if (player) this.output.occupant.set("string", player.Name);
		};

		this.event.subscribe(this.vehicleSeat.GetPropertyChangedSignal("Occupant"), update);
		this.onEnable(update);
	}

	getDebugInfo(ctx: BlockLogicTickContext): readonly string[] {
		const char = this.vehicleSeat.Occupant?.Parent;
		const player = char && Players.GetPlayerFromCharacter(char);

		return [...super.getDebugInfo(ctx), `Occupied by ${player?.Name}`];
	}
}

const list: BlockBuildersWithoutIdAndDefaults = {
	passengerseat: {
		displayName: "Passenger seat",
		description: "Allow your friends to have immeasurable fun with you",

		logic: { definition, ctor: Logic },
	},
	armlesspassengerseat: {
		displayName: "Armless Passenger seat",
		description: "Allow your friends to have immeasurable fun with you, but armless",

		logic: { definition, ctor: Logic },
	},
	flatseat: {
		displayName: "Flat seat",
		description: "Allow your friends backs to have immeasurable back pain",

		logic: { definition, ctor: Logic },
	},
};
export const PassengerSeatBlocks = BlockCreation.arrayFromObject(list);


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

const definitionScanner = {
	inputOrder: ["enabled", "frequency", "range", "visibility"],
	input: {
		enabled: {
			displayName: "Enabled",
			types: {
				bool: {
					config: true,
				},
			},
		},
		frequency: {
			displayName: "Frequency",
			types: {
				number: {
					config: 868,
					clamp: {
						showAsSlider: true,
						min: 434,
						max: 1500,
					},
				},
			},
		},
		range: {
			displayName: "Range",
			types: {
				number: {
					config: 50,
					clamp: {
						showAsSlider: true,
						min: 0,
						max: 2048,
					},
				},
			},
		},
		visibility: {
			displayName: "Detection Area Visibility",
			types: {
				bool: { config: false },
			},
			connectorHidden: true,
		},
	},
	output: {
		connected: {
			displayName: "Connected",
			types: ["bool"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

const definitionReceiver = {
	inputOrder: ["frequency", "range", "visibility"],
	input: {
		frequency: {
			displayName: "Frequency",
			types: {
				number: {
					config: 868,
					clamp: {
						showAsSlider: true,
						min: 434,
						max: 1500,
					},
				},
			},
		},
		range: {
			displayName: "Range",
			types: {
				number: {
					config: 50,
					clamp: {
						showAsSlider: true,
						min: 0,
						max: 2048,
					},
				},
			},
		},
		visibility: {
			displayName: "Detection Area Visibility",
			types: {
				bool: { config: false },
			},
			connectorHidden: true,
		},
	},
	output: {
		scanners: {
			displayName: "Scanners",
			types: ["number"],
		},
		connected: {
			displayName: "Connected",
			types: ["bool"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

type ProximityModel = BlockModel & {
	readonly Sphere: BasePart | UnionOperation | MeshPart;
};

const update = new ArgsSignal<[receiver: ProximityReceiverBlock]>();
const allReceivers = new Map<ProximityModel, ProximityReceiverBlock>();

const allProxies = new Set<ProximityModel>();
const tpSphere = () => {
	for (const model of allProxies) {
		const sphere = model.Sphere;

		sphere.AssemblyLinearVelocity = Vector3.zero;
		sphere.AssemblyAngularVelocity = Vector3.zero;
		sphere.PivotTo(model.PrimaryPart!.CFrame);
	}
};
RunService.PreRender.Connect(tpSphere); // for logic visualizer
RunService.PreSimulation.Connect(tpSphere); // for actual contact

abstract class LogicShared<T extends typeof definitionScanner | typeof definitionReceiver> extends InstanceBlockLogic<
	T,
	ProximityModel
> {
	readonly detected = new ArgsSignal<[state: boolean]>();

	constructor(definition: T, block: InstanceBlockLogicArgs) {
		super(definition, block);
		const sphere = this.instance.Sphere;

		this.onk(["range"], ({ range }) => {
			if (!sphere) return;
			sphere.Size = Vector3.one.mul(range);
		});

		this.onk(["visibility"], ({ visibility }) => {
			sphere.Transparency = visibility ? 0.8 : 1;
		});

		this.onEnable(() => allProxies.add(this.instance));
		this.onDisable(() => allProxies.delete(this.instance));
	}
}

@injectable
class ProximityScannerBlock extends LogicShared<typeof definitionScanner> {
	constructor(block: InstanceBlockLogicArgs) {
		super(definitionScanner, block);

		const sphere = this.instance.Sphere;
		const touching = new Set<ProximityModel>();
		const connected = new Set<ProximityReceiverBlock>();

		const frequencyInputCache = this.initializeInputCache("frequency");

		const updateOutput = () => {
			this.output.connected.set("bool", connected.size() !== 0);
		};

		const connect = (receiver: ProximityReceiverBlock) => {
			connected.add(receiver);
			receiver.connected.add(this);
			receiver.setOutput.Fire();

			updateOutput();
		};
		const disconnect = (receiver: ProximityReceiverBlock) => {
			touching.delete(receiver.instance);
			connected.delete(receiver);
			receiver.connected.delete(this);
			receiver.setOutput.Fire();

			updateOutput();
		};

		const disconnectAll = () => {
			for (const receiver of connected) {
				disconnect(receiver);
			}
		};

		this.onk(["enabled"], ({ enabled }) => {
			sphere.CanTouch = enabled;
			if (!enabled) disconnectAll();
		});

		this.onDisable(disconnectAll);

		this.event.subscribe(update, (receiver) => {
			if (receiver.frequency !== frequencyInputCache.tryGet()) {
				disconnect(receiver);
			} else {
				if (touching.has(receiver.instance)) {
					connect(receiver);
				}
			}
		});

		const tryGetReceiverByPart = (part: BasePart) => {
			const partModel = BlockManager.tryGetBlockModelByPart(part);
			if (!partModel) return;

			return allReceivers.get(partModel as ProximityModel);
		};

		this.event.subscribe(sphere.Touched, (part) => {
			const receiver = tryGetReceiverByPart(part);
			if (!receiver) return;

			touching.add(receiver.instance);
			if (receiver.frequency === frequencyInputCache.tryGet()) {
				connect(receiver);
			}
		});

		this.event.subscribe(sphere.TouchEnded, (part) => {
			const receiver = tryGetReceiverByPart(part);
			if (!receiver) return;
			disconnect(receiver);
		});
	}
}
@injectable
class ProximityReceiverBlock extends LogicShared<typeof definitionReceiver> {
	frequency?: number;
	readonly connected = new Set<ProximityScannerBlock>();
	setOutput = new ArgsSignal<[]>();
	constructor(block: InstanceBlockLogicArgs) {
		super(definitionReceiver, block);

		this.onEnable(() => allReceivers.set(this.instance, this));
		this.onDisable(() => {
			this.instance.Sphere.Destroy();
			this.frequency = -1;
			update.Fire(this);
			allReceivers.delete(this.instance);
		});

		this.event.subscribe(this.setOutput, () => {
			this.output.connected.set("bool", this.connected.size() !== 0);
			this.output.scanners.set("number", this.connected.size());
		});

		this.onk(["frequency"], ({ frequency }) => {
			this.frequency = frequency;
			update.Fire(this);
		});
	}
}

export const ProximityBlocks = [
	{
		...BlockCreation.defaults,
		id: "proximityscanner",
		displayName: "Proximity Scanner",
		description: "Looks for Receivers on the same frequency, returns true when connected, false if not",
		search: { partialAliases: ["proxy", "gun", "reader"] },
		logic: { definition: definitionScanner, ctor: ProximityScannerBlock },
	},
	{
		...BlockCreation.defaults,
		id: "proximityreceiver",
		displayName: "Proximity Receiver",
		description: "Returns if it is within proximity of a scanner on the same frequency, and how many of them",
		search: {
			partialAliases: ["proxy", "bullet"],
			aliases: ["keycard"],
		},

		logic: { definition: definitionReceiver, ctor: ProximityReceiverBlock },
	},
] as const satisfies BlockBuilder[];


import { RunService, Workspace } from "@rbxts/services";
import { InstanceBlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { GameDefinitions } from "shared/data/GameDefinitions";
import { GameEnvironment } from "shared/data/GameEnvironment";
import type { PlayerDataStorage } from "client/PlayerDataStorage";
import type { PlacedBlockConfig } from "shared/blockLogic/BlockConfig";
import type {
	BlockLogicFullBothDefinitions,
	GenericBlockLogic,
	InstanceBlockLogicArgs,
} from "shared/blockLogic/BlockLogic";
import type { BlockBuildersWithoutIdAndDefaults, BlockLogicInfo } from "shared/blocks/Block";

const definition = {
	input: {
		enabled: {
			displayName: "Enabled",
			tooltip: "Disables aerodynamic properties if you are a fan of using this block for other purposes",
			types: {
				bool: {
					config: true,
				},
			},
			connectorHidden: true,
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

type WingBlock = BlockModel & {
	readonly WingSurface:
		| BasePart
		| (UnionOperation & {
				readonly VectorForce: VectorForce;
		  });
};

// Constants
const FORCE_MULTIPLIER = -20.5; // F = -ρ * A * v * CL, ~~ -(5 * 5.3 * 4)
const HEIGHT_FACTOR_EXPONENT = 2; // for h = 1 - (y/H)^exp
const MIN_HORIZONTAL_SPEED = 30; // Minimum horizontal speed for full lift (studs/sec) - lower for easier gliding

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

		const fluidForcesEnabled = !playerData?.config.get().physics.simplified_aerodynamics;
		if (fluidForcesEnabled) {
			this.instance.WingSurface.EnableFluidForces = true;
			return;
		}

		this.onkFirstInputs(["enabled"], ({ enabled }) => {
			if (!enabled) {
				this.instance.WingSurface.EnableFluidForces = false;
				this.disable();
				return;
			}

			// Create force constraints
			const attachment = new Instance("Attachment", this.instance.WingSurface);
			const vectorForce = new Instance("VectorForce", this.instance.WingSurface);
			vectorForce.RelativeTo = Enum.ActuatorRelativeTo.Attachment0;
			vectorForce.Attachment0 = attachment;

			// Set up wing material properties
			const density = math.max(0.7, new PhysicalProperties(this.instance.WingSurface.Material).Density / 2);
			this.instance.WingSurface.CustomPhysicalProperties = new PhysicalProperties(density, 0.3, 0.5, 1, 1);

			// Calculate wing surface area and dimensions
			const wing = this.instance.WingSurface;
			const wingArea = wing.Size.X * wing.Size.Z;

			// Calculate effective surface vector based on wing type
			let effectiveSurface: Vector3;
			if (wing.IsA("WedgePart")) {
				// Wedge wings: area acts as lift, divided by 2 for balance
				const thickness = wing.Size.X;
				effectiveSurface = new Vector3(wingArea, thickness, thickness).div(2);
			} else {
				// Regular wings: area acts perpendicular to wing surface
				const thickness = wing.Size.Y;
				effectiveSurface = new Vector3(thickness, wingArea, thickness);
			}

			this.event.subscribe(RunService.Heartbeat, () => {
				if (!this.instance.FindFirstChild("WingSurface")) {
					return;
				}

				const wing = this.instance.WingSurface;

				// Step 1: Calculate effective velocity including rotation
				// Linear velocity component
				const linearVelocity = wing.AssemblyLinearVelocity;

				// Angular velocity contribution: v = ω × r (cross product)
				// where r is the vector from center of mass to wing position
				const angularVelocity = wing.AssemblyAngularVelocity;
				const relativePosition = wing.Position.sub(wing.AssemblyCenterOfMass);
				const rotationalVelocity = angularVelocity.Cross(relativePosition);

				// Total effective velocity
				const effectiveVelocity = linearVelocity.add(rotationalVelocity);

				// Step 2: Calculate horizontal speed for lift scaling
				const horizontalVelocity = new Vector3(effectiveVelocity.X, 0, effectiveVelocity.Z);
				const horizontalSpeed = horizontalVelocity.Magnitude;

				// Gradual lift dropoff using smoothstep curve
				// Provides smooth transition from 0 to full lift
				const speedRatio = math.min(horizontalSpeed / MIN_HORIZONTAL_SPEED, 1);
				const speedFactor = speedRatio * speedRatio * (3 - 2 * speedRatio); // Smoothstep interpolation

				// Step 3: Convert to local space
				const relativeVelocity = wing.CFrame.PointToObjectSpace(wing.Position.add(effectiveVelocity));

				// Step 4: Reduce horizontal drag to prevent speed loss during gliding
				// Only apply force multiplier to vertical component (Y) for lift
				// Reduce horizontal components (X, Z) to minimize drag
				const HORIZONTAL_DRAG_REDUCTION = 0.01; // Reduce horizontal drag by 99%
				const adjustedVelocity = new Vector3(
					relativeVelocity.X * HORIZONTAL_DRAG_REDUCTION,
					relativeVelocity.Y, // Full vertical component for lift
					relativeVelocity.Z * HORIZONTAL_DRAG_REDUCTION,
				);

				// Step 5: Apply force multiplier to adjusted velocity
				const velocityForce = adjustedVelocity.mul(FORCE_MULTIPLIER);

				// Step 6: Calculate lift force based on effective surface
				const liftForce = effectiveSurface.mul(velocityForce);

				// Step 7: Scale lift by speed factor (less lift at low speeds)
				const scaledLiftForce = liftForce.mul(speedFactor);

				// Step 8: Average with previous force for stability
				const averagedForce = scaledLiftForce.add(vectorForce.Force).div(2);

				// Step 9: Apply height factor (air density decreases with altitude)
				const heightFactor = math.clamp(
					1 -
						math.pow(
							(wing.Position.Y - GameDefinitions.HEIGHT_OFFSET) / GameEnvironment.ZeroAirHeight,
							HEIGHT_FACTOR_EXPONENT,
						),
					0,
					1,
				);

				// Step 10: Apply final force
				const finalForce = averagedForce.mul(heightFactor);

				vectorForce.Enabled = finalForce.Magnitude > Workspace.Gravity * wing.Mass;
				vectorForce.Force = finalForce;
			});
		});
	}

	initializeInputs(config: PlacedBlockConfig, allBlocks: ReadonlyMap<BlockUuid, GenericBlockLogic>): void {
		super.initializeInputs(config, allBlocks);
	}
}

const logic: BlockLogicInfo = { definition, ctor: Logic };
const list: BlockBuildersWithoutIdAndDefaults = {
	wing1x1: {
		displayName: "Wing 1x1",
		description: "A part with advanced aerodynamic properties",
		logic,
		mirror: { behaviour: "wedgeWing" },
	},
	wing1x2: {
		displayName: "Wing 1x2",
		description: "A part with advanced aerodynamic properties but a bit longer",
		logic,
		mirror: { behaviour: "wedgeWing" },
	},
	wing1x3: {
		displayName: "Wing 1x3",
		description: "A part with advanced aerodynamic properties but two bits longer",
		logic,
		mirror: { behaviour: "wedgeWing" },
	},
	wing1x4: {
		displayName: "Wing 1x4",
		description: "A part with advanced aerodynamic properties but the joke is overused",
		logic,
		mirror: { behaviour: "wedgeWing" },
	},
	wedgewing1x1: {
		displayName: "Wedge Wing 1x1",
		description: "A wedge shaped wing",
		logic,
		mirror: { behaviour: "wedgeWing" },
	},
	wedgewing1x2: {
		displayName: "Wedge Wing 1x2",
		description: "A wedge shaped wing but longer",
		logic,
		mirror: { behaviour: "wedgeWing" },
	},
	wedgewing1x3: {
		displayName: "Wedge Wing 1x3",
		description: "A wedge shaped wing but much longer",
		logic,
		mirror: { behaviour: "wedgeWing" },
	},
	wedgewing1x4: {
		displayName: "Wedge Wing 1x4",
		description: "A humongously long wedge shaped wing",
		logic,
		mirror: { behaviour: "wedgeWing" },
	},
};
export const WingBlocks = BlockCreation.arrayFromObject(list);


import { RunService } from "@rbxts/services";
import { Colors } from "engine/shared/Colors";
import { Component } from "engine/shared/component/Component";
import { Element } from "engine/shared/Element";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockConfigDefinitions } from "shared/blocks/BlockConfigDefinitions";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { dataToString } from "shared/blocks/blocks/ScreenBlock";
import { BlockManager } from "shared/building/BlockManager";
import type { MainScreenLayout } from "client/gui/MainScreenLayout";
import type { BlockConfigPart } from "shared/blockLogic/BlockConfig";
import type {
	BlockLogicArgs,
	BlockLogicBothDefinitions,
	BlockLogicFullBothDefinitions,
} from "shared/blockLogic/BlockLogic";
import type { BlockLogicTypes } from "shared/blockLogic/BlockLogicTypes";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: [
		"text",
		"position",
		"size",
		"anchor",
		"rotation",
		"textColor",
		"textTransparency",
		"backgroundColor",
		"backgroundTransparency",
		"zIndex",
	],
	input: {
		text: {
			displayName: "Text",
			types: BlockConfigDefinitions.any,
		},
		position: {
			displayName: "Position",
			unit: "Relative Vector3 (0-1)",
			types: {
				vector3: { config: new Vector3(0.5, 0.5) },
			},
		},
		size: {
			displayName: "Size",
			unit: "Relative Vector3 (0-1)",
			types: {
				vector3: { config: new Vector3(0.1, 0.2) },
			},
		},
		anchor: {
			displayName: "Anchor",
			unit: "Relative Vector3 (0-1)",
			types: {
				vector3: { config: new Vector3(0.5, 0.5) },
			},
		},
		rotation: {
			displayName: "Rotation",
			unit: "degrees",
			types: {
				number: { config: 0 },
			},
		},
		textColor: {
			displayName: "Text Color",
			types: {
				color: { config: Colors.white },
			},
		},
		textTransparency: {
			displayName: "Text Transparency",
			types: {
				number: { config: 0 },
			},
		},
		backgroundColor: {
			displayName: "Background Color",
			types: {
				color: { config: Colors.black },
			},
		},
		backgroundTransparency: {
			displayName: "Background Transparency",
			types: {
				number: { config: 0 },
			},
		},
		zIndex: {
			displayName: "Z Index",
			types: {
				number: { config: 0 },
			},
		},
	},
	output: {
		pressed: {
			displayName: "Pressed",
			types: ["bool"],
		},
	},
} satisfies BlockLogicFullBothDefinitions;

const replace = (text: string, str: string, newstr: string) => {
	const idx = text.find(str)[0];
	if (idx) {
		return `${text.sub(1, idx - 1)}${newstr}${text.sub(idx + str.size())}`;
	}

	return text;
};

export type { Logic as SpeakerBlockLogic };
@injectable
class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs, @inject screen: MainScreenLayout) {
		super(definition, block);
		if (!RunService.IsClient()) return;

		const label = Element.create("TextButton", {
			Name: `BlockText${block.instance?.Name}`,
			Font: Enum.Font.Ubuntu,
			TextColor3: Colors.white,
			TextScaled: true,
			BackgroundTransparency: 0,
			Text: "",
			BorderSizePixel: 0,
			Parent: screen.fullScreenScaled8,
		});
		this.onDisable(() => label.Destroy());

		this.onk(["text"], ({ text }) => (label.Text = dataToString(text)));
		this.onk(["position"], ({ position }) => (label.Position = new UDim2(position.X, 0, position.Y, 0)));
		this.onk(["size"], ({ size }) => (label.Size = new UDim2(size.X, 0, size.Y, 0)));
		this.onk(["rotation"], ({ rotation }) => (label.Rotation = rotation));
		this.onk(["anchor"], ({ anchor }) => (label.AnchorPoint = new Vector2(anchor.X, anchor.Y)));
		this.onk(["textColor"], ({ textColor }) => (label.TextColor3 = textColor));
		this.onk(["textTransparency"], ({ textTransparency }) => (label.TextTransparency = textTransparency));
		this.onk(["backgroundColor"], ({ backgroundColor }) => (label.BackgroundColor3 = backgroundColor));
		this.onk(
			["backgroundTransparency"],
			({ backgroundTransparency }) => (label.BackgroundTransparency = backgroundTransparency),
		);
		this.onk(["zIndex"], ({ zIndex }) => (label.ZIndex = zIndex));

		label.MouseButton1Down.Connect(() => this.output.pressed.set("bool", true));
		label.MouseButton1Up.Connect(() => this.output.pressed.set("bool", false));
		label.MouseLeave.Connect(() => this.output.pressed.set("bool", false));

		this.output.pressed.set("bool", false);
	}
}

class PreviewComponent extends Component {
	constructor(block: BlockModel) {
		super();

		this.$onInjectAuto((screen: MainScreenLayout) => {
			type DefinitionToConfig<TDef extends BlockLogicBothDefinitions> = {
				[k in keyof TDef["input"]]?: BlockConfigPart<
					(keyof BlockLogicTypes.Primitives & keyof TDef["input"][k]["types"]) | "unset" | "wire"
				>;
			};

			const map = <T, V>(value: T, func: (value: T) => V) => func(value);

			const label = Element.create("TextButton", {
				Name: `BlockTextPreview`,
				Font: Enum.Font.Ubuntu,
				TextScaled: true,
				BorderSizePixel: 0,
				Parent: screen.fullScreenScaled8,
			});
			this.onDestroy(() => label.Destroy());

			this.event.loop(0.1, () => {
				const config = (BlockManager.manager.config.get(block) ?? {}) as DefinitionToConfig<typeof definition>;

				label.Text = label.Text = dataToString(
					BlockCreation.defaultIfWiredUnset(config.text, definition.input.text.types.string.config),
				);
				label.Position = map(
					BlockCreation.defaultIfWiredUnset(config.position, definition.input.position.types.vector3.config),
					(position) => new UDim2(position.X, 0, position.Y, 0),
				);
				label.Size = map(
					BlockCreation.defaultIfWiredUnset(config.size, definition.input.size.types.vector3.config),
					(size) => new UDim2(size.X, 0, size.Y, 0),
				);
				label.Rotation = BlockCreation.defaultIfWiredUnset(
					config.rotation,
					definition.input.rotation.types.number.config,
				);
				label.AnchorPoint = map(
					BlockCreation.defaultIfWiredUnset(config.anchor, definition.input.anchor.types.vector3.config),
					(anchor) => new Vector2(anchor.X, anchor.Y),
				);
				label.TextColor3 = BlockCreation.defaultIfWiredUnset(
					config.textColor,
					definition.input.textColor.types.color.config,
				);
				label.TextTransparency = BlockCreation.defaultIfWiredUnset(
					config.textTransparency,
					definition.input.textTransparency.types.number.config,
				);
				label.BackgroundColor3 = BlockCreation.defaultIfWiredUnset(
					config.backgroundColor,
					definition.input.backgroundColor.types.color.config,
				);
				label.BackgroundTransparency = BlockCreation.defaultIfWiredUnset(
					config.backgroundTransparency,
					definition.input.backgroundTransparency.types.number.config,
				);
				label.ZIndex = BlockCreation.defaultIfWiredUnset(
					config.zIndex,
					definition.input.zIndex.types.number.config,
				);
			});
		});
	}
}

export const GuiButtonBlock = {
	...BlockCreation.defaults,
	id: "guibutton",
	displayName: "GUI Button",
	description: "Places a button on your screen",

	logic: { definition, ctor: Logic, preview: PreviewComponent },
	modelSource: {
		model: BlockCreation.Model.fAutoCreated("x4GuiLogicBlockPrefab", "BUTTON"),
		category: () => BlockCreation.Categories.gui,
	},
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { Colors } from "engine/shared/Colors";
import { Component } from "engine/shared/component/Component";
import { Element } from "engine/shared/Element";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { BlockManager } from "shared/building/BlockManager";
import type { MainScreenLayout } from "client/gui/MainScreenLayout";
import type { BlockConfigPart } from "shared/blockLogic/BlockConfig";
import type {
	BlockLogicArgs,
	BlockLogicBothDefinitions,
	BlockLogicFullBothDefinitions,
} from "shared/blockLogic/BlockLogic";
import type { BlockLogicTypes } from "shared/blockLogic/BlockLogicTypes";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: [
		"image",
		"position",
		"size",
		"anchor",
		"rotation",
		"backgroundColor",
		"backgroundTransparency",
		"zIndex",
	],
	input: {
		image: {
			displayName: "Image ID",
			types: {
				string: { config: "17407819496" },
			},
		},
		position: {
			displayName: "Position",
			unit: "Relative Vector3 (0-1)",
			types: {
				vector3: { config: new Vector3(0.5, 0.5) },
			},
		},
		size: {
			displayName: "Size",
			unit: "Relative Vector3 (0-1)",
			types: {
				vector3: { config: new Vector3(0.5, 0.5) },
			},
		},
		anchor: {
			displayName: "Anchor",
			unit: "Relative Vector3 (0-1)",
			types: {
				vector3: { config: new Vector3(0.5, 0.5) },
			},
		},
		rotation: {
			displayName: "Rotation",
			unit: "degrees",
			types: {
				number: { config: 0 },
			},
		},
		backgroundColor: {
			displayName: "Background Color",
			types: {
				color: { config: Colors.black },
			},
		},
		backgroundTransparency: {
			displayName: "Background Transparency",
			types: {
				number: { config: 1 },
			},
		},
		zIndex: {
			displayName: "Z Index",
			types: {
				number: { config: 0 },
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as SpeakerBlockLogic };
@injectable
class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs, @inject screen: MainScreenLayout) {
		super(definition, block);
		if (!RunService.IsClient()) return;

		const label = Element.create("ImageLabel", {
			Name: `BlockImage${block.instance?.Name}`,
			BackgroundTransparency: 1,
			BorderSizePixel: 0,
			Parent: screen.fullScreenScaled8,
		});
		this.onDisable(() => label.Destroy());

		this.onk(["image"], ({ image }) => (label.Image = `rbxassetid://${image}`));
		this.onk(["position"], ({ position }) => (label.Position = new UDim2(position.X, 0, position.Y, 0)));
		this.onk(["size"], ({ size }) => (label.Size = new UDim2(size.X, 0, size.Y, 0)));
		this.onk(["rotation"], ({ rotation }) => (label.Rotation = rotation));
		this.onk(["anchor"], ({ anchor }) => (label.AnchorPoint = new Vector2(anchor.X, anchor.Y)));
		this.onk(["backgroundColor"], ({ backgroundColor }) => (label.BackgroundColor3 = backgroundColor));
		this.onk(
			["backgroundTransparency"],
			({ backgroundTransparency }) => (label.BackgroundTransparency = backgroundTransparency),
		);
		this.onk(["zIndex"], ({ zIndex }) => (label.ZIndex = zIndex));
	}
}

class PreviewComponent extends Component {
	constructor(block: BlockModel) {
		super();

		this.$onInjectAuto((screen: MainScreenLayout) => {
			type DefinitionToConfig<TDef extends BlockLogicBothDefinitions> = {
				[k in keyof TDef["input"]]?: BlockConfigPart<
					(keyof BlockLogicTypes.Primitives & keyof TDef["input"][k]["types"]) | "unset" | "wire"
				>;
			};

			const map = <T, V>(value: T, func: (value: T) => V) => func(value);

			const label = Element.create("ImageLabel", {
				Name: `BlockImagePreview`,
				BorderSizePixel: 0,
				Parent: screen.fullScreenScaled8,
			});
			this.onDestroy(() => label.Destroy());

			this.event.loop(0.1, () => {
				const config = (BlockManager.manager.config.get(block) ?? {}) as DefinitionToConfig<typeof definition>;

				label.Image = map(
					BlockCreation.defaultIfWiredUnset(config.image, definition.input.image.types.string.config),
					(image) => `rbxassetid://${image}`,
				);
				label.Position = map(
					BlockCreation.defaultIfWiredUnset(config.position, definition.input.position.types.vector3.config),
					(position) => new UDim2(position.X, 0, position.Y, 0),
				);
				label.Size = map(
					BlockCreation.defaultIfWiredUnset(config.size, definition.input.size.types.vector3.config),
					(size) => new UDim2(size.X, 0, size.Y, 0),
				);
				label.Rotation = BlockCreation.defaultIfWiredUnset(
					config.rotation,
					definition.input.rotation.types.number.config,
				);
				label.AnchorPoint = map(
					BlockCreation.defaultIfWiredUnset(config.anchor, definition.input.anchor.types.vector3.config),
					(anchor) => new Vector2(anchor.X, anchor.Y),
				);
				label.BackgroundColor3 = BlockCreation.defaultIfWiredUnset(
					config.backgroundColor,
					definition.input.backgroundColor.types.color.config,
				);
				label.BackgroundTransparency = BlockCreation.defaultIfWiredUnset(
					config.backgroundTransparency,
					definition.input.backgroundTransparency.types.number.config,
				);
				label.ZIndex = BlockCreation.defaultIfWiredUnset(
					config.zIndex,
					definition.input.zIndex.types.number.config,
				);
			});
		});
	}
}

export const GuiImageBlock = {
	...BlockCreation.defaults,
	id: "guiimage",
	displayName: "GUI Image",
	description: "Draws an image on your screen",

	logic: { definition, ctor: Logic, preview: PreviewComponent },
	modelSource: {
		model: BlockCreation.Model.fAutoCreated("x4GuiLogicBlockPrefab", "IMAGE"),
		category: () => BlockCreation.Categories.gui,
	},
} as const satisfies BlockBuilder;


import { RunService } from "@rbxts/services";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { dataToString } from "shared/blocks/blocks/ScreenBlock";
import type { RideMode } from "client/modes/ride/RideMode";
import type { BlockLogicArgs, BlockLogicFullBothDefinitions } from "shared/blockLogic/BlockLogic";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: ["name", "text"],
	input: {
		name: {
			displayName: "Name",
			types: {
				string: { config: "Altitude" },
			},
		},
		text: {
			displayName: "Text",
			types: {
				bool: { config: false },
				byte: { config: 0 },
				number: { config: 0 },
				string: { config: "" },
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

export type { Logic as SpeakerBlockLogic };
@injectable
class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs, @inject rideMode: RideMode) {
		super(definition, block);
		if (!RunService.IsClient()) return;

		this.onEnable(() => {
			task.defer(() => {
				const scene = rideMode.rideModeScene;
				const gui = scene.infoTextTemplate();
				gui.FormattedText!.Text = "%s";

				const control = scene.addMeter(gui);
				this.onDisable(() => control.destroy());

				this.onk(["name"], ({ name }) => (gui.Title.Text = name.upper()));
				this.onk(["text"], ({ text }) => control.text.value.set(dataToString(text)));
			});
		});
	}
}

export const GuiStatBlock = {
	...BlockCreation.defaults,
	id: "guistat",
	displayName: "GUI Meter",
	description: "Writes text as a meter in the top left part of your screen",

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


import { RunService } from "@rbxts/services";
import { Colors } from "engine/shared/Colors";
import { Component } from "engine/shared/component/Component";
import { Element } from "engine/shared/Element";
import { BlockLogic } from "shared/blockLogic/BlockLogic";
import { BlockConfigDefinitions } from "shared/blocks/BlockConfigDefinitions";
import { BlockCreation } from "shared/blocks/BlockCreation";
import { dataToString } from "shared/blocks/blocks/ScreenBlock";
import { BlockManager } from "shared/building/BlockManager";
import type { MainScreenLayout } from "client/gui/MainScreenLayout";
import type { BlockConfigPart } from "shared/blockLogic/BlockConfig";
import type {
	BlockLogicArgs,
	BlockLogicBothDefinitions,
	BlockLogicFullBothDefinitions,
} from "shared/blockLogic/BlockLogic";
import type { BlockLogicTypes } from "shared/blockLogic/BlockLogicTypes";
import type { BlockBuilder } from "shared/blocks/Block";

const definition = {
	inputOrder: [
		"text",
		"fontSize",
		"position",
		"anchor",
		"rotation",
		"textColor",
		"textTransparency",
		"backgroundColor",
		"backgroundTransparency",
		"format",
		"zIndex",
	],
	input: {
		text: {
			displayName: "Text",
			types: BlockConfigDefinitions.any,
		},
		fontSize: {
			displayName: "Font Size",
			unit: "pt",
			types: {
				number: { config: 14 },
			},
		},
		position: {
			displayName: "Position",
			unit: "Relative Vector3 (0-1)",
			types: {
				vector3: { config: new Vector3(0.5, 0.5) },
			},
		},
		anchor: {
			displayName: "Anchor",
			unit: "Relative Vector3 (0-1)",
			types: {
				vector3: { config: new Vector3(0.5, 0.5) },
			},
		},
		rotation: {
			displayName: "Rotation",
			unit: "degrees",
			types: {
				number: { config: 0 },
			},
		},
		textColor: {
			displayName: "Text Color",
			types: {
				color: { config: Colors.white },
			},
		},
		textTransparency: {
			displayName: "Text Transparency",
			types: {
				number: { config: 0 },
			},
		},
		backgroundColor: {
			displayName: "Background Color",
			types: {
				color: { config: Colors.black },
			},
		},
		backgroundTransparency: {
			displayName: "Background Transparency",
			types: {
				number: { config: 1 },
			},
		},
		format: {
			displayName: "Format",
			types: {
				string: { config: "{}" },
			},
		},
		zIndex: {
			displayName: "Z Index",
			types: {
				number: { config: 0 },
			},
		},
	},
	output: {},
} satisfies BlockLogicFullBothDefinitions;

const replace = (text: string, str: string, newstr: string) => {
	const idx = text.find(str)[0];
	if (idx) {
		return `${text.sub(1, idx - 1)}${newstr}${text.sub(idx + str.size())}`;
	}

	return text;
};

export type { Logic as SpeakerBlockLogic };
@injectable
class Logic extends BlockLogic<typeof definition> {
	constructor(block: BlockLogicArgs, @inject screen: MainScreenLayout) {
		super(definition, block);
		if (!RunService.IsClient()) return;

		const label = Element.create("TextLabel", {
			Name: `BlockText${block.instance?.Name}`,
			Font: Enum.Font.Ubuntu,
			TextColor3: Colors.white,
			AutomaticSize: Enum.AutomaticSize.XY,
			BackgroundTransparency: 1,
			Text: "",
			BorderSizePixel: 0,
			Parent: screen.fullScreenScaled8,
		});
		this.onDisable(() => label.Destroy());

		const format = this.initializeInputCache("format");
		this.onk(["text"], ({ text }) => (label.Text = replace(format.tryGet() ?? "{}", "{}", dataToString(text))));
		this.onk(["fontSize"], ({ fontSize }) => (label.TextSize = fontSize));
		this.onk(["position"], ({ position }) => (label.Position = new UDim2(position.X, 0, position.Y, 0)));
		this.onk(["rotation"], ({ rotation }) => (label.Rotation = rotation));
		this.onk(["anchor"], ({ anchor }) => (label.AnchorPoint = new Vector2(anchor.X, anchor.Y)));
		this.onk(["textColor"], ({ textColor }) => (label.TextColor3 = textColor));
		this.onk(["textTransparency"], ({ textTransparency }) => (label.TextTransparency = textTransparency));
		this.onk(["backgroundColor"], ({ backgroundColor }) => (label.BackgroundColor3 = backgroundColor));
		this.onk(
			["backgroundTransparency"],
			({ backgroundTransparency }) => (label.BackgroundTransparency = backgroundTransparency),
		);
		this.onk(["zIndex"], ({ zIndex }) => (label.ZIndex = zIndex));
	}
}

class PreviewComponent extends Component {
	constructor(block: BlockModel) {
		super();

		this.$onInjectAuto((screen: MainScreenLayout) => {
			type DefinitionToConfig<TDef extends BlockLogicBothDefinitions> = {
				[k in keyof TDef["input"]]?: BlockConfigPart<
					(keyof BlockLogicTypes.Primitives & keyof TDef["input"][k]["types"]) | "unset" | "wire"
				>;
			};

			const map = <T, V>(value: T, func: (value: T) => V) => func(value);

			const label = Element.create("TextLabel", {
				Name: `BlockTextPreview`,
				Font: Enum.Font.Ubuntu,
				AutomaticSize: Enum.AutomaticSize.XY,
				BorderSizePixel: 0,
				Parent: screen.fullScreenScaled8,
			});
			this.onDestroy(() => label.Destroy());

			this.event.loop(0.1, () => {
				const config = (BlockManager.manager.config.get(block) ?? {}) as DefinitionToConfig<typeof definition>;

				label.Text = label.Text = replace(
					BlockCreation.defaultIfWiredUnset(config.format, definition.input.format.types.string.config),
					"{}",
					dataToString(
						BlockCreation.defaultIfWiredUnset(config.text, definition.input.text.types.string.config),
					),
				);
				label.TextSize = BlockCreation.defaultIfWiredUnset(
					config.fontSize,
					definition.input.fontSize.types.number.config,
				);
				label.Position = map(
					BlockCreation.defaultIfWiredUnset(config.position, definition.input.position.types.vector3.config),
					(position) => new UDim2(position.X, 0, position.Y, 0),
				);
				label.Rotation = BlockCreation.defaultIfWiredUnset(
					config.rotation,
					definition.input.rotation.types.number.config,
				);
				label.AnchorPoint = map(
					BlockCreation.defaultIfWiredUnset(config.anchor, definition.input.anchor.types.vector3.config),
					(anchor) => new Vector2(anchor.X, anchor.Y),
				);
				label.TextColor3 = BlockCreation.defaultIfWiredUnset(
					config.textColor,
					definition.input.textColor.types.color.config,
				);
				label.TextTransparency = BlockCreation.defaultIfWiredUnset(
					config.textTransparency,
					definition.input.textTransparency.types.number.config,
				);
				label.BackgroundColor3 = BlockCreation.defaultIfWiredUnset(
					config.backgroundColor,
					definition.input.backgroundColor.types.color.config,
				);
				label.BackgroundTransparency = BlockCreation.defaultIfWiredUnset(
					config.backgroundTransparency,
					definition.input.backgroundTransparency.types.number.config,
				);
				label.ZIndex = BlockCreation.defaultIfWiredUnset(
					config.zIndex,
					definition.input.zIndex.types.number.config,
				);
			});
		});
	}
}

export const GuiTextBlock = {
	...BlockCreation.defaults,
	id: "guitext",
	displayName: "GUI Text",
	description: "Writes text on your screen",

	logic: { definition, ctor: Logic, preview: PreviewComponent },
	modelSource: {
		model: BlockCreation.Model.fAutoCreated("x4GuiLogicBlockPrefab", "TEXT"),
		category: () => BlockCreation.Categories.gui,
	},
} as const satisfies BlockBuilder;


import { RunService, ServerStorage, Workspace } from "@rbxts/services";
import { Component } from "engine/shared/component/Component";
import { ComponentInstance } from "engine/shared/component/ComponentInstance";
import { Objects } from "engine/shared/fixes/Objects";
import { BlockManager } from "shared/building/BlockManager";
import { SharedBuilding } from "shared/building/SharedBuilding";
import type { BuildingPlot } from "shared/building/BuildingPlot";

/** {@link PlotWelder} that automatically subscribes to {@link BuildingPlot} block changing */
@injectable
export class AutoPlotWelder extends Component {
	constructor(plot: BuildingPlot, @inject blockList: BlockList) {
		super();

		const welder = this.parent(new PlotWelder(plot, blockList));

		this.event.subscribe(plot.placeOperation.executed, (arg, result) => welder.weldOnPlot(result.model));
		// this.event.subscribe(plot.multiPlaceOperation.executed, (arg, result) => {
		// 	for (const model of result.models) {
		// 		welder.weldOnPlot(model);
		// 	}
		// });
		this.event.subscribe(plot.deleteOperation.executed, (arg, result) => {
			if (arg === "all") {
				welder.deleteWelds();
			} else {
				for (const block of arg) {
					welder.unweld(block);
					welder.deleteWeld(block);
				}
			}
		});

		this.event.subscribe(plot.editOperation.executing, (arg) => {
			for (const edit of arg) {
				welder.unweldFromOtherBlocks(edit.instance);
			}
		});
		this.event.subscribe(plot.editOperation.executed, (arg) => {
			for (const edit of arg) {
				welder.moveCollisions(edit.instance, edit.instance.GetPivot());
			}
		});
	}
}

@injectable
export class PlotWelder extends Component {
	readonly collidersParent: WorldModel;
	private readonly weldCache = new Map<BasePart, WeldConstraint[]>();

	constructor(
		private readonly plot: BuildingPlot,
		@inject private readonly blockList: BlockList,
	) {
		super();

		this.collidersParent = new Instance("WorldModel");
		this.collidersParent.Name = "PlotCollision";
		this.collidersParent.Parent = RunService.IsClient() ? Workspace : ServerStorage;

		ComponentInstance.init(this, this.collidersParent);
	}

	deleteWelds() {
		this.collidersParent.ClearAllChildren();
	}
	deleteWeld(block: BlockModel) {
		this.collidersParent.FindFirstChild(BlockManager.manager.uuid.get(block))?.Destroy();
	}

	private scaleWeldColliders(block: Model, originalModel: Model, scale: Vector3 | undefined) {
		scale ??= Vector3.one;

		const origModelCenter = originalModel.GetPivot();
		const blockCenter = block.GetPivot();

		const update = (instance: Instance, origInstance: Instance) => {
			// assuming no duplicate instance names on a single layer

			for (const origChild of origInstance.GetChildren()) {
				const name = origChild.Name;
				const child = instance.WaitForChild(name);

				if (child.IsA("BasePart") && origChild.IsA("BasePart")) {
					child.Size = origChild.Size.mul(origChild.CFrame.Rotation.Inverse().mul(scale).Abs());

					const offset = origModelCenter.ToObjectSpace(origChild.CFrame);
					child.Position = blockCenter.ToWorldSpace(new CFrame(offset.Position.mul(scale))).Position;
				}

				update(child, origChild);
			}
		};

		update(block, originalModel);
	}

	weldOnPlot(block: BlockModel) {
		const id = BlockManager.manager.id.get(block);
		if (!id) return;

		const originalCollider = this.blockList.blocks[id]?.weldRegions;
		if (!originalCollider) return;

		const collider = originalCollider.Clone();

		collider.Name = BlockManager.manager.uuid.get(block);
		collider.PivotTo(block.GetPivot());
		this.scaleWeldColliders(collider, originalCollider, BlockManager.manager.scale.get(block));

		collider.Parent = this.collidersParent;

		this.weld(collider, block);
	}

	private weld(colliders: Model, block: BlockModel) {
		const getTarget = (collider: BasePart): BasePart | undefined => {
			const targetBlock = this.plot.getBlock(collider.Parent!.Name as BlockUuid);
			let part: BasePart | undefined;

			const targetstr = collider.GetAttribute("target") as string | undefined;
			if (targetstr !== undefined) {
				part = (targetBlock as unknown as Record<string, BasePart>)[targetstr];
			}

			return (
				part ??
				(targetBlock.GetChildren().find((c) => c.Name.lower() !== "colbox" && c.IsA("BasePart")) as
					| BasePart
					| undefined) ??
				targetBlock.PrimaryPart
			);
		};

		const weldedTo = new Set<BasePart>();
		for (const collider of colliders.GetChildren()) {
			if (!collider.IsA("BasePart")) {
				$err("Found a non-BasePart in a collider!!!");
				continue;
			}

			const touchingWith = (colliders.Parent as WorldRoot).GetPartsInPart(collider);
			if (touchingWith.size() === 0) continue;

			const targetPart = getTarget(collider);
			if (!targetPart) continue;

			for (const anotherCollider of touchingWith) {
				const anotherTargetPart = getTarget(anotherCollider);
				if (!anotherTargetPart) continue;

				if (weldedTo.has(anotherTargetPart)) {
					continue;
				}
				weldedTo.add(anotherTargetPart);

				this.makeJoints(targetPart, anotherTargetPart);
			}
		}

		const weldData = BlockManager.manager.welds.get(block);
		if (weldData) {
			SharedBuilding.applyWelds(block, this.plot, weldData);
		}

		for (const welded of weldedTo) {
			const otherBlock = BlockManager.tryGetBlockModelByPart(welded);
			if (!otherBlock) continue;

			const weldData = BlockManager.manager.welds.get(otherBlock);
			if (weldData) {
				SharedBuilding.applyWelds(otherBlock, this.plot, weldData);
			}
		}
	}

	moveCollisions(block: BlockModel, newpivot: CFrame) {
		const child = this.collidersParent.FindFirstChild(BlockManager.manager.uuid.get(block)) as Model | undefined;
		if (!child) throw "what";

		this.unweldFromOtherBlocks(block);
		child.PivotTo(newpivot);
		this.scaleWeldColliders(
			child,
			this.blockList.blocks[BlockManager.manager.id.get(block)]!.weldRegions,
			BlockManager.manager.scale.get(block),
		);

		this.weld(child, block);
	}

	private makeJoints(part0: BasePart, part1: BasePart) {
		const weld = new Instance("WeldConstraint");

		weld.Part0 = part0;
		weld.Part1 = part1;
		weld.Name = "AutoWeld";

		weld.Parent = part0;

		const c0 = this.weldCache.get(part0);
		if (!c0) {
			this.weldCache.set(part0, [weld]);
			part0.Destroying.Connect(() => this.weldCache.delete(part0));
		} else {
			this.weldCache.set(part0, [...c0, weld]);
		}

		const c1 = this.weldCache.get(part1);
		if (!c1) {
			this.weldCache.set(part1, [weld]);
			part0.Destroying.Connect(() => this.weldCache.delete(part0));
		} else {
			this.weldCache.set(part1, [...c1, weld]);
		}

		weld.Destroying.Connect(() => {
			const c0 = this.weldCache.get(part0);
			if (c0) this.weldCache.set(part0, c0.except([weld]));

			const c1 = this.weldCache.get(part1);
			if (c1) this.weldCache.set(part1, c1.except([weld]));
		});
	}

	unweld(model: BlockModel): Set<BasePart> {
		const connected = new Set<BasePart>();

		const modelParts = model.GetChildren().filter((value) => value.IsA("BasePart") && value.CanCollide);
		for (let i = 0; i < modelParts.size(); i++) {
			const modelPart = modelParts[i] as BasePart;
			const welds = modelPart.GetJoints();
			welds.forEach((element) => {
				if (element.IsA("Constraint")) {
					if (element.Attachment0?.Parent?.IsA("BasePart")) {
						connected.add(element.Attachment0.Parent);
					}
					if (element.Attachment1?.Parent?.IsA("BasePart")) {
						connected.add(element.Attachment1.Parent);
					}
				} else {
					if (element.Part0) connected.add(element.Part0);
					if (element.Part1) connected.add(element.Part1);
				}

				element.Destroy();
			});
		}

		return connected;
	}

	getWeldsToOtherBlocks(model: BlockModel): Set<WeldConstraint> {
		const result = new Set<WeldConstraint>();

		const modelParts = model
			.GetChildren()
			.filter((value) => value.IsA("BasePart") && value.Name.lower() !== "colbox");
		for (let i = 0; i < modelParts.size(); i++) {
			const modelPart = modelParts[i] as BasePart;
			const welds = this.weldCache.get(modelPart) ?? Objects.empty;
			for (const weld of welds) {
				if ((weld.Part0?.IsDescendantOf(model) ?? true) && (weld.Part1?.IsDescendantOf(model) ?? true)) {
					continue;
				}

				result.add(weld);
			}
		}

		return result;
	}
	unweldFromOtherBlocks(model: BlockModel): void {
		for (const weld of this.getWeldsToOtherBlocks(model)) {
			weld.Destroy();
		}
	}
}


import { RunService, Workspace } from "@rbxts/services";
import { InstanceComponent } from "engine/shared/component/InstanceComponent";
import { Signal } from "engine/shared/event/Signal";
import { BB } from "engine/shared/fixes/BB";
import { BlockManager } from "shared/building/BlockManager";

const getPlotBuildingRegion = (plot: PlotModel): BB => {
	const heightLimit = 400;
	const buildingPlane = plot.BuildingArea;

	return new BB(
		buildingPlane.GetPivot().add(new Vector3(0, heightLimit / 2, 0)),
		buildingPlane.Size.add(new Vector3(0, heightLimit, 0)).add(new Vector3(0.2, 0.2, 0.2)),
	);
};

export class SharedPlot extends InstanceComponent<PlotModel> {
	/** @client */
	private static readonly _anyChanged = new Signal<(plot: SharedPlot) => void>();
	/** @client */
	static readonly anyChanged = this._anyChanged.asReadonly();

	/** @client */
	readonly changed = new Signal();

	readonly version;
	readonly ownerId;
	readonly blacklistedPlayers;
	readonly isolationMode;
	readonly bounds: BB;

	readonly origin: CFrame;
	readonly boundingBox: BB;

	constructor(instance: PlotModel) {
		super(instance);

		this.changed.Connect(() => SharedPlot._anyChanged.Fire(this));

		this.version = this.event.observableFromAttribute<number>(instance, "version");
		if (RunService.IsClient()) {
			this.version.subscribe(() => this.changed.Fire());
		}

		this.ownerId = this.event.observableFromAttribute<number>(instance, "ownerid");
		this.blacklistedPlayers = this.event.observableFromAttributeJson<readonly number[]>(instance, "blacklisted");
		this.isolationMode = this.event.observableFromAttribute<boolean>(instance, "isolation");
		this.bounds = getPlotBuildingRegion(instance);
		this.origin = this.getCenter();
		this.boundingBox = this.bounds;
	}

	getCenter() {
		return this.instance.BuildingArea.GetPivot();
	}

	/** @deprecated TOBEDELETED */
	getBlockDatas(): readonly PlacedBlockData[] {
		return this.getBlocks().map(BlockManager.getBlockDataByBlockModel);
	}
	/** @deprecated TOBEDELETED */
	getBlocks(): readonly BlockModel[] {
		return this.instance.WaitForChild("Blocks").GetChildren() as BlockModel[];
	}
	getBlock(uuid: BlockUuid): BlockModel {
		return (this.instance.WaitForChild("Blocks") as unknown as Record<BlockUuid, BlockModel>)[uuid];
	}
	tryGetBlock(uuid: BlockUuid): BlockModel | undefined {
		return this.instance.WaitForChild("Blocks").FindFirstChild(uuid) as BlockModel | undefined;
	}
	getSpawnCFrame() {
		return new CFrame(this.getSpawnPosition()).mul(CFrame.Angles(0, math.rad(90), 0));
	}
	getSpawnPosition() {
		return this.instance.BuildingArea.GetPivot().Position.add(
			new Vector3(this.instance.BuildingArea.ExtentsSize.X / 2 + 2, 10, 0),
		);
	}

	/** Is the provided `Instance` is a plot model */
	static isPlot(model: Instance | undefined): model is PlotModel {
		return model?.Parent === Workspace.FindFirstChild("Plots");
	}

	/** Is the provided instance a block on this plot */
	hasBlock(instance: BlockModel | BasePart): boolean {
		if (!instance) {
			return false;
		}

		const fastcheck = instance.IsA("Model") ? instance.Parent?.Parent : undefined;
		if (fastcheck && SharedPlot.isPlot(fastcheck)) {
			return true;
		}

		let parent = instance.Parent;
		while (parent) {
			if (SharedPlot.isPlot(parent)) {
				return true;
			}

			parent = parent.Parent;
		}

		return false;
	}

	/** Is player allowed to build on this plot */
	isBuildingAllowed(playerId: number): boolean {
		return this.ownerId.get() === playerId;
	}

	isBlacklisted(player: Player): boolean {
		return this.isolationMode.get() === true || this.blacklistedPlayers.get()?.includes(player.UserId) === true;
	}
}


import { Workspace } from "@rbxts/services";
import { SharedPlot } from "shared/building/SharedPlot";

const plotsFolder = Workspace.WaitForChild("Plots");

const count = plotsFolder.GetAttribute("count") as number | undefined;
if (plotsFolder.GetChildren().size() !== count) {
	print(
		`!! Target plot count ${count} is not equal to loaded plot count ${plotsFolder.GetChildren().size()}, waiting...`,
	);
}
while (plotsFolder.GetChildren().size() !== count) {
	task.wait();
}
for (const plot of plotsFolder.GetChildren()) {
	plot.WaitForChild("BuildingArea");
}

/** Reading the plots data */
export class SharedPlots {
	static instance: SharedPlots;

	static initialize() {
		const plots: readonly SharedPlot[] = (plotsFolder.GetChildren() as unknown as PlotModel[])
			.map((p) => new SharedPlot(p).with((c) => c.enable()))
			.sort((left, right) => left.instance.Name < right.instance.Name);
		const plotComponents: ReadonlyMap<PlotModel, SharedPlot> = new Map(plots.map((p) => [p.instance, p]));

		return (this.instance = new SharedPlots(plots, plotComponents));
	}

	constructor(
		readonly plots: readonly SharedPlot[],
		private readonly plotComponents: ReadonlyMap<PlotModel, SharedPlot>,
	) {}

	waitForPlot(userid: number) {
		while (!this.tryGetPlotByOwnerID(userid)) {
			task.wait(0.2);
		}

		return this.getPlotComponentByOwnerID(userid);
	}

	/** Checks if player is allowed to build on the prodived plot */
	isBuildingAllowed(plot: PlotModel, playerId: number): boolean {
		return this.plotComponents.get(plot)!.isBuildingAllowed(playerId);
	}

	/** Returns the player owned plot, if exists */
	tryGetPlotByOwnerID(ownerID: number): SharedPlot | undefined {
		for (const plot of this.plots) {
			if (plot.ownerId.get() === ownerID) {
				return plot;
			}
		}

		return undefined;
	}
	/** Returns the player owned plot, if exists */
	static staticTryGetPlotByOwnerID(ownerID: number): PlotModel | undefined {
		for (const plot of plotsFolder.GetChildren() as unknown as PlotModel[]) {
			if ((plot.GetAttribute("ownerid") as number) === ownerID) {
				return plot;
			}
		}

		return undefined;
	}

	/** Returns the plot component by the plot model */
	getPlotComponent(plot: PlotModel): SharedPlot {
		return this.plotComponents.get(plot)!;
	}

	/** Returns the player owned plot */
	getPlotComponentByOwnerID(ownerID: number): SharedPlot {
		const plot = this.tryGetPlotByOwnerID(ownerID);
		if (!plot) throw `Player ${ownerID} does not have a plot`;

		return plot;
	}

	/** Returns the player owned plot */
	getPlotByOwnerID(ownerID: number): PlotModel {
		return this.getPlotComponentByOwnerID(ownerID).instance;
	}
}


import { Debris, ReplicatedStorage } from "@rbxts/services";
import { EffectBase } from "shared/effects/EffectBase";
import type { EffectCreator } from "shared/effects/EffectBase";

ReplicatedStorage.WaitForChild("Assets");

type Args = {
	readonly part: BasePart;
	readonly index?: number;
};
@injectable
export class ExplosionEffect extends EffectBase<Args> {
	readonly soundsFolder = ReplicatedStorage.Assets.Sounds.Explosion.GetChildren();

	constructor(@inject creator: EffectCreator) {
		super(creator, "explosion_effect");
	}

	override justRun({ part, index }: Args): void {
		if (!part) return;

		const soundIndex = index ?? math.random(0, this.soundsFolder.size() - 1);
		const sound = this.soundsFolder[soundIndex].Clone() as Sound;

		sound.Parent = part;
		sound.Play();

		this.playVisualEffect(part);

		Debris.AddItem(sound, sound.TimeLength);
	}

	private playVisualEffect(part: BasePart): void {
		ReplicatedStorage.Assets.Effects.Explosion.GetChildren().forEach((effect) => {
			task.spawn(() => {
				const instance = effect.Clone() as ParticleEmitter;
				instance.Parent = part;
				instance.Enabled = true;
				task.wait(0.1);
				instance.Enabled = false;
			});
		});
	}
}


import { Debris, ReplicatedStorage } from "@rbxts/services";
import { EffectBase } from "shared/effects/EffectBase";
import type { EffectCreator } from "shared/effects/EffectBase";

type Args = {
	readonly part: BasePart;
	readonly duration?: number;
};
@injectable
export class FireEffect extends EffectBase<Args> {
	constructor(@inject creator: EffectCreator) {
		super(creator, "effect_fire");
	}

	override justRun({ part, duration }: Args): void {
		if (!part) return;

		const effects = ReplicatedStorage.Assets.Effects.Fire.GetChildren();
		effects.forEach((value) => {
			const obj = value.Clone();
			obj.Parent = part;

			if (obj.IsA("Sound")) {
				if (math.random(1, 4) === 1) {
					obj.Play();
				} else {
					obj.Destroy();
					return;
				}
			}

			// Delete effect
			Debris.AddItem(obj, duration);
		});
	}
}


import { Debris, ReplicatedStorage, Workspace } from "@rbxts/services";
import { EffectBase } from "shared/effects/EffectBase";
import { Sound } from "shared/Sound";
import type { EffectCreator } from "shared/effects/EffectBase";

ReplicatedStorage.WaitForChild("Assets");

type Args = {
	readonly blocks: readonly BasePart[];
	readonly index?: number;
};
@injectable
export class ImpactSoundEffect extends EffectBase<Args> {
	private readonly materialSounds: { readonly [key: string]: Instance[] } = {
		Default: ReplicatedStorage.Assets.Sounds.Impact.Materials.Metal.GetChildren(),

		Metal: ReplicatedStorage.Assets.Sounds.Impact.Materials.Metal.GetChildren(),
		Glass: ReplicatedStorage.Assets.Sounds.Impact.Materials.Glass.GetChildren(),
		Wood: ReplicatedStorage.Assets.Sounds.Impact.Materials.Wood.GetChildren(),
		WoodPlanks: ReplicatedStorage.Assets.Sounds.Impact.Materials.Wood.GetChildren(),
	};

	constructor(@inject creator: EffectCreator) {
		super(creator, "impact_sound_effect");
	}

	override justRun({ blocks, index }: Args): void {
		if (!blocks || blocks.size() === 0) return;

		for (const part of blocks) {
			if (!part.IsDescendantOf(Workspace)) continue;

			const soundsFolder = this.materialSounds[part.Material.Name] ?? this.materialSounds["Default"];
			const soundIndex = index ?? math.random(0, soundsFolder.size() - 1);
			const sound = soundsFolder[soundIndex].Clone() as Sound;

			sound.RollOffMaxDistance = 1000;
			sound.Volume = Sound.getWorldVolume(part.GetPivot().Y) * 0.5;
			sound.Parent = part;
			sound.Play();

			Debris.AddItem(sound, sound.TimeLength);
		}
	}
}


import { Debris, ReplicatedStorage } from "@rbxts/services";
import { EffectBase } from "shared/effects/EffectBase";
import type { EffectCreator } from "shared/effects/EffectBase";

type Args = {
	readonly part: BasePart;
};
@injectable
export class SparksEffect extends EffectBase<Args> {
	constructor(@inject creator: EffectCreator) {
		super(creator, "sparks_effect");
	}

	override justRun({ part }: Args): void {
		if (!part) return;

		const sparks = ReplicatedStorage.Assets.Effects.Sparks.Clone();
		sparks.Parent = part;

		// Delete effect
		Debris.AddItem(sparks, 1.5);
	}
}