local WeaponUtils = {} local rBotUtils = require(game.ReplicatedStorage.Shared.SharedUtils.sharedBotUtils) local BotUtils = require("./BotUtils") --//////////////////////////////////////////////////////// -- CONFIG --//////////////////////////////////////////////////////// local VOXEL_SIZE = 0.5 local MAX_VOXELS_PER_EXPLOSION = 1500 local MATERIAL_RESISTANCE = { [Enum.Material.Concrete] = 0.75, [Enum.Material.Slate] = 0.65, [Enum.Material.Wood] = 0.35, [Enum.Material.Plastic] = 0.2, Default = 0.5 } local overlapParams = OverlapParams.new() overlapParams.FilterType = Enum.RaycastFilterType.Include local function getVoxelFolder() return workspace:FindFirstChild("VoxelDebris") end local function snap(v) return math.floor(v / VOXEL_SIZE + 0.5) end local DEBUG = true local DEBUG_LIFETIME = 0.35 local function spawnDebugSphere(position: Vector3, radius: number, color: Color3) local sphere = Instance.new("Part") sphere.Shape = Enum.PartType.Ball sphere.Anchored = true sphere.CanCollide = false sphere.Material = Enum.Material.Neon sphere.Transparency = 0.75 sphere.Color = color sphere.Size = Vector3.new(radius * 2, radius * 2, radius * 2) sphere.Position = position sphere.Parent = workspace game:GetService("Debris"):AddItem(sphere, DEBUG_LIFETIME) end --//////////////////////////////////////////////////////// -- EXPLOSION (2 PHASE COLUMN SYSTEM) --//////////////////////////////////////////////////////// function WeaponUtils.ExplosionQuery(origin: Vector3, innerRadius: number, outerRadius: number, maxDamage: number) -------------------------------------------------------- -- BOT DAMAGE -------------------------------------------------------- for _, model in pairs(workspace.Bots:GetChildren()) do local bot = BotUtils.GetBotByModel(model) if not bot then continue end local position = rBotUtils.GetBotPosition(bot.key) local damage = WeaponUtils.CalculateExplosionDamage(origin, position, outerRadius, maxDamage) if damage > 0 then bot.Components.Health:TakeDamage(damage) end end if DEBUG then spawnDebugSphere(origin, outerRadius, Color3.fromRGB(255, 60, 60)) -- red outer spawnDebugSphere(origin, innerRadius, Color3.fromRGB(255, 140, 0)) -- orange inner end local folder = getVoxelFolder() if not folder then return end overlapParams.FilterDescendantsInstances = {folder} local parts = workspace:GetPartBoundsInRadius(origin, innerRadius, overlapParams) if #parts == 0 then return end local radiusSq = innerRadius * innerRadius -------------------------------------------------------- -- PHASE 1: FIND VALID VOXELS (SEEDS) -------------------------------------------------------- local validColumns = {} local processed = 0 for i = 1, #parts do if processed > MAX_VOXELS_PER_EXPLOSION then break end local part = parts[i] if not part:IsA("BasePart") then continue end local offset = part.Position - origin local distSq = offset:Dot(offset) if distSq > radiusSq then continue end local dist = math.sqrt(distSq) local falloff = 1 - (dist / innerRadius) local power = falloff * falloff local resistance = MATERIAL_RESISTANCE[part.Material] or MATERIAL_RESISTANCE.Default if power > resistance then -- STORE COLUMN KEY (X + Y ONLY) local gx = snap(part.Position.X) local gy = snap(part.Position.Y) local key = gx .. "," .. gy validColumns[key] = true end processed += 1 end -------------------------------------------------------- -- PHASE 2: DESTROY FULL Z COLUMNS (NO CHECKS) -------------------------------------------------------- for i = 1, #parts do local part = parts[i] if not part:IsA("BasePart") then continue end local gx = snap(part.Position.X) local gy = snap(part.Position.Y) local key = gx .. "," .. gy if validColumns[key] then part:Destroy() end end end --//////////////////////////////////////////////////////// -- DAMAGE --//////////////////////////////////////////////////////// function WeaponUtils.CalculateExplosionDamage(origin: Vector3, position: Vector3, radius: number, maxDamage: number) local offset = position - origin local distSq = offset:Dot(offset) if distSq >= radius * radius then return 0 end local dist = math.sqrt(distSq) local normalized = dist / radius local damage = maxDamage * (1 - normalized)^2 return math.clamp(damage, 0, maxDamage) end return WeaponUtils