So I’m using the Roblox weapons kit
But I don’t know how to make it deal more damage when its a headshot?
I think it has something to do with this script but I’m not the best at scripting, help?
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
local ContextActionService = game:GetService("ContextActionService")
local CollectionService = game:GetService("CollectionService")
local ContentProvider = game:GetService("ContentProvider")
local IsServer = RunService:IsServer()
local WeaponsSystemFolder = script.Parent.Parent
local Libraries = WeaponsSystemFolder:WaitForChild("Libraries")
local BaseWeapon = require(Libraries:WaitForChild("BaseWeapon"))
local Parabola = require(Libraries:WaitForChild("Parabola"))
local Roblox = require(Libraries:WaitForChild("Roblox"))
local Effects = WeaponsSystemFolder:WaitForChild("Assets"):WaitForChild("Effects")
local ShotsFolder = Effects:WaitForChild("Shots")
local HitMarksFolder = Effects:WaitForChild("HitMarks")
local CasingsFolder = Effects:WaitForChild("Casings")
local NO_BULLET_DECALS = false
local NO_BULLET_CASINGS = false
--The ignore list will fill up over time. This is how many seconds it will go before
--being refreshed in order to keep it from filling up with instances that aren't in
--the datamodel anymore.
local IGNORE_LIST_LIFETIME = 5
local MAX_BULLET_TIME = 10
local localRandom = Random.new()
local localPlayer = not IsServer and Players.LocalPlayer
local BulletWeapon = {}
BulletWeapon.__index = BulletWeapon
setmetatable(BulletWeapon, BaseWeapon)
BulletWeapon.CanAimDownSights = true
BulletWeapon.CanBeFired = true
BulletWeapon.CanBeReloaded = true
BulletWeapon.CanHit = true
function BulletWeapon.new(weaponsSystem, instance)
local self = BaseWeapon.new(weaponsSystem, instance)
setmetatable(self, BulletWeapon)
self.usesCharging = false
self.charge = 0
self.chargeSoundPitchMin = 0.5
self.chargeSoundPitchMax = 1
self.triggerDisconnected = false
self.startupFinished = false -- TODO: make startup time use a configuration value
self.burstFiring = false
self.burstIdx = 0
self.nextFireTime = 0
self.recoilIntensity = 0
self.aimPoint = Vector3.new()
self:addOptionalDescendant("tipAttach", "TipAttachment")
self:addOptionalDescendant("boltMotor", "BoltMotor")
self:addOptionalDescendant("boltMotorStart", "BoltMotorStart")
self:addOptionalDescendant("boltMotorTarget", "BoltMotorTarget")
self:addOptionalDescendant("chargeGlowPart", "ChargeGlow")
self:addOptionalDescendant("chargeCompleteParticles", "ChargeCompleteParticles")
self:addOptionalDescendant("dischargeCompleteParticles", "DischargeCompleteParticles")
self:addOptionalDescendant("muzzleFlash0", "MuzzleFlash0")
self:addOptionalDescendant("muzzleFlash1", "MuzzleFlash1")
self:addOptionalDescendant("muzzleFlashBeam", "MuzzleFlash")
self.hitMarkTemplate = HitMarksFolder:FindFirstChild(self:getConfigValue("HitMarkEffect", "BulletHole"))
self.casingTemplate = CasingsFolder:FindFirstChild(self:getConfigValue("CasingEffect", ""))
self:addOptionalDescendant("casingEjectPoint", "CasingEjectPoint")
self.ignoreList = {}
self.ignoreListRefreshTime = 0
self:addOptionalDescendant("handAttach", "LeftHandAttachment")
self.handAlignPos = nil
self.handAlignRot = nil
self.chargingParticles = {}
self.instance.DescendantAdded:Connect(function(descendant)
if descendant.Name == "ChargingParticles" and descendant:IsA("ParticleEmitter") then
table.insert(self.chargingParticles, descendant)
end
end)
for _, v in pairs(self.instance:GetDescendants()) do
if v.Name == "ChargingParticles" and v:IsA("ParticleEmitter") then
table.insert(self.chargingParticles, v)
end
end
self:doInitialSetup()
return self
end
function BulletWeapon:onEquippedChanged()
BaseWeapon.onEquippedChanged(self)
if not IsServer then
if self.weaponsSystem.camera then
if self.equipped then
self.startupFinished = false
end
end
if self.equipped then
ContextActionService:BindAction("ReloadWeapon", function(...) self:onReloadAction(...) end, false, Enum.KeyCode.R, Enum.KeyCode.ButtonX)
else
ContextActionService:UnbindAction("ReloadWeapon")
-- Stop charging/discharging sounds
local chargingSound = self:getSound("Charging")
local dischargingSound = self:getSound("Discharging")
if chargingSound and chargingSound.Playing then
chargingSound:Stop()
end
if dischargingSound and dischargingSound.Playing then
dischargingSound:Stop()
end
end
self.triggerDisconnected = false
end
end
function BulletWeapon:onReloadAction(actionName, inputState, inputObj)
if inputState == Enum.UserInputState.Begin and not self.reloading then
self:reload()
end
end
function BulletWeapon:animateBoltAction(isOpen)
if not self.boltMotor or not self.boltMotorStart or not self.boltMotorTarget then
return
end
if isOpen then
self:tryPlaySound("BoltOpenSound")
else
self:tryPlaySound("BoltCloseSound")
end
local actionMoveTime = isOpen and self:getConfigValue("ActionOpenTime", 0.025) or self:getConfigValue("ActionCloseTime", 0.075)
local targetCFrame = isOpen and self.boltMotorTarget.CFrame or self.boltMotorStart.CFrame
local boltTween = TweenService:Create(self.boltMotor, TweenInfo.new(actionMoveTime, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut), { C0 = targetCFrame })
boltTween:Play()
boltTween.Completed:Wait()
end
function BulletWeapon:getRandomSeedForId(id)
return id
end
-- This function is only called on clients
function BulletWeapon:simulateFire(firingPlayer, fireInfo)
BaseWeapon.simulateFire(self, fireInfo)
-- Play "Fired" sound
if self.lastFireSound then
self.lastFireSound:Stop()
end
self.lastFireSound = self:tryPlaySound("Fired", self:getConfigValue("FiredPlaybackSpeedRange", 0.1))
-- Simulate each projectile/bullet fired from current weapon
local numProjectiles = self:getConfigValue("NumProjectiles", 1)
local randomGenerator = Random.new(self:getRandomSeedForId(fireInfo.id))
for i = 1, numProjectiles do
self:simulateProjectile(firingPlayer, fireInfo, i, randomGenerator)
end
-- Animate the bolt if the current gun has one
local actionOpenTime = self:getConfigValue("ActionOpenTime", 0.025)
if self.boltMotor then
coroutine.wrap(function()
self:animateBoltAction(true)
wait(actionOpenTime)
self:animateBoltAction(false)
end)()
end
-- Eject bullet casings and play "CasingHitSound" (child of casing) sound if applicable for current weapon
if not NO_BULLET_CASINGS and self.casingTemplate and self.casingEjectPoint then
local casing = self.casingTemplate:Clone()
casing.Anchored = false
casing.Archivable = false
casing.CFrame = self.casingEjectPoint.WorldCFrame
casing.Velocity = self.casingEjectPoint.Parent.Velocity + (self.casingEjectPoint.WorldAxis * localRandom:NextNumber(self:getConfigValue("CasingEjectSpeedMin", 15), self:getConfigValue("CasingEjectSpeedMax", 18)))
casing.Parent = workspace.CurrentCamera
CollectionService:AddTag(casing, "WeaponsSystemIgnore")
local casingHitSound = casing:FindFirstChild("CasingHitSound")
if casingHitSound then
local touchedConn = nil
touchedConn = casing.Touched:Connect(function(hitPart)
if not hitPart:IsDescendantOf(self.instance) then
casingHitSound:Play()
touchedConn:Disconnect()
touchedConn = nil
end
end)
end
Debris:AddItem(casing, 2)
end
if self.player == Players.LocalPlayer then
coroutine.wrap(function()
-- Wait for "RecoilDelayTime" before adding recoil
local startTime = tick()
local recoilDelayTime = self:getConfigValue("RecoilDelayTime", 0.07)
while tick() < startTime + recoilDelayTime do
RunService.RenderStepped:Wait()
end
RunService.RenderStepped:Wait()
-- Add recoil to camera
local recoilMin, recoilMax = self:getConfigValue("RecoilMin", 0.05), self:getConfigValue("RecoilMax", 0.5)
local intensityToAdd = randomGenerator:NextNumber(recoilMin, recoilMax)
local xIntensity = math.sin(tick() * 2) * intensityToAdd * math.rad(0.05)
local yIntensity = intensityToAdd * 0.025
self.weaponsSystem.camera:addRecoil(Vector2.new(xIntensity, yIntensity))
if not (self.weaponsSystem.camera:isZoomed() and self:getConfigValue("HasScope", false)) then
self.recoilIntensity = math.clamp(self.recoilIntensity * 1 + (intensityToAdd / 10), 0.005, 1)
end
-- Make crosshair reflect recoil/spread amount
local weaponsGui = self.weaponsSystem.gui
if weaponsGui then
weaponsGui:setCrosshairScale(1 + intensityToAdd)
end
end)()
end
end
function BulletWeapon:getIgnoreList(includeLocalPlayer)
local now = tick()
local ignoreList = self.ignoreList
if not ignoreList or now - self.ignoreListRefreshTime > IGNORE_LIST_LIFETIME then
ignoreList = {
self.instanceIsTool and self.instance.Parent or self.instance,
workspace.CurrentCamera
}
if not RunService:IsServer() then
if includeLocalPlayer and Players.LocalPlayer and Players.LocalPlayer.Character then
table.insert(ignoreList, Players.LocalPlayer.Character)
end
end
self.ignoreList = ignoreList
end
return ignoreList
end
-- This function is only called on clients
function BulletWeapon:simulateProjectile(firingPlayer, fireInfo, projectileIdx, randomGenerator)
local localPlayerInitiatedShot = self.player == Players.LocalPlayer
-- Retrieve config values
local bulletSpeed = self:getConfigValue("BulletSpeed", 1000)
local maxDistance = self:getConfigValue("MaxDistance", 2000)
local trailLength = self:getConfigValue("TrailLength", nil)
local trailLengthFactor = self:getConfigValue("TrailLengthFactor", 1)
local showEntireTrailUntilHit = self:getConfigValue("ShowEntireTrailUntilHit", false)
local gravityFactor = self:getConfigValue("GravityFactor", 0)
local minSpread = self:getConfigValue("MinSpread", 0)
local maxSpread = self:getConfigValue("MaxSpread", 0)
local shouldMovePart = self:getConfigValue("ShouldMovePart", false)
local explodeOnImpact = self:getConfigValue("ExplodeOnImpact", false)
local blastRadius = self:getConfigValue("BlastRadius", 8)
-- Cheat the origin of the shot back if gun tip in wall/object
if self.tipAttach ~= nil then
local tipCFrame = self.tipAttach.WorldCFrame
local tipPos = tipCFrame.Position
local tipDir = tipCFrame.LookVector
local amountToCheatBack = math.abs((self.instance:FindFirstChild("Handle").Position - tipPos):Dot(tipDir)) + 1
local gunRay = Ray.new(tipPos - tipDir.Unit * amountToCheatBack, tipDir.Unit * amountToCheatBack)
local hitPart, hitPoint = Roblox.penetrateCast(gunRay, self:getIgnoreList(localPlayerInitiatedShot))
if hitPart and math.abs((tipPos - hitPoint).Magnitude) > 0 then
fireInfo.origin = hitPoint - tipDir.Unit * 0.1
fireInfo.dir = tipDir.Unit
end
end
local origin, dir = fireInfo.origin, fireInfo.dir
dir = Roblox.applySpread(dir, randomGenerator, math.rad(minSpread), math.rad(maxSpread))
-- Initialize variables for visuals/particle effects
local bulletEffect = self.bulletEffectTemplate:Clone()
bulletEffect.CFrame = CFrame.new(origin, origin + dir)
bulletEffect.Parent = workspace.CurrentCamera
CollectionService:AddTag(bulletEffect, "WeaponsSystemIgnore")
local leadingParticles = bulletEffect:FindFirstChild("LeadingParticles", true)
local attachment0 = bulletEffect:FindFirstChild("Attachment0")
local trailParticles = nil
if attachment0 then
trailParticles = attachment0:FindFirstChild("TrailParticles")
end
local hitAttach = bulletEffect:FindFirstChild("HitEffect")
local hitParticles = bulletEffect:FindFirstChild("HitParticles", true)
local numHitParticles = self:getConfigValue("NumHitParticles", 3)
local hitSound = bulletEffect:FindFirstChild("HitSound", true)
local flyingSound = bulletEffect:FindFirstChild("Flying", true)
local muzzleFlashTime = self:getConfigValue("MuzzleFlashTime", 0.03)
local muzzleFlashShown = false
local beamThickness0 = self:getConfigValue("BeamWidth0", 1.5)
local beamThickness1 = self:getConfigValue("BeamWidth1", 1.8)
local beamFadeTime = self:getConfigValue("BeamFadeTime", nil)
-- Enable beam trails for projectile
local beam0 = bulletEffect:FindFirstChild("Beam0")
if beam0 then
beam0.Enabled = true
end
local beam1 = bulletEffect:FindFirstChild("Beam1")
if beam1 then
beam1.Enabled = true
end
-- Emit muzzle particles
local muzzleParticles = bulletEffect:FindFirstChild("MuzzleParticles", true)
local numMuzzleParticles = self:getConfigValue("NumMuzzleParticles", 50)
if muzzleParticles then
muzzleParticles.Parent.CFrame = CFrame.new(origin, origin + dir)
local numSteps = 5
for _ = 1, numSteps do
muzzleParticles.Parent.Velocity = Vector3.new(localRandom:NextNumber(-10, 10), localRandom:NextNumber(-10, 10), localRandom:NextNumber(-10, 10))
muzzleParticles:Emit(numMuzzleParticles / numSteps)
end
end
-- Show muzzle flash
if self.tipAttach and self.muzzleFlash0 and self.muzzleFlash1 and self.muzzleFlashBeam and projectileIdx == 1 then
local minFlashRotation, maxFlashRotation = self:getConfigValue("MuzzleFlashRotation0", -math.pi), self:getConfigValue("MuzzleFlashRotation1", math.pi)
local minFlashSize, maxFlashSize = self:getConfigValue("MuzzleFlashSize0", 1), self:getConfigValue("MuzzleFlashSize1", 1)
local flashRotation = localRandom:NextNumber(minFlashRotation, maxFlashRotation)
local flashSize = localRandom:NextNumber(minFlashSize, maxFlashSize)
local baseCFrame = self.tipAttach.CFrame * CFrame.Angles(0, 0, flashRotation)
self.muzzleFlash0.CFrame = baseCFrame * CFrame.new(flashSize * -0.5, 0, 0) * CFrame.Angles(0, math.pi, 0)
self.muzzleFlash1.CFrame = baseCFrame * CFrame.new(flashSize * 0.5, 0, 0) * CFrame.Angles(0, math.pi, 0)
self.muzzleFlashBeam.Enabled = true
self.muzzleFlashBeam.Width0 = flashSize
self.muzzleFlashBeam.Width1 = flashSize
muzzleFlashShown = true
end
-- Play projectile flying sound
if flyingSound then
flyingSound:Play()
end
-- Enable trail particles
if trailParticles then
trailParticles.Enabled = true
end
-- Set up parabola for projectile path
local parabola = Parabola.new()
parabola:setPhysicsLaunch(origin, dir * bulletSpeed, nil, 35 * -gravityFactor)
-- More samples for higher gravity since path will be more curved but raycasts can only be straight lines
if gravityFactor > 0.66 then
parabola:setNumSamples(3)
elseif gravityFactor > 0.33 then
parabola:setNumSamples(2)
else
parabola:setNumSamples(1)
end
-- Set up/initialize variables used in steppedCallback
local stepConn = nil
local pTravelDistance = 0 -- projected travel distance so far if projectile never stops
local startTime = tick()
local didHit = false
local stoppedMotion = false
local stoppedMotionAt = 0
local timeSinceStart = 0
local flyingVisualEffectsFinished = false -- true if all particle effects shown while projectile is flying are done
local visualEffectsFinishTime = math.huge
local visualEffectsLingerTime = 0 -- max time any visual effect needs to finish
if beamFadeTime then
visualEffectsLingerTime = beamFadeTime
end
local hitInfo = {
sid = fireInfo.id,
pid = projectileIdx,
maxDist = maxDistance,
part = nil,
p = nil,
n = nil,
m = Enum.Material.Air,
d = 1e9,
}
local steppedCallback = function(dt)
local now = tick()
timeSinceStart = now - startTime
local travelDist = bulletSpeed * dt -- distance projectile has travelled since last frame
trailLength = trailLength or travelDist * trailLengthFactor
-- Note: the next three variables are all in terms of distance from starting point (which should be tip of current weapon)
local projBack = pTravelDistance - trailLength -- furthest back part of projectile (including the trail effect, so will be the start of the trail effect if any)
local projFront = pTravelDistance -- most forward part of projectile
local maxDist = hitInfo.maxDist or 0 -- before it collides, this is the max distance the projectile can travel. After it collides, this is the hit point
-- This will make trailing beams render from tip of gun to wherever projectile is until projectile is destroyed
if showEntireTrailUntilHit then
projBack = 0
end
-- Validate projBack and projFront
projBack = math.clamp(projBack, 0, maxDist)
projFront = math.clamp(projFront, 0, maxDist)
if not didHit then
-- Check if bullet hit since last frame
local castProjBack, castProjFront = projFront, projFront + travelDist
parabola:setDomain(castProjBack, castProjFront)
local hitPart, hitPoint, hitNormal, hitMaterial, hitT = parabola:findPart(self.ignoreList)
if hitPart then
didHit = true
projFront = castProjBack + hitT * (castProjFront - castProjBack) -- set projFront to point along projectile arc where an object was hit
parabola:setDomain(projBack, projFront) -- update parabola domain to match new projFront
-- Update hitInfo
hitInfo.part = hitPart
hitInfo.p = hitPoint
hitInfo.n = hitNormal
hitInfo.m = hitMaterial
hitInfo.d = (hitPoint - origin).Magnitude
hitInfo.t = hitT
hitInfo.maxDist = projFront -- since the projectile hit, maxDist is now the hitPoint instead of maxDistance
-- Register hit on clients
self:onHit(hitInfo)
-- Notify the server that this projectile hit something from client that initiated the shot
-- Show hit indicators on gui of client that shot projectile
if localPlayerInitiatedShot then
local hitInfoClone = {}
for hitInfoKey, value in pairs(hitInfo) do
hitInfoClone[hitInfoKey] = value
end
self.weaponsSystem.getRemoteEvent("WeaponHit"):FireServer(self.instance, hitInfoClone)
end
-- Deal with all effects that start/stop/change on hit
-- Disable trail particles
if trailParticles then
trailParticles.Enabled = false
end
-- Stop bullet flying sound
if flyingSound and flyingSound.IsPlaying then
flyingSound:Stop()
end
-- Hide the actual projectile model
if bulletEffect then
bulletEffect.Transparency = 1
end
-- Stop emitting leading particles
if leadingParticles then
leadingParticles.Rate = 0
visualEffectsLingerTime = math.max(visualEffectsLingerTime, leadingParticles.Lifetime.Max)
end
-- Show the explosion on clients for explosive projectiles
if explodeOnImpact then
local explosion = Instance.new("Explosion")
explosion.Position = hitPoint + (hitNormal * 0.5)
explosion.BlastRadius = blastRadius
explosion.BlastPressure = 0 -- no blast pressure because the real explosion happens on server
explosion.ExplosionType = Enum.ExplosionType.NoCraters
explosion.DestroyJointRadiusPercent = 0
explosion.Visible = true
if localPlayerInitiatedShot then
-- Trigger hit indicators on client that initiated the shot if the explosion hit another player/humanoid
explosion.Hit:Connect(function(explodedPart, hitDist)
local humanoid = self.weaponsSystem.getHumanoid(explodedPart)
if humanoid and
explodedPart.Name == "UpperTorso" and
humanoid:GetState() ~= Enum.HumanoidStateType.Dead and
self.weaponsSystem.gui and
explodedPart.Parent ~= self.player.Character and
self.weaponsSystem.playersOnDifferentTeams(self.weaponsSystem.getPlayerFromHumanoid(humanoid), self.player)
then
self.weaponsSystem.gui:OnHitOtherPlayer(self:calculateDamage(hitInfo.d), humanoid)
end
end)
end
explosion.Parent = workspace
end
-- Make sure hitAttach is in correct position before showing hit effects
if hitAttach and beam0 and beam0.Attachment1 then
parabola:renderToBeam(beam0)
hitAttach.CFrame = beam0.Attachment1.CFrame * CFrame.Angles(0, math.rad(90), 0)
end
-- Show hit particle effect
local hitPartColor = hitPart and hitPart.Color or Color3.fromRGB(255, 255, 255)
if hitPart and hitPart:IsA("Terrain") then
hitPartColor = workspace.Terrain:GetMaterialColor(hitMaterial or Enum.Material.Sand)
end
if hitInfo.h and hitInfo.h:IsA("Humanoid") and hitParticles and numHitParticles > 0 and hitPart then
-- Show particle effect for hitting a player/humanoid
hitParticles.Color = ColorSequence.new(Color3.fromRGB(255, 255, 255))
hitParticles:Emit(numHitParticles)
visualEffectsLingerTime = math.max(visualEffectsLingerTime, hitParticles.Lifetime.Max)
elseif (not hitInfo.h or not hitInfo.h:IsA("Humanoid")) and hitParticles and numHitParticles > 0 then
-- Show particle effect for hitting anything else
if hitPart and self:getConfigValue("HitParticlesUsePartColor", true) then
local existingSeq = hitParticles.Color
local newKeypoints = {}
for i, keypoint in pairs(existingSeq.Keypoints) do
local newColor = keypoint.Value
if newColor == Color3.fromRGB(255, 0, 255) then
newColor = hitPartColor
end
newKeypoints[i] = ColorSequenceKeypoint.new(keypoint.Time, newColor)
end
hitParticles.Color = ColorSequence.new(newKeypoints)
end
hitParticles:Emit(numHitParticles)
visualEffectsLingerTime = math.max(visualEffectsLingerTime, hitParticles.Lifetime.Max)
end
-- Play hit sound
if hitSound then
hitSound:Play()
visualEffectsLingerTime = math.max(visualEffectsLingerTime, hitSound.TimeLength)
end
-- Manage/show decals, billboards, and models (such as an arrow) that appear where the projectile hit (only if the hit object was not a humanoid/player)
local hitPointObjectSpace = hitPart.CFrame:pointToObjectSpace(hitPoint)
local hitNormalObjectSpace = hitPart.CFrame:vectorToObjectSpace(hitNormal)
if not NO_BULLET_DECALS and
hitPart and
not hitPart.Parent or not hitPart.Parent:FindFirstChildOfClass("Humanoid") and
hitPointObjectSpace and
hitNormalObjectSpace and
self.hitMarkTemplate
then
-- Clone hitMark (this contains all the decals/billboards/models to show on the hit surface)
local hitMark = self.hitMarkTemplate:Clone()
hitMark.Parent = hitPart
CollectionService:AddTag(hitMark, "WeaponsSystemIgnore")
-- Move/align hitMark to the hit surface
local incomingVec = parabola:sampleVelocity(1).Unit
if self:getConfigValue("AlignHitMarkToNormal", true) then
-- Make hitMark face straight out from surface where projectile hit (good for decals)
local forward = hitNormalObjectSpace
local up = incomingVec
local right = -forward:Cross(up).Unit
up = forward:Cross(right)
local orientationCFrame = CFrame.fromMatrix(hitPointObjectSpace + hitNormalObjectSpace * 0.05, right, up, -forward)
hitMark.CFrame = hitPart.CFrame:toWorldSpace(orientationCFrame)
else
-- Make hitmark appear stuck in the hit surface from the direction the projectile came from (good for things like arrows)
hitMark.CFrame = hitPart.CFrame * CFrame.new(hitPointObjectSpace, hitPointObjectSpace + hitPart.CFrame:vectorToObjectSpace(incomingVec))
end
-- Weld hitMark to the hitPart
local weld = Instance.new("WeldConstraint")
weld.Part0 = hitMark
weld.Part1 = hitPart
weld.Parent = hitMark
-- Fade glow decal over time
local glowDecal = hitMark:FindFirstChild("Glow")
if glowDecal then
coroutine.wrap(function()
local heartbeat = RunService.Heartbeat
for i = 0, 1, 1/60 do
heartbeat:Wait()
glowDecal.Transparency = (i ^ 2)
end
end)()
end
-- Set bullethole decal color and fade over time
local bulletHole = hitMark:FindFirstChild("BulletHole")
if bulletHole then
bulletHole.Color3 = hitPartColor
TweenService:Create(
bulletHole,
TweenInfo.new(1, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, false, 4),
{ Transparency = 1 }
):Play()
end
-- Fade impact billboard's size and transparency over time
local impactBillboard = hitMark:FindFirstChild("ImpactBillboard")
if impactBillboard then
local impact = impactBillboard:FindFirstChild("Impact")
local impactTween = TweenService:Create(
impact,
TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out, 0, false, 0),
{ Size = UDim2.new(1, 0, 1, 0) }
)
impactTween.Completed:Connect(function()
TweenService:Create(
impact,
TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out, 0, false, 0),
{ Size = UDim2.new(0.5, 0, 0.5, 0), ImageTransparency = 1 }
):Play()
end)
impactTween:Play()
end
-- Destroy hitMark in 5 seconds
Debris:AddItem(hitMark, 5)
end
flyingVisualEffectsFinished = true
visualEffectsFinishTime = now + visualEffectsLingerTime
end
end
-- Will enter this if-statement if projectile hit something or maxDistance has been reached
if projFront >= maxDist then
if not stoppedMotion then
stoppedMotion = true
stoppedMotionAt = now
end
-- Stop particle effects if projectile didn't hit anything and projBack has reached the end
if projBack >= maxDist and not flyingVisualEffectsFinished then
flyingVisualEffectsFinished = true
visualEffectsFinishTime = now + visualEffectsLingerTime
end
end
-- Update parabola domain
parabola:setDomain(projBack, projFront)
-- Continue updating pTravelDistance until projBack has reached maxDist (this helps with some visual effects)
if projBack < maxDist then
pTravelDistance = math.max(0, timeSinceStart * bulletSpeed)
end
-- Update visual effects each frame
-- Update CFrame/velocity of projectile if the projectile uses a model (such as rocket or grenade)
if shouldMovePart then
local bulletPos = parabola:samplePoint(1)
local bulletVelocity = parabola:sampleVelocity(1)
bulletEffect.CFrame = CFrame.new(bulletPos, bulletPos + bulletVelocity)
bulletEffect.Velocity = bulletVelocity.Unit * bulletSpeed
end
-- Update thickness and render trailing beams
local thickness0 = beamThickness0
local thickness1 = beamThickness1
if beamFadeTime then
-- Fade out trail beams if projectile is no longer moving (hit something or reached max distance)
local timeSinceEnd = stoppedMotion and (now - stoppedMotionAt) or 0
local fadeAlpha = math.clamp(timeSinceEnd / beamFadeTime, 0, 1)
thickness0 = thickness0 * (1 - fadeAlpha)
thickness1 = thickness1 * (1 - fadeAlpha)
end
if beam0 then
beam0.Width0 = thickness0
beam0.Width1 = thickness1
parabola:renderToBeam(beam0)
end
if beam1 then
beam1.Width0 = thickness0
beam1.Width1 = thickness1
parabola:renderToBeam(beam1)
end
-- Disable muzzle flash after muzzleFlashTime seconds have passed
if muzzleFlashShown and timeSinceStart > muzzleFlashTime and self.muzzleFlashBeam then
self.muzzleFlashBeam.Enabled = false
muzzleFlashShown = false
end
-- Destroy projectile and attached visual effects when visual effects are done showing or max bullet time has been reached
local timeSinceParticleEffectsFinished = now - visualEffectsFinishTime
if (flyingVisualEffectsFinished and timeSinceParticleEffectsFinished > 0) or timeSinceStart > MAX_BULLET_TIME then
if bulletEffect then
bulletEffect:Destroy()
bulletEffect = nil
end
stepConn:Disconnect()
end
end
stepConn = RunService.Heartbeat:Connect(steppedCallback)
-- Get rid of charge on chargeable weapons
if not IsServer and self.usesCharging then
self.charge = math.clamp(self.charge - self:getConfigValue("FireDischarge", 1), 0, 1)
end
end
function BulletWeapon:calculateDamage(travelDistance)
local zeroDamageDistance = self:getConfigValue("ZeroDamageDistance", 10000)
local fullDamageDistance = self:getConfigValue("FullDamageDistance", 1000)
local distRange = zeroDamageDistance - fullDamageDistance
local falloff = math.clamp(1 - (math.max(0, travelDistance - fullDamageDistance) / math.max(1, distRange)), 0, 1)
return math.max(self:getConfigValue("HitDamage", 10) * falloff, 0)
end
function BulletWeapon:applyDamage(hitInfo)
local damage = self:calculateDamage(hitInfo.d)
if damage <= 0 then
return
end
self.weaponsSystem.doDamage(hitInfo.h, damage, nil, self.player)
end
function BulletWeapon:onHit(hitInfo)
local hitPoint = hitInfo.p
local hitNormal = hitInfo.n
local hitPart = hitInfo.part
if hitPart and hitPart.Parent then
local humanoid = self.weaponsSystem.getHumanoid(hitPart)
hitInfo.h = humanoid or hitPart
if IsServer and
(not hitInfo.h:IsA("Humanoid") or
self.weaponsSystem.playersOnDifferentTeams(self.weaponsSystem.getPlayerFromHumanoid(hitInfo.h), self.player))
then
self:applyDamage(hitInfo)
elseif hitInfo.h:IsA("Humanoid") and
hitInfo.h:GetState() ~= Enum.HumanoidStateType.Dead and
self.weaponsSystem.gui and
self.player == Players.LocalPlayer and
self.weaponsSystem.playersOnDifferentTeams(self.weaponsSystem.getPlayerFromHumanoid(hitInfo.h), self.player)
then
-- Show hit indicators on gui of client that shot projectile if players are not on same team
self.weaponsSystem.gui:OnHitOtherPlayer(self:calculateDamage(hitInfo.d), hitInfo.h)
end
end
-- Create invisible explosion on server that deals damage to anything caught in the explosion
if IsServer and self:getConfigValue("ExplodeOnImpact", false) then
local blastRadius = self:getConfigValue("BlastRadius", 8)
local blastPressure = self:getConfigValue("BlastPressure", 10000)
local blastDamage = self:getConfigValue("BlastDamage", 100)
local explosion = Instance.new("Explosion")
explosion.Position = hitPoint + (hitNormal * 0.5)
explosion.BlastRadius = blastRadius
explosion.BlastPressure = blastPressure
explosion.ExplosionType = Enum.ExplosionType.NoCraters
explosion.DestroyJointRadiusPercent = 0
explosion.Visible = false
explosion.Hit:Connect(function(explodedPart, hitDist)
local damageMultiplier = (1 - math.clamp((hitDist / blastRadius), 0, 1))
local damageToDeal = blastDamage * damageMultiplier
local humanoid = self.weaponsSystem.getHumanoid(explodedPart)
if humanoid then
if explodedPart.Name == "UpperTorso" and
humanoid:GetState() ~= Enum.HumanoidStateType.Dead and
self.weaponsSystem.playersOnDifferentTeams(self.weaponsSystem.getPlayerFromHumanoid(humanoid), self.player)
then
-- Do damage to players/humanoids
self.weaponsSystem.doDamage(humanoid, damageToDeal, nil, self.player)
end
elseif not CollectionService:HasTag(explodedPart, "WeaponsSystemIgnore") then
-- Do damage to a part (sends damage to breaking system)
self.weaponsSystem.doDamage(explodedPart, damageToDeal, nil, self.player)
end
end)
explosion.Parent = workspace
end
end
function BulletWeapon:fire(origin, dir, charge)
if not self:isCharged() then
return
end
BaseWeapon.fire(self, origin, dir, charge)
end
function BulletWeapon:onFired(firingPlayer, fireInfo, fromNetwork)
if not IsServer and firingPlayer == Players.LocalPlayer and fromNetwork then
return
end
local cooldownTime = self:getConfigValue("ShotCooldown", 0.1)
local fireMode = self:getConfigValue("FireMode", "Semiautomatic")
local isSemiAuto = fireMode == "Semiautomatic"
local isBurst = fireMode == "Burst"
if isBurst and not self.burstFiring then
self.burstIdx = 0
self.burstFiring = true
elseif isSemiAuto then
self.triggerDisconnected = true
end
-- Calculate cooldown time for burst firing
if self.burstFiring then
self.burstIdx = self.burstIdx + 1
if self.burstIdx >= self:getConfigValue("NumBurstShots", 3) then
self.burstFiring = false
self.triggerDisconnected = true
else
cooldownTime = self:getConfigValue("BurstShotCooldown", nil) or cooldownTime
end
end
self.nextFireTime = tick() + cooldownTime
BaseWeapon.onFired(self, firingPlayer, fireInfo, fromNetwork)
end
function BulletWeapon:onConfigValueChanged(valueName, newValue, oldValue)
BaseWeapon.onConfigValueChanged(self, valueName, newValue, oldValue)
if valueName == "ShotEffect" then
self.bulletEffectTemplate = ShotsFolder:FindFirstChild(self:getConfigValue("ShotEffect", "Bullet"))
if self.bulletEffectTemplate then
local config = self.bulletEffectTemplate:FindFirstChildOfClass("Configuration")
if config then
self:importConfiguration(config)
end
local beam0 = self.bulletEffectTemplate:FindFirstChild("Beam0")
if beam0 then
coroutine.wrap(function()
ContentProvider:PreloadAsync({ beam0 })
end)()
end
end
elseif valueName == "HitMarkEffect" then
self.hitMarkTemplate = HitMarksFolder:FindFirstChild(self:getConfigValue("HitMarkEffect", "BulletHole"))
if self.hitMarkTemplate then
local config = self.hitMarkTemplate:FindFirstChildOfClass("Configuration")
if config then
self:importConfiguration(config)
end
end
elseif valueName == "CasingEffect" then
self.casingTemplate = CasingsFolder:FindFirstChild(self:getConfigValue("CasingEffect", ""))
if self.casingTemplate then
local config = self.casingTemplate:FindFirstChildOfClass("Configuration")
if config then
self:importConfiguration(config)
end
end
elseif valueName == "ChargeRate" then
self.usesCharging = newValue ~= nil
end
end
function BulletWeapon:onActivatedChanged()
BaseWeapon.onActivatedChanged(self)
if not IsServer then
-- Reload if no ammo left in clip
if self.equipped and self:getAmmoInWeapon() <= 0 then
self:reload()
return
end
-- Fire weapon
if self.activated and self.player == localPlayer and self:canFire() and tick() > self.nextFireTime then
self:doLocalFire()
end
-- Reenable trigger after activated changes to false
if not self.activated and self.triggerDisconnected and not self.burstFiring then
self.triggerDisconnected = false
end
end
end
function BulletWeapon:onRenderStepped(dt)
BaseWeapon.onRenderStepped(self, dt)
if not self.tipAttach then return end
if not self.equipped then return end
local tipCFrame = self.tipAttach.WorldCFrame
if self.player == Players.LocalPlayer then
-- Retrieve aim point from camera and update player's aim animation
local aimTrack = self:getAnimTrack(self:getConfigValue("AimTrack", "RifleAim"))
local aimZoomTrack = self:getAnimTrack(self:getConfigValue("AimZoomTrack", "RifleAimDownSights"))
if aimTrack then
local aimDir = tipCFrame.LookVector
local gunLookRay = Ray.new(tipCFrame.p, aimDir * 500)
local _, gunHitPoint = Roblox.penetrateCast(gunLookRay, self.ignoreList)
if self.weaponsSystem.aimRayCallback then
local _, hitPoint = Roblox.penetrateCast(self.weaponsSystem.aimRayCallback(), self.ignoreList)
self.aimPoint = hitPoint
else
self.aimPoint = gunHitPoint
end
if not aimTrack.IsPlaying and not self.reloading then
aimTrack:Play(0.15)
coroutine.wrap(function() -- prevent player from firing until gun is fully out
wait(self:getConfigValue("StartupTime", 0.2))
self.startupFinished = true
end)()
end
if aimZoomTrack and not self.reloading then
if not aimZoomTrack.IsPlaying then
aimZoomTrack:Play(0.15)
end
aimZoomTrack:AdjustSpeed(0.001)
if self.weaponsSystem.camera:isZoomed() then
if aimTrack.WeightTarget ~= 0 then
aimZoomTrack:AdjustWeight(1)
aimTrack:AdjustWeight(0)
end
elseif aimTrack.WeightTarget ~= 1 then
aimZoomTrack:AdjustWeight(0)
aimTrack:AdjustWeight(1)
end
end
local MIN_ANGLE = -80
local MAX_ANGLE = 80
local aimYAngle = math.deg(self.recoilIntensity)
if self.weaponsSystem.camera.enabled then
-- Gets pitch and recoil from camera to figure out how high/low to aim the gun
aimYAngle = math.deg(self.weaponsSystem.camera:getRelativePitch() + self.weaponsSystem.camera.currentRecoil.Y + self.recoilIntensity)
end
local aimTimePos = 2 * ((aimYAngle - MIN_ANGLE) / (MAX_ANGLE - MIN_ANGLE))
aimTrack:AdjustSpeed(0.001)
aimTrack.TimePosition = math.clamp(aimTimePos, 0.001, 1.97)
if aimZoomTrack then
aimZoomTrack.TimePosition = math.clamp(aimTimePos, 0.001, 1.97)
end
-- Update recoil (decay over time)
local recoilDecay = self:getConfigValue("RecoilDecay", 0.825)
self.recoilIntensity = math.clamp(self.recoilIntensity * recoilDecay, 0, math.huge)
else
warn("no aimTrack")
end
end
end
function BulletWeapon:setChargingParticles(charge)
local ratePerCharge = self:getConfigValue("ChargingParticlesRatePerCharge", 20)
local rate = ratePerCharge * charge
for _, v in pairs(self.chargingParticles) do
v.Rate = rate
end
end
function BulletWeapon:onStepped(dt)
if not self.tipAttach then return end
if not self.equipped then return end
BaseWeapon.onStepped(self, dt)
local now = tick()
local chargingSound = self:getSound("Charging")
local dischargingSound = self:getSound("Discharging")
if self.usesCharging then
-- Update charge amount
local chargeBefore = self.charge
self:handleCharging(dt)
local chargeDelta = self.charge - chargeBefore
-- Update charge particles
if chargeDelta > 0 then
self:setChargingParticles(self.charge)
else
self:setChargingParticles(0)
end
-- Play charging sounds
if chargingSound then
if chargingSound.Looped then
if chargeDelta < 0 then
chargingSound:Stop()
else
if not chargingSound.Playing and self.charge < 1 and chargeDelta > 0 then
chargingSound:Play()
end
chargingSound.PlaybackSpeed = self.chargeSoundPitchMin + (self.charge * (self.chargeSoundPitchMax - self.chargeSoundPitchMin))
end
else
if chargeDelta > 0 and self.charge <= 1 and not chargingSound.Playing then
chargingSound.TimePosition = chargingSound.TimeLength * self.charge
chargingSound:Play()
elseif chargeDelta <= 0 and chargingSound.Playing then
chargingSound:Stop()
end
end
end
if dischargingSound then
if dischargingSound.Looped then
if chargeDelta > 0 then
dischargingSound:Stop()
else
if not dischargingSound.Playing and self.charge > 0 then
dischargingSound:Play()
end
dischargingSound.PlaybackSpeed = self.chargeSoundPitchMin + (self.charge * (self.chargeSoundPitchMax - self.chargeSoundPitchMin))
end
else
if chargeDelta < 0 and self.charge >= 0 and not dischargingSound.Playing then
dischargingSound.TimePosition = dischargingSound.TimeLength * self.charge
dischargingSound:Play()
elseif chargeDelta >= 0 and dischargingSound.Playing then
dischargingSound:Stop()
end
end
end
-- Play charge/discharge completed sounds and particle effects
if chargeBefore < 1 and self.charge >= 1 then
local chargeCompleteSound = self:getSound("ChargeComplete")
if chargeCompleteSound then
chargeCompleteSound:Play()
end
if chargingSound and chargingSound.Playing then
chargingSound:Stop()
end
if self.chargeCompleteParticles then
self.chargeCompleteParticles:Emit(self:getConfigValue("NumChargeCompleteParticles", 25))
end
end
if chargeBefore > 0 and self.charge <= 0 then
local dischargeCompleteSound = self:getSound("DischargeComplete")
if dischargeCompleteSound then
dischargeCompleteSound:Play()
end
if dischargingSound and dischargingSound.Playing then
dischargingSound:Stop()
end
if self.dischargeCompleteParticles then
self.dischargeCompleteParticles:Emit(self:getConfigValue("NumDischargeCompleteParticles", 25))
end
end
self:renderCharge()
else
if chargingSound then
chargingSound:Stop()
end
if dischargingSound then
dischargingSound:Stop()
end
end
if self.usesCharging and self.chargeGlowPart then
self.chargeGlowPart.Transparency = 1 - self.charge
end
-- Fire weapon if it is fully charged
if self:canFire() and now > self.nextFireTime then
self:doLocalFire()
end
end
function BulletWeapon:handleCharging(dt)
local chargeDelta
local shouldCharge = self.activated or self.burstFiring or self:getConfigValue("ChargePassively", false)
if self.reloading or self.triggerDisconnected then
shouldCharge = false
end
if shouldCharge then
chargeDelta = self:getConfigValue("ChargeRate", 0) * dt
else
chargeDelta = self:getConfigValue("DischargeRate", 0) * -dt
end
self.charge = math.clamp(self.charge + chargeDelta, 0, 1)
end
function BulletWeapon:isCharged()
return not self.usesCharging or self.charge >= 1
end
function BulletWeapon:canFire()
return self.player == Players.LocalPlayer and (self.burstFiring or self.activated) and not self.triggerDisconnected and not self.reloading and self:isCharged() and self.startupFinished
end
function BulletWeapon:doLocalFire()
if self.tipAttach then
local tipCFrame = self.tipAttach.WorldCFrame
local tipPos = tipCFrame.Position
local aimDir = (self.aimPoint - tipPos).Unit
self:fire(tipPos, aimDir, self.charge)
end
end
return BulletWeapon