I wanna fix that 3 studs walls are getting penetrated.
Video:
Script:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local fireEvent = ReplicatedStorage:WaitForChild("FPS_Fire")
-- Weapon stats table
local WeaponStats = {
Glock19 = {
Damage = {
Head = 114,
Torso = 37,
Limb = 22,
Default = 29,
},
ArmorMultiplier = 0.5, -- 50% reduction
MaxRange = 150, -- studs
PenetrationCount = 1, -- how many walls it can penetrate
PenetrationLoss = 0.5, -- lose 50% damage per wall
PenetrableMaterials = {
[Enum.Material.Wood] = true,
[Enum.Material.Fabric] = true,
[Enum.Material.ForceField] = true,
[Enum.Material.Neon] = true,
[Enum.Material.Plastic] = 0.05,
[Enum.Material.SmoothPlastic] = 0.05,
-- Less power: can't penetrate metal, concrete, brick, etc.
},
},
AK47 = {
Damage = {
Head = 119,
Torso = 29,
Limb = 22,
Default = 37,
},
ArmorMultiplier = 0.5, -- 50% reduction
MaxRange = 600, -- studs
PenetrationCount = 2,
PenetrationLoss = 0.4, -- lose 40% damage per wall
-- For AK, allow probabilistic penetration for metal/concrete/brick
PenetrableMaterials = {
[Enum.Material.Wood] = true,
[Enum.Material.Plastic] = true,
[Enum.Material.Fabric] = true,
[Enum.Material.Glass] = true,
[Enum.Material.ForceField] = true,
[Enum.Material.SmoothPlastic] = true,
[Enum.Material.Neon] = true,
[Enum.Material.Metal] = 0.10, -- 10% chance to penetrate metal
[Enum.Material.Concrete] = 0.15, -- 15% chance to penetrate concrete
[Enum.Material.Brick] = 0.15, -- 15% chance to penetrate brick
-- Still can't penetrate granite, marble, etc.
},
},
Default = {
Damage = {
Head = 100,
Torso = 25,
Limb = 15,
Default = 25,
},
ArmorMultiplier = 0.6,
MaxRange = 500, -- fallback
PenetrationCount = 1,
PenetrationLoss = 0.5,
PenetrableMaterials = {
[Enum.Material.Wood] = true,
[Enum.Material.Plastic] = true,
[Enum.Material.Fabric] = true,
[Enum.Material.Glass] = true,
[Enum.Material.ForceField] = true,
[Enum.Material.SmoothPlastic] = true,
[Enum.Material.Neon] = true,
},
}
}
-- Helper to get weapon stats
local function GetWeaponStats(weaponName)
if not weaponName then return WeaponStats.Default end
local stats = WeaponStats[weaponName]
if stats then return stats end
-- Try lowercase
stats = WeaponStats[weaponName:lower()]
if stats then return stats end
-- Try uppercase
stats = WeaponStats[weaponName:upper()]
if stats then return stats end
return WeaponStats.Default
end
fireEvent.OnServerEvent:Connect(function(player, origin, direction, weaponName)
-- Raycast parameters: ignore shooter
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.FilterDescendantsInstances = {player.Character}
raycastParams.IgnoreWater = false
local weaponStats = GetWeaponStats(weaponName)
local maxRange = weaponStats.MaxRange or 500
local penetrationCount = weaponStats.PenetrationCount or 1
local penetrationLoss = weaponStats.PenetrationLoss or 0.5
local penetrableMaterials = weaponStats.PenetrableMaterials or WeaponStats.Default.PenetrableMaterials
local rayLength = maxRange
local remainingRange = maxRange
local currentOrigin = origin
local currentDirection = direction
local ignoreList = {player.Character}
local debugParts = {} -- Store debug parts to exclude from raycast
local hitHumanoid = nil
local hitPartName = nil
local damage = 0
local hitType = "Miss"
local armored = false
local armorMultiplier = weaponStats.ArmorMultiplier or 0.6
local penetrations = 0
local lastResult = nil
local hitPosition = origin + direction * maxRange
local MAX_PENETRATION_THICKNESS = 2 -- studs; if wall is thicker than this, can't penetrate
while penetrations <= penetrationCount and remainingRange > 0 do
raycastParams.FilterDescendantsInstances = ignoreList
local result = Workspace:Raycast(currentOrigin, currentDirection * remainingRange, raycastParams)
if not result then
-- No more hits, bullet traveled full remaining range
break
end
lastResult = result
hitPosition = result.Position
local hitPart = result.Instance
if hitPart then
hitPartName = hitPart.Name:lower()
local character = hitPart:FindFirstAncestorOfClass("Model")
if character then
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.Health > 0 and character ~= player.Character then
hitHumanoid = humanoid
-- R15 body part names
if hitPartName == "head" then
damage = weaponStats.Damage.Head or 100
hitType = "Headshot"
elseif hitPartName == "uppertorso" or hitPartName == "lowertorso" then
damage = weaponStats.Damage.Torso or 25
hitType = "Bodyshot"
elseif hitPartName == "leftupperarm" or hitPartName == "leftlowerarm" or hitPartName == "lefthand"
or hitPartName == "rightupperarm" or hitPartName == "rightlowerarm" or hitPartName == "righthand"
or hitPartName == "leftupperleg" or hitPartName == "leftlowerleg" or hitPartName == "leftfoot"
or hitPartName == "rightupperleg" or hitPartName == "rightlowerleg" or hitPartName == "rightfoot" then
damage = weaponStats.Damage.Limb or 15
hitType = "Limbshot"
else
damage = weaponStats.Damage.Default or 25
hitType = "Bodyshot"
end
-- Apply penetration loss
for i = 1, penetrations do
damage = math.floor(damage * (1 - penetrationLoss))
end
-- Check for armor
local armoredValue = character:FindFirstChild("Armored")
if armoredValue and armoredValue:IsA("BoolValue") and armoredValue.Value == true then
armored = true
local oldDamage = damage
damage = math.floor(damage * armorMultiplier)
print("[FPS_ShootHandler] Target is ARMORED! Weapon: "..tostring(weaponName)..". Damage reduced from "..oldDamage.." to "..damage..".")
end
break -- Stop on first humanoid hit
end
end
-- If not a humanoid, check if penetrable (per-weapon)
if hitPart:IsA("BasePart") and penetrableMaterials[hitPart.Material] then
-- Determine if penetration is allowed (boolean or probabilistic)
local penetrationEntry = penetrableMaterials[hitPart.Material]
local canPenetrate = false
local penetrationChance = nil
local penetrationSucceeded = false
if typeof(penetrationEntry) == "boolean" then
canPenetrate = penetrationEntry
penetrationSucceeded = canPenetrate
elseif typeof(penetrationEntry) == "number" then
penetrationChance = penetrationEntry
if math.random() < penetrationChance then
canPenetrate = true
penetrationSucceeded = true
else
canPenetrate = false
penetrationSucceeded = false
end
end
-- Robust thickness calculation: cast a ray from entry point through the part to find exit point
local thickness = 0.5 -- default thickness penalty
if hitPart.Size then
local entryPoint = result.Position
local epsilon = 0.01
local rayOrigin = entryPoint + currentDirection.Unit * epsilon
local rayDirection = currentDirection.Unit * (hitPart.Size.Magnitude + 2 * epsilon)
-- Only check for intersection with the hitPart itself
local thicknessParams = RaycastParams.new()
thicknessParams.FilterType = Enum.RaycastFilterType.Whitelist
thicknessParams.FilterDescendantsInstances = {hitPart}
thicknessParams.IgnoreWater = false
local exitResult = Workspace:Raycast(rayOrigin, rayDirection, thicknessParams)
if exitResult and exitResult.Instance == hitPart then
thickness = (exitResult.Position - entryPoint).Magnitude
else
-- If no exit found, fallback to minimum thickness
thickness = 0.05
end
if thickness < 0.05 then thickness = 0.05 end -- minimum epsilon
end
-- If wall is too thick, can't penetrate
if canPenetrate and thickness < MAX_PENETRATION_THICKNESS then
penetrations = penetrations + 1
-- Add this part to ignore list for next raycast
table.insert(ignoreList, hitPart)
-- Reduce remaining range by distance traveled + a penalty for thickness
local distanceTraveled = (result.Position - currentOrigin).Magnitude
remainingRange = remainingRange - distanceTraveled - thickness
-- Move origin to just outside the wall, along the ray direction, using thickness
-- This ensures the next raycast starts outside the wall, even for thin walls
local epsilon = 0.01
currentOrigin = result.Position + currentDirection.Unit * (thickness + epsilon)
-- Only spawn yellow debug part if penetration is guaranteed (true) or chance is reasonably high (>= 0.2)
-- This avoids showing yellow for very low chance penetrations (e.g., AK spraying at metal)
if (typeof(penetrationEntry) == "boolean" and penetrationEntry == true)
or (typeof(penetrationEntry) == "number" and penetrationEntry >= 0.2 and penetrationSucceeded) then
local debugPart = Instance.new("Part")
debugPart.Size = Vector3.new(0.3, 0.3, 0.3)
debugPart.Shape = Enum.PartType.Block
debugPart.Color = Color3.new(1, 1, 0)
debugPart.Material = Enum.Material.Neon
debugPart.Anchored = true
debugPart.CanCollide = false
debugPart.CFrame = CFrame.new(result.Position)
debugPart.Parent = Workspace
game:GetService("Debris"):AddItem(debugPart, 1)
table.insert(debugParts, debugPart)
table.insert(ignoreList, debugPart)
end
continue -- Continue to next penetration
else
-- Not allowed to penetrate (either failed chance or too thick)
break
end
else
-- Hit non-penetrable part, stop
break
end
else
break
end
end
-- Optionally, print if the hit is at the very edge of range (e.g., within 1 stud of maxRange)
local distanceTraveled = (hitPosition - origin).Magnitude
if distanceTraveled >= maxRange - 1 then
print("[FPS_ShootHandler] Shot reached maximum range (" .. tostring(maxRange) .. " studs)")
end
-- Damage if hit a humanoid
if hitHumanoid and damage > 0 then
hitHumanoid:TakeDamage(damage)
print("[FPS_ShootHandler] "..hitType.."! Weapon: "..tostring(weaponName)..". Hit part: "..(hitPartName or "Unknown")..". Applied "..damage.." damage."..(armored and " (Armored)" or ""))
end
-- Spawn debug part at hit position ONLY if not too far (i.e., only if lastResult is not nil)
if lastResult then
local debugPart = Instance.new("Part")
debugPart.Size = Vector3.new(0.3, 0.3, 0.3)
debugPart.Shape = Enum.PartType.Block
debugPart.Color = Color3.new(1, 0, 0)
debugPart.Material = Enum.Material.Neon
debugPart.Anchored = true
debugPart.CanCollide = false
debugPart.CFrame = CFrame.new(hitPosition)
debugPart.Parent = Workspace
-- Auto-destroy after 1 second
game:GetService("Debris"):AddItem(debugPart, 1)
table.insert(debugParts, debugPart)
table.insert(ignoreList, debugPart)
end
end)
What should I do?