CCCPaste

Destruction module


local module = {}

--// Services
local ServerStorage = game:GetService("ServerStorage")
local AdditionModule = ServerStorage:WaitForChild("AdditionalModules")

--// Local references
local workspaceParticlesParent = workspace:FindFirstChild("Particles") or workspace -- fallback

--// Splits `part` into smaller parts. If the part is long/thin (aspect ratio below threshold)
-- it splits only along the two larger axes into 4 sub-parts, otherwise it splits into 8 octants.
-- Returns an array of new parts (the original `part` is destroyed).
function module.SubdivideCube(part, minSize)
	--// Early out: if the part is already small enough, return it as-is.
	local s = part.Size
	if s.X <= minSize and s.Y <= minSize and s.Z <= minSize then
		return { part }
	end

	local cframe = part.CFrame
	local parent = workspaceParticlesParent
	local minDim = math.min(s.X, s.Y, s.Z)
	local maxDim = math.max(s.X, s.Y, s.Z)
	local aspectRatio = minDim / maxDim
	local threshold = 0.5

	local newParts = {}

	--// Helper: create a clone with size/cframe and parent it
	local function makePart(newSize, newCFrame)
		local newPart = part:Clone()
		newPart.Size = newSize
		newPart.CFrame = newCFrame
		newPart.Parent = parent
		return newPart
	end

	if aspectRatio < threshold then
		--// long thin part: split only the two larger axes => produce 4 parts
		local smallestAxis
		if s.X == minDim then
			smallestAxis = "X"
		elseif s.Y == minDim then
			smallestAxis = "Y"
		else
			smallestAxis = "Z"
		end

		--// new size halves the larger axes, but keeps the smallest axis unchanged
		local newSize = Vector3.new(
			(smallestAxis == "X") and s.X or s.X / 2,
			(smallestAxis == "Y") and s.Y or s.Y / 2,
			(smallestAxis == "Z") and s.Z or s.Z / 2
		)

		--// compute offsets depending on which axis stayed full-size
		local offX = (smallestAxis == "X") and 0 or newSize.X / 2
		local offY = (smallestAxis == "Y") and 0 or newSize.Y / 2
		local offZ = (smallestAxis == "Z") and 0 or newSize.Z / 2

		local offsets = {}
		if smallestAxis == "X" then
			offsets = {
				Vector3.new(0, offY, offZ),
				Vector3.new(0, offY, -offZ),
				Vector3.new(0, -offY, offZ),
				Vector3.new(0, -offY, -offZ),
			}
		elseif smallestAxis == "Y" then
			offsets = {
				Vector3.new(offX, 0, offZ),
				Vector3.new(offX, 0, -offZ),
				Vector3.new(-offX, 0, offZ),
				Vector3.new(-offX, 0, -offZ),
			}
		else -- "Z"
			offsets = {
				Vector3.new(offX, offY, 0),
				Vector3.new(offX, -offY, 0),
				Vector3.new(-offX, offY, 0),
				Vector3.new(-offX, -offY, 0),
			}
		end

		for _, offset in ipairs(offsets) do
			local newCFrame = cframe * CFrame.new(offset)
			table.insert(newParts, makePart(newSize, newCFrame))
		end
	else
		--// more cubic: split into 8 octants
		local newSize = Vector3.new(s.X / 2, s.Y / 2, s.Z / 2)
		local half = newSize * 0.5
		local offsets = {
			Vector3.new( half.X,  half.Y,  half.Z),
			Vector3.new(-half.X,  half.Y,  half.Z),
			Vector3.new( half.X, -half.Y,  half.Z),
			Vector3.new(-half.X, -half.Y,  half.Z),
			Vector3.new( half.X,  half.Y, -half.Z),
			Vector3.new(-half.X,  half.Y, -half.Z),
			Vector3.new( half.X, -half.Y, -half.Z),
			Vector3.new(-half.X, -half.Y, -half.Z),
		}
		for _, offset in ipairs(offsets) do
			local newCFrame = cframe * CFrame.new(offset)
			table.insert(newParts, makePart(newSize, newCFrame))
		end
	end

	--// Remove the original part (we replaced it with children)
	part:Destroy()
	return newParts
end

--// Returns true if the axis-aligned box defined by `part` intersects or lies within the sphere.
function module.IsPartInSphere(part, sphereCenter, sphereRadius)
	-- Transform the sphere center into the part's local space
	local localPoint = part.CFrame:PointToObjectSpace(sphereCenter)
	local halfSize = part.Size * 0.5
	local dx = math.max(math.abs(localPoint.X) - halfSize.X, 0)
	local dy = math.max(math.abs(localPoint.Y) - halfSize.Y, 0)
	local dz = math.max(math.abs(localPoint.Z) - halfSize.Z, 0)
	local distance = math.sqrt(dx * dx + dy * dy + dz * dz)
	return distance <= sphereRadius
end

--// Returns a minimum voxel size for subdivision based on the largest dimension of `part`.
function module.CalculateMinVoxelSize(part, fraction)
	fraction = fraction or 0.1
	local s = part.Size
	local maxDim = math.max(s.X, s.Y, s.Z)
	local minVoxelSize = math.max(maxDim * fraction, 1)
	return minVoxelSize
end

--// Calculates how many times to halve the largest dimension until it reaches the min voxel size.
function module.CalculateSubdivisionIterations(part, minVoxelSize)
	local s = part.Size
	local maxDim = math.max(s.X, s.Y, s.Z)
	--// iterations = ceil(log2(maxDim / minVoxelSize))
	local ratio = maxDim / math.max(minVoxelSize, 1e-6)
	local iterations = math.ceil(math.log(ratio) / math.log(2))
	return math.max(iterations, 0)
end

--// Recursively subdivides parts that intersect the sphere, down to minSize or until iterations expire.
function module.RecursiveSubdivision(parts, sphereCenter, sphereRadius, minSize, iterations)
	local newParts = {}
	for _, part in ipairs(parts) do
		-- Only subdivide if the part is larger than minSize (by magnitude) and intersects the sphere
		if part and part.Parent and part.Size.Magnitude > minSize and module.IsPartInSphere(part, sphereCenter, sphereRadius) then
			local subdivided = module.SubdivideCube(part, minSize)
			for _, sp in ipairs(subdivided) do
				table.insert(newParts, sp)
			end
		else
			table.insert(newParts, part)
		end
	end

	if iterations and iterations > 1 then
		return module.RecursiveSubdivision(newParts, sphereCenter, sphereRadius, minSize, iterations - 1)
	else
		return newParts
	end
end

--// Public function: finds map parts intersecting a sphere, subdivides them around the sphere,
-- applies impulses to pieces inside the sphere, and unsubdivides (restores) the others.
function module.CauseDestructionAroundPoint(Point: Vector3, Radius: number)
	--// Helper to gather parts that belong to workspace.Map and intersect the sphere
	local function CalculateHitParts()
		-- Create a temporary invisible part to query GetPartsInPart
		local Temp = Instance.new("Part")
		Temp.Shape = Enum.PartType.Ball
		Temp.Size = Vector3.new(Radius * 2, Radius * 2, Radius * 2) -- Size should be diameter
		Temp.Position = Point
		Temp.Transparency = 1
		Temp.Anchored = true
		Temp.CanCollide = false
		Temp.Parent = workspace

		-- Get parts inside the temp sphere-shaped part
		local initial = workspace:GetPartsInPart(Temp)

		-- Clean up temp immediately
		Temp:Destroy()

		-- Filter to only parts that are within the Map hierarchy (same logic as original)
		local mapParts = {}
		for _, v in ipairs(initial) do
			if v and v:FindFirstAncestor("Map") then
				table.insert(mapParts, v)
			end
		end

		-- If nothing to do, return empty
		if #mapParts == 0 then
			return {}
		end

		-- Find the largest part (by Size.Magnitude) to determine min voxel size & iterations
		local biggestPart = mapParts[1]
		for i = 2, #mapParts do
			local candidate = mapParts[i]
			if candidate and candidate.Size.Magnitude > biggestPart.Size.Magnitude then
				biggestPart = candidate
			end
		end

		if not biggestPart then
			return {}
		end

		-- Calculate min voxel size and subdivision iterations based on the biggest part
		local minSize = module.CalculateMinVoxelSize(biggestPart, 0.05) -- fraction preserved from original
		local iterations = module.CalculateSubdivisionIterations(biggestPart, minSize)

		-- Recursively subdivide relevant parts around the sphere center
		local finalParts = module.RecursiveSubdivision(mapParts, Point, Radius, minSize, iterations)

		-- Separate parts that are inside the sphere (to be affected) and extras to restore
		local extras = {}
		for _, v in ipairs(finalParts) do
			if v and module.IsPartInSphere(v, Point, Radius) then
				-- Make dynamic
				v.Anchored = false
				-- Apply a modest impulse (kept from original)
				if v:IsA("BasePart") and v:CanSetNetworkOwnership() then
					-- ApplyImpulse exists on BasePart, if available
					pcall(function()
						v:ApplyImpulse(Vector3.new(40, 40, 40))
					end)
				else
					-- Fallback: change velocity if ApplyImpulse isn't available
					if v:IsA("BasePart") then
						v.AssemblyLinearVelocity = v.AssemblyLinearVelocity + Vector3.new(40, 40, 40)
					end
				end
			else
				table.insert(extras, v)
			end
		end

		-- Defer unsubdivide/restoration to avoid blocking
		if #extras > 0 then
			task.spawn(function()
				local ok, Result = pcall(function()
					return require(script.UnSubdivideModule).Unsubdivide(extras)
				end)
				if ok and type(Result) == "table" then
					for _, restored in ipairs(Result) do
						if restored then
							restored.Parent = workspace:FindFirstChild("Map") or workspace
						end
					end
				end
			end)
		end

		return finalParts
	end

	-- Execute the calculation
	CalculateHitParts()
end

return module