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