Lego Destruction System (LDS)

Created by Samtyy (Aka Armahan, Prymitif, Durkheim)
Not Creatable ModuleScript

The Lego Destruction System (LDS) is a highly optimized, voxel-based destruction engine designed for Roblox. It dynamically processes CSG (Constructive Solid Geometry) subtractions to create satisfying, blocky craters on any BasePart, while instantly generating physics-simulated debris that inherits the exact textures and materials of the destroyed object.

Performance First: To guarantee zero server lag, LDS relies on a strict Server-Client architecture. The Server only handles the math and the CSG, while the Client handles all visual particles, sound effects, and physical debris rendering.

Setup & Installation

Proper hierarchy is required for the system to function. Follow this structure carefully:

  1. Create a Folder named Breakable directly inside the Workspace. All parts you want to be destructible must reside in this folder.
  2. Place the LDS Server Module inside ReplicatedStorage.Assets.Modules.Server.LDS.
  3. Place the LDS Client LocalScript inside StarterPlayer.StarterPlayerScripts.
  4. Create a standard Server Script in ServerScriptService to initialize the engine (see Example Usage).

Once .Start() is called, LDS will automatically generate a temporary Remotes folder in ReplicatedStorage if you haven't set up the paths yet.

System Architecture

Understanding how LDS splits its workload is crucial for modifying or extending it:

Server API

Interact with LDS using the Server Module.

DestructionServer.Start()

Server

Initializes the Destruction Engine. Validates the existence of the Breakable folder and establishes OnServerEvent connections for client impact requests.

local DestructionModule = require(ReplicatedStorage.Assets.Modules.Server.LDS)
DestructionModule.Start()

DestructionServer.ProcessImpact(hitPoint, customParams, player)

Server

Programmatically triggers an explosion at a specific world position. This is the core function you will call from your weapons or spells.

Parameters

Name Type Description
hitPoint Vector3 The exact world position where the explosion occurs.
customParams table (optional) A dictionary containing ImpactRadius, HoleVoidRadius, and ImpactForce. If nil, defaults are used.
player Player (optional) The player who caused the impact. Passed to clients so the local player doesn't trigger redundant visuals.

Part Attributes

You can fine-tune how specific walls or objects react to explosions by assigning these attributes directly to the BaseParts inside the Breakable folder.

Attribute Name Value Type Behavior
Unbreakable boolean If true, the part becomes immune to all LDS explosions. Ideal for map boundaries or core structural pillars.
Undividable boolean If true, the part will not be carved by CSG. Instead, taking any impact will cause it to instantly shatter into raw debris entirely.

Client Configuration

The visual behavior of the debris can be configured at the top of the Client Script. Tweak these constants to fit your game's aesthetic:

Example Usage

Here is an example of a Server Script (placed in ServerScriptService) that initializes LDS and listens for a weapon firing.

--// Variables & Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DestructionModule = require(ReplicatedStorage.Assets.Modules.Server.LDS)

--// 1. Initialize the Engine
DestructionModule.Start()

local Remote = ReplicatedStorage.Assets.Signals.Remotes.LDS_Destruction

--// 2. Listen for Weapon Impacts
Remote.OnServerEvent:Connect(function(player, hitPoint, attackType)

    if attackType == "Sniper" then
        -- Small, precise crater
        DestructionModule.ProcessImpact(hitPoint, {
            ImpactRadius = 3,
            HoleVoidRadius = 2,
            ImpactForce = 100
        }, player)

    elseif attackType == "Rocket" then
        -- Massive destruction
        DestructionModule.ProcessImpact(hitPoint, {
            ImpactRadius = 12,
            HoleVoidRadius = 6,
            ImpactForce = 80
        }, player)
    end
end)

Source Code

Server Module (LDS)

Place in ReplicatedStorage.Assets.Modules.Server.LDS

--// Section: Initialization
local DestructionServer = {}

DestructionServer.Settings = {
	Autorun = false,
	AutorunFunction = nil
}

--// Section: Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")

--// Section: Variables
local DEFAULT_CONFIG = {
	ImpactRadius = 7,
	HoleVoidRadius = 3,
	ImpactForce = 65
}

local MAX_IMPACT_RADIUS = 12
local MAX_CSG_CUTTERS = 2000

local LEGO_PRIORITIES = {
	{Vector3.new(4, 1, 2), Vector3.new(2, 1, 4), Vector3.new(4, 2, 1), Vector3.new(1, 2, 4)},
	{Vector3.new(2, 1, 2), Vector3.new(2, 2, 1), Vector3.new(1, 2, 2)},
	{Vector3.new(2, 1, 1), Vector3.new(1, 1, 2), Vector3.new(1, 2, 1)},
	{Vector3.new(1, 1, 1)},
	{Vector3.new(1, 0.4, 1), Vector3.new(1, 1, 0.4), Vector3.new(0.4, 1, 1)}
}

_G.ActiveDebrisTable = {}

local RemoteEvent
local BreakableFolder
local isRunning = false

--// Section: Utility Functions
local function copyVisuals(originalPart, newPart)
	for _, child in ipairs(originalPart:GetChildren()) do
		if child:IsA("Texture") or child:IsA("Decal") or child:IsA("SurfaceAppearance") then
			child:Clone().Parent = newPart
		end
	end
end

local function generateDebrisData(originalPart, spawnCFrame, hitPoint, dist, size, debrisList, force)
	local id = "Debris_" .. HttpService:GenerateGUID(false)
	local dir = (spawnCFrame.Position - hitPoint).Unit

	if dir.Magnitude ~= dir.Magnitude then dir = Vector3.new(0, 1, 0) end

	local forceMultiplier = force / 35
	local spread = Vector3.new(math.random(-15, 15) / 10, math.random(5, 20) / 10, math.random(-15, 15) / 10) * forceMultiplier
	local finalVel = (dir + spread).Unit * (force * (0.8 + math.random() * 0.5))
	local angVel = Vector3.new(math.random(-50, 50), math.random(-50, 50), math.random(-50, 50)) * forceMultiplier

	local textures = {}
	for _, child in ipairs(originalPart:GetChildren()) do
		if child:IsA("Texture") or child:IsA("Decal") then
			table.insert(textures, {
				Class = child.ClassName,
				Texture = child.Texture,
				Face = child.Face,
				Color3 = child.Color3,
				Transparency = child.Transparency,
				StudsPerTileU = child:IsA("Texture") and child.StudsPerTileU or nil,
				StudsPerTileV = child:IsA("Texture") and child.StudsPerTileV or nil
			})
		end
	end

	local data = {
		ID = id,
		Size = size,
		CFrame = spawnCFrame,
		Color = originalPart.Color,
		Material = originalPart.Material,
		TopS = originalPart.TopSurface,
		BotS = originalPart.BottomSurface,
		LeftS = originalPart.LeftSurface,
		RightS = originalPart.RightSurface,
		FrontS = originalPart.FrontSurface,
		BackS = originalPart.BackSurface,
		Velocity = finalVel,
		AngularVelocity = angVel,
		Visuals = textures
	}

	_G.ActiveDebrisTable[id] = data
	table.insert(debrisList, data)

	task.delay(15, function()
		if _G.ActiveDebrisTable[id] then
			_G.ActiveDebrisTable[id] = nil
		end
	end)
end

local function getRandomLegoSize()
	local priorityGroup = LEGO_PRIORITIES[math.random(1, 4)]
	return priorityGroup[math.random(1, #priorityGroup)]
end

local function checkFloating(part)
	task.spawn(function()
		task.wait(0.1)
		if not part or not part.Parent or not part.Anchored then return end

		local overlapParams = OverlapParams.new()
		overlapParams.FilterType = Enum.RaycastFilterType.Exclude
		overlapParams.FilterDescendantsInstances = {part}

		local touching = Workspace:GetPartsInPart(part, overlapParams)
		local isSupported = false

		for _, tPart in ipairs(touching) do
			if tPart:IsA("Terrain") or (tPart.Anchored and tPart.Name ~= "Debris") then
				isSupported = true
				break
			end
		end

		if not isSupported then
			part.Anchored = false
		end
	end)
end

--// Section: Core Logic
local function calculateVoxelCutters(part, hitPoint, debrisList, params)
	local radius = math.clamp(params.ImpactRadius or DEFAULT_CONFIG.ImpactRadius, 1, MAX_IMPACT_RADIUS)
	local force = params.ImpactForce or DEFAULT_CONFIG.ImpactForce

	if part:GetAttribute("Undividable") or part.Size.Magnitude < (radius * 1.5) then
		local numDebris = math.random(8, 15)
		for _ = 1, numDebris do
			local offset = Vector3.new(math.random(-2, 2), math.random(-2, 2), math.random(-2, 2))
			generateDebrisData(part, part.CFrame * CFrame.new(offset), hitPoint, 0, getRandomLegoSize(), debrisList, force)
		end
		part:Destroy()
		return nil
	end

	local impactDir = (part.Position - hitPoint).Unit
	if impactDir.Magnitude ~= impactDir.Magnitude then impactDir = Vector3.new(0, 1, 0) end

	local cutters = {}
	local localHit = part.CFrame:PointToObjectSpace(hitPoint)
	local localImpactDir = part.CFrame:VectorToObjectSpace(impactDir)
	local craterCenter = localHit + (localImpactDir * (radius * 0.3))

	craterCenter = Vector3.new(
		math.floor(craterCenter.X / 2 + 0.5) * 2,
		math.floor(craterCenter.Y / 2 + 0.5) * 2,
		math.floor(craterCenter.Z / 2 + 0.5) * 2
	)

	local step = 2
	local R = math.ceil(radius)
	if R % 2 ~= 0 then R = R + 1 end

	local usedPositions = {}

	for x = -R, R, step do
		for y = -R, R, step do
			for z = -R, R, step do
				local dist = Vector3.new(x, y, z).Magnitude
				local noise = math.random(-15, 15) / 10
				local effectiveDist = dist + noise

				if effectiveDist <= radius * 0.9 then
					local snapX = craterCenter.X + x
					local snapY = craterCenter.Y + y
					local snapZ = craterCenter.Z + z

					local size
					if effectiveDist < radius * 0.4 then
						size = Vector3.new(4, 4, 4)
					else
						local choices = {Vector3.new(4, 2, 2), Vector3.new(2, 2, 4), Vector3.new(2, 4, 2), Vector3.new(2, 2, 2)}
						size = choices[math.random(1, #choices)]
					end

					local posKey = snapX .. "_" .. snapY .. "_" .. snapZ

					if not usedPositions[posKey] then
						usedPositions[posKey] = true

						local cutter = Instance.new("Part")
						cutter.Shape = Enum.PartType.Block
						cutter.Size = size
						cutter.CFrame = part.CFrame * CFrame.new(snapX, snapY, snapZ)
						cutter.Anchored = true
						cutter.CanCollide = false
						cutter.Transparency = 1

						table.insert(cutters, cutter)

						if math.random() < 0.45 then
							generateDebrisData(part, cutter.CFrame, hitPoint, (cutter.Position - hitPoint).Magnitude, getRandomLegoSize(), debrisList, force)
						end
					end
				end
			end
			if #cutters >= MAX_CSG_CUTTERS then break end
		end
		if #cutters >= MAX_CSG_CUTTERS then break end
	end

	return cutters
end

local function executeCSG(part, cutters, hitPoint)
	local isAutoUnanchor = part.Anchored and part:GetAttribute("AutoUnanchor")
	local partCFrame = part.CFrame
	local localHit = partCFrame:PointToObjectSpace(hitPoint)

	if isAutoUnanchor then
		local size = part.Size
		local longestAxis = "Y"

		if size.X >= size.Y and size.X >= size.Z then longestAxis = "X" end
		if size.Z >= size.X and size.Z >= size.Y then longestAxis = "Z" end

		local topCutter = Instance.new("Part")
		topCutter.Size = Vector3.new(2000, 2000, 2000)

		local bottomCutter = Instance.new("Part")
		bottomCutter.Size = Vector3.new(2000, 2000, 2000)

		if longestAxis == "Y" then
			topCutter.CFrame = partCFrame * CFrame.new(0, localHit.Y + 1000, 0)
			bottomCutter.CFrame = partCFrame * CFrame.new(0, localHit.Y - 1000, 0)
		elseif longestAxis == "X" then
			topCutter.CFrame = partCFrame * CFrame.new(localHit.X + 1000, 0, 0)
			bottomCutter.CFrame = partCFrame * CFrame.new(localHit.X - 1000, 0, 0)
		else
			topCutter.CFrame = partCFrame * CFrame.new(0, 0, localHit.Z + 1000)
			bottomCutter.CFrame = partCFrame * CFrame.new(0, 0, localHit.Z - 1000)
		end

		topCutter.Anchored = true
		bottomCutter.Anchored = true

		local bottomPieceCutters = {topCutter}
		for _, c in ipairs(cutters) do table.insert(bottomPieceCutters, c) end

		local topPieceCutters = {bottomCutter}
		for _, c in ipairs(cutters) do table.insert(topPieceCutters, c) end

		local bottomSuccess, bottomResult = pcall(function()
			return part:SubtractAsync(bottomPieceCutters, Enum.CollisionFidelity.PreciseConvexDecomposition, Enum.RenderFidelity.Automatic)
		end)

		local topSuccess, topResult = pcall(function()
			return part:SubtractAsync(topPieceCutters, Enum.CollisionFidelity.PreciseConvexDecomposition, Enum.RenderFidelity.Automatic)
		end)

		topCutter:Destroy()
		bottomCutter:Destroy()
		for _, c in ipairs(cutters) do c:Destroy() end

		local sliced = false

		if bottomSuccess and bottomResult then
			bottomResult.UsePartColor = true
			bottomResult.Parent = BreakableFolder
			copyVisuals(part, bottomResult)
			bottomResult:SetAttribute("AutoUnanchor", true)
			checkFloating(bottomResult)
			sliced = true
		end

		if topSuccess and topResult then
			topResult.UsePartColor = true
			topResult.Parent = BreakableFolder
			copyVisuals(part, topResult)
			topResult:SetAttribute("AutoUnanchor", true)
			checkFloating(topResult)
			sliced = true
		end

		if sliced then
			part:Destroy()
			return
		end
	end

	local success, result = pcall(function()
		return part:SubtractAsync(cutters, Enum.CollisionFidelity.PreciseConvexDecomposition, Enum.RenderFidelity.Automatic)
	end)

	for _, c in ipairs(cutters) do
		c:Destroy()
	end

	if success and result then
		result.UsePartColor = true
		result.Parent = BreakableFolder
		copyVisuals(part, result)

		if isAutoUnanchor then
			result:SetAttribute("AutoUnanchor", true)
			checkFloating(result)
		end

		part:Destroy()
	end
end

--// Section: API
function DestructionServer.ProcessImpact(hitPoint, customParams, player)
	local params = customParams or DEFAULT_CONFIG
	local overlapParams = OverlapParams.new()
	overlapParams.FilterType = Enum.RaycastFilterType.Include
	overlapParams.FilterDescendantsInstances = {BreakableFolder}

	local searchRadius = math.clamp(params.ImpactRadius or DEFAULT_CONFIG.ImpactRadius, 1, MAX_IMPACT_RADIUS)
	local rawParts = Workspace:GetPartBoundsInRadius(hitPoint, searchRadius, overlapParams)

	local validParts = {}
	for _, part in ipairs(rawParts) do
		if part:IsA("BasePart") and not part:GetAttribute("Unbreakable") then
			table.insert(validParts, part)
		end
	end

	if #validParts == 0 then return end

	local impactDebrisList = {}
	local pendingCSG = {}

	for _, part in ipairs(validParts) do
		local cutters = calculateVoxelCutters(part, hitPoint, impactDebrisList, params)
		if cutters and #cutters > 0 then
			table.insert(pendingCSG, {Part = part, Cutters = cutters, HitPoint = hitPoint})
		end
	end

	if RemoteEvent then
		RemoteEvent:FireAllClients("RenderImpact", hitPoint, impactDebrisList, player, params)
	end

	for _, taskData in ipairs(pendingCSG) do
		task.spawn(function()
			executeCSG(taskData.Part, taskData.Cutters, taskData.HitPoint)
		end)
	end
end

--// Section: Networking
local function setupNetworking()
	RemoteEvent.OnServerEvent:Connect(function(player, action, ...)
		local args = {...}

		if action == "RequestImpact" then
			local hitPoint = args[1]
			local customParams = args[2] or DEFAULT_CONFIG

			DestructionServer.ProcessImpact(hitPoint, customParams, player)

		elseif action == "CollectDebris" then
			local debrisID = args[1]

			if _G.ActiveDebrisTable[debrisID] then
				_G.ActiveDebrisTable[debrisID] = nil
				RemoteEvent:FireAllClients("RemoveDebris", debrisID)
			end
		end
	end)

	Players.PlayerAdded:Connect(function(player)
		local existingDebris = {}
		for _, data in pairs(_G.ActiveDebrisTable) do
			table.insert(existingDebris, data)
		end

		if #existingDebris > 0 then
			RemoteEvent:FireClient(player, "SyncExistingDebris", existingDebris)
		end
	end)
end

--// Section: Initialization
function DestructionServer.Start()
	if isRunning then return end
	isRunning = true

	local signalsFolder = ReplicatedStorage:FindFirstChild("Assets") and ReplicatedStorage.Assets:FindFirstChild("Signals") and ReplicatedStorage.Assets.Signals:FindFirstChild("Remotes")

	if not signalsFolder then
		signalsFolder = Instance.new("Folder")
		signalsFolder.Name = "LDS_TempRemotes"
		signalsFolder.Parent = ReplicatedStorage
	end

	RemoteEvent = signalsFolder:FindFirstChild("LDS_Destruction")
	if not RemoteEvent then
		RemoteEvent = Instance.new("RemoteEvent")
		RemoteEvent.Name = "LDS_Destruction"
		RemoteEvent.Parent = signalsFolder
	end

	BreakableFolder = Workspace:FindFirstChild("Breakable")
	if not BreakableFolder then
		BreakableFolder = Instance.new("Folder")
		BreakableFolder.Name = "Breakable"
		BreakableFolder.Parent = Workspace
	end

	setupNetworking()
end

function DestructionServer.Stop()
	isRunning = false
end

return DestructionServer

Client Module

Place in StarterPlayer.StarterPlayerScripts

--// Services
local UserInputService = game:GetService("UserInputService")
local Debris = game:GetService("Debris")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Players = game:GetService("Players")

--// Variables
local LocalPlayer = Players.LocalPlayer
local Camera = Workspace.CurrentCamera

local CameraShake = require(ReplicatedStorage.Assets.Modules.Essentials.CameraShakeUpdated)
local CameraShakePresets = require(ReplicatedStorage.Assets.Modules.Essentials.CameraShakeUpdated:WaitForChild("Presets"))

local Remote = game.ReplicatedStorage.Assets.Signals.Remotes.LDS_Destruction
local BreakableFolder = Workspace:WaitForChild("Breakable")

local ClientDebrisFolder = Instance.new("Folder")
ClientDebrisFolder.Name = "ClientDebris"
ClientDebrisFolder.Parent = Workspace

--// Configurations
local IMPACT_RADIUS = 7
local IMPACT_FORCE = 65
local DEBRIS_LIFETIME = 15
local SUCTION_RADIUS = 10 

local USE_DUST = true 
local USE_SPARKS = true
local FREEZE_FRAME_DURATION = 0.08 
local FREEZE_GRAVITY = 0 

local DEBRIS_SOUND_ID = "rbxassetid://114470768765253"
local VELOCITY_THRESHOLD = 5
local SOUND_COOLDOWN = 0.15

local debrisBeingSucked = {}

--// Audio System
local function bindCollisionSound(debrisPart)
	local lastSoundTime = 0

	local sound = Instance.new("Sound")
	sound.SoundId = DEBRIS_SOUND_ID
	sound.Volume = 0.4
	sound.RollOffMaxDistance = 40
	sound.RollOffMinDistance = 5
	sound.RollOffMode = Enum.RollOffMode.Linear
	sound.Parent = debrisPart

	local volumeFactor = debrisPart.Size.X * debrisPart.Size.Y * debrisPart.Size.Z
	if volumeFactor < 2 then
		sound.PlaybackSpeed = math.random(120, 140) / 100 
	elseif volumeFactor < 6 then
		sound.PlaybackSpeed = math.random(90, 110) / 100  
	else
		sound.PlaybackSpeed = math.random(70, 85) / 100   
	end

	debrisPart.Touched:Connect(function()
		local id = debrisPart:GetAttribute("DebrisID")
		if id and debrisBeingSucked[id] then return end 

		local currentVel = debrisPart.AssemblyLinearVelocity.Magnitude

		if currentVel > VELOCITY_THRESHOLD then
			local currentTime = tick()
			if currentTime - lastSoundTime >= SOUND_COOLDOWN then
				lastSoundTime = currentTime
				sound.Volume = math.clamp(currentVel / 60, 0.1, 0.5) 
				sound.PlaybackSpeed = sound.PlaybackSpeed * (math.random(95, 105) / 100)
				sound:Play()
			end
		end
	end)
end

--// Visual Effects
local function spawnSparks(hitPoint)
	if not USE_SPARKS then return end
	local numSparks = math.random(math.floor(IMPACT_RADIUS * 1.5), math.floor(IMPACT_RADIUS * 3))
	for i = 1, numSparks do
		local spark = Instance.new("Part")
		spark.Size = Vector3.new(0.1, 0.1, math.random(4, 12)/10) 
		spark.Position = hitPoint + (Vector3.new(math.random(-10,10), math.random(-10,10), math.random(-10,10)).Unit * math.random())
		spark.Material = Enum.Material.Neon
		spark.Color = Color3.fromRGB(255, 200, 50) 
		spark.CanCollide = false
		spark.Anchored = false
		spark.Parent = Workspace
		spark.AssemblyLinearVelocity = Vector3.new(math.random(-10,10), math.random(-5,15), math.random(-10,10)).Unit * (IMPACT_FORCE * math.random(1.5, 3.0))
		spark.CFrame = CFrame.lookAt(spark.Position, spark.Position + spark.AssemblyLinearVelocity)
		Debris:AddItem(spark, math.random(5, 12) / 10) 
	end
end

local function spawnDust(hitPoint)
	if not USE_DUST then return end
	local numDust = math.random(math.floor(IMPACT_RADIUS * 3), math.floor(IMPACT_RADIUS * 5))
	for i = 1, numDust do
		local dust = Instance.new("Part")
		dust.Shape = Enum.PartType.Ball
		local baseSize = math.random(1, 3) / 10 
		dust.Size = Vector3.new(baseSize, baseSize, baseSize)
		local spawnSpread = Vector3.new(math.random(-10,10), math.random(-10,10), math.random(-10,10)).Unit * (math.random() * IMPACT_RADIUS * 0.9)
		dust.Position = hitPoint + spawnSpread
		dust.Material = Enum.Material.Sand
		dust.Color = Color3.fromRGB(200, 200, 200)
		dust.Transparency = 0.2 
		dust.CanCollide = false
		dust.Anchored = true 
		dust.Parent = Workspace

		local driftDirection = (spawnSpread.Unit + Vector3.new(math.random(-5,5)/10, math.random(5,15)/10, math.random(-5,5)/10)).Unit
		local driftSpeed = math.random(2, 6) / 100 

		task.spawn(function()
			local fadeTime = math.random(40, 70) / 10 
			local steps = 60 
			local waitTime = fadeTime / steps
			for s = 1, steps do
				task.wait(waitTime)
				if dust and dust.Parent then
					dust.Transparency = dust.Transparency + ((1 - 0.2) / steps)
					dust.CFrame = dust.CFrame + (driftDirection * driftSpeed)
					driftSpeed = driftSpeed * 0.96
				else break end
			end
			if dust and dust.Parent then dust:Destroy() end
		end)
	end
end

local function triggerVisuals(hitPoint)
	local originalGravity = Workspace.Gravity
	Workspace.Gravity = FREEZE_GRAVITY
	task.delay(FREEZE_FRAME_DURATION, function() Workspace.Gravity = originalGravity end)

	local sound = Instance.new("Sound")
	sound.SoundId = "rbxassetid://100674613725056" 
	sound.Volume = 1
	sound.RollOffMaxDistance = 100
	local attachment = Instance.new("Attachment", Workspace.Terrain)
	attachment.WorldPosition = hitPoint
	sound.Parent = attachment
	sound:Play()
	Debris:AddItem(attachment, 4) 

	task.spawn(function()
		if CameraShakePresets and CameraShakePresets["Impact"] then CameraShakePresets["Impact"]() end
	end)

	spawnDust(hitPoint)
	spawnSparks(hitPoint)
end

--// Debris Generation
local function createOfficialDebris(debrisList)
	for _, data in ipairs(debrisList) do
		if ClientDebrisFolder:FindFirstChild(data.ID) then continue end

		local d = Instance.new("Part")
		d.Name = data.ID 
		d.Size = data.Size
		d.CFrame = data.CFrame
		d.Color = data.Color
		d.Material = data.Material
		d.TopSurface, d.BottomSurface = data.TopS, data.BotS
		d.LeftSurface, d.RightSurface = data.LeftS, data.RightS
		d.FrontSurface, d.BackSurface = data.FrontS, data.BackS
		d.Anchored = false
		d.CanCollide = true
		d.Parent = ClientDebrisFolder
		
		if data.Visuals then
			for _, vis in ipairs(data.Visuals) do
				local visualObj = Instance.new(vis.Class)
				visualObj.Texture = vis.Texture
				visualObj.Face = vis.Face
				visualObj.Color3 = vis.Color3
				visualObj.Transparency = vis.Transparency
				if vis.Class == "Texture" then
					visualObj.StudsPerTileU = vis.StudsPerTileU
					visualObj.StudsPerTileV = vis.StudsPerTileV
				end
				visualObj.Parent = d
			end
		end

		d:SetAttribute("DebrisID", data.ID)

		d.AssemblyLinearVelocity = data.Velocity
		d.AssemblyAngularVelocity = data.AngularVelocity

		bindCollisionSound(d)

		task.spawn(function()
			task.wait(DEBRIS_LIFETIME - 1)

			if d and d.Parent then
				d.CanCollide = false
				local shrinkTweenInfo = TweenInfo.new(1, Enum.EasingStyle.Linear)
				local shrinkTween = TweenService:Create(d, shrinkTweenInfo, {
					Size = Vector3.new(0, 0, 0)
				})

				shrinkTween:Play()
				shrinkTween.Completed:Connect(function()
					if d and d.Parent then
						d:Destroy()
					end
				end)
			end
		end)
	end
end

--// Suction System
RunService.Heartbeat:Connect(function()
	local char = LocalPlayer.Character
	if not char or not char:FindFirstChild("HumanoidRootPart") then return end

	local rootPos = char.HumanoidRootPart.Position

	for _, debris in ipairs(ClientDebrisFolder:GetChildren()) do
		local id = debris:GetAttribute("DebrisID")

		if id and not debrisBeingSucked[id] then
			local dist = (debris.Position - rootPos).Magnitude

			if dist <= SUCTION_RADIUS then
				debrisBeingSucked[id] = true

				debris.CanCollide = false
				debris.Anchored = true

				local tweenInfo = TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.In)
				local goalCFrame = CFrame.new(rootPos) * CFrame.Angles(debris.CFrame:ToEulerAnglesXYZ())

				local tween = TweenService:Create(debris, tweenInfo, {
					CFrame = goalCFrame,
					Size = Vector3.new(0, 0, 0) 
				})

				tween:Play()
				tween.Completed:Connect(function()
					if debris and debris.Parent then
						Remote:FireServer("CollectDebris", id)
						debris:Destroy()
					end
				end)
			end
		end
	end
end)

--// Player Inputs
UserInputService.InputBegan:Connect(function(input, processed)
	if processed or input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
	local mousePos = UserInputService:GetMouseLocation()
	local rayParams = RaycastParams.new() 
	rayParams.FilterType = Enum.RaycastFilterType.Include
	rayParams.FilterDescendantsInstances = {BreakableFolder}
	local result = Workspace:Raycast(Camera:ViewportPointToRay(mousePos.X, mousePos.Y).Origin, Camera:ViewportPointToRay(mousePos.X, mousePos.Y).Direction * 1000, rayParams)

	if result then 
		local hitPoint = result.Position
		local hitPart = result.Instance

		if hitPart:GetAttribute("Unbreakable") then return end

		local customParams = {
			ImpactRadius = IMPACT_RADIUS,
			HoleVoidRadius = IMPACT_RADIUS * 0.4,
			ImpactForce = IMPACT_FORCE
		}

		Remote:FireServer("RequestImpact", hitPoint, customParams)
		triggerVisuals(hitPoint)
	end
end)

--// Server Synchronization
Remote.OnClientEvent:Connect(function(action, ...)
	local args = {...}

	if action == "RenderImpact" then
		local hitPoint = args[1]
		local debrisList = args[2]
		local sender = args[3]

		if sender ~= game.Players.LocalPlayer then
			triggerVisuals(hitPoint)
		end
		createOfficialDebris(debrisList)

	elseif action == "SyncExistingDebris" then
		local existingDebrisList = args[1]
		createOfficialDebris(existingDebrisList)

	elseif action == "RemoveDebris" then
		local idToRemove = args[1]
		local debrisObj = ClientDebrisFolder:FindFirstChild(idToRemove)
		if debrisObj then
			debrisObj:Destroy()
		end
	end
end)