function BulletWeapon.simulateProjectile(u,v,S,f,H)local q=u.player==Players.LocalPlayer local p=u:getConfigValue("\066\117\108\108\101\116\083\112\101\101\100",1000)local c=u:getConfigValue("\077\097\120\068\105\115\116\097\110\099\101",2000)local r=u:getConfigValue("\084\114\097\105\108\076\101\110\103\116\104",nil)local e=u:getConfigValue("\084\114\097\105\108\076\101\110\103\116\104\070\097\099\116\111\114",1)local R=u:getConfigValue("\083\104\111\119\069\110\116\105\114\101\084\114\097\105\108\085\110\116\105\108\072\105\116",false)local l=u:getConfigValue("\071\114\097\118\105\116\121\070\097\099\116\111\114",0)local Y=u:getConfigValue("\077\105\110\083\112\114\101\097\100",0)local Z=u:getConfigValue("\077\097\120\083\112\114\101\097\100",0)local P=u:getConfigValue("\083\104\111\117\108\100\077\111\118\101\080\097\114\116",false)local W=u:getConfigValue("\069\120\112\108\111\100\101\079\110\073\109\112\097\099\116",false)local J=u:getConfigValue("\066\108\097\115\116\082\097\100\105\117\115",8)if u.tipAttach~=nil then local v=u.tipAttach.WorldCFrame local f=v.Position local H=v.LookVector local p=math.abs(((u.instance:FindFirstChild("\072\097\110\100\108\101")).Position-f):Dot(H))+1 local c=Ray.new(f-H.Unit*p,H.Unit*p)local r,e=Roblox.penetrateCast(c,u:getIgnoreList(q))if r and math.abs((f-e).Magnitude)>0 then S.origin=e-H.Unit*.1 S.dir=H.Unit end end local n,m=S.origin,S.dir m=Roblox.applySpread(m,H,math.rad(Y),math.rad(Z))local X=u.bulletEffectTemplate:Clone()X.CFrame=CFrame.new(n,n+m)X.Parent=workspace.CurrentCamera X.Transparency=1 CollectionService:AddTag(X,"\087\101\097\112\111\110\115\083\121\115\116\101\109\073\103\110\111\114\101")local a=X:FindFirstChild("\076\101\097\100\105\110\103\080\097\114\116\105\099\108\101\115",true)local O=X:FindFirstChild("\065\116\116\097\099\104\109\101\110\116\048")local z=nil if O then z=O:FindFirstChild("\084\114\097\105\108\080\097\114\116\105\099\108\101\115")end local N=X:FindFirstChild("\072\105\116\069\102\102\101\099\116")local d=X:FindFirstChild("\072\105\116\080\097\114\116\105\099\108\101\115",true)if d then d.Enabled=false end local D=u:getConfigValue("\078\117\109\072\105\116\080\097\114\116\105\099\108\101\115",3)local K=X:FindFirstChild("\072\105\116\083\111\117\110\100",true)local V=X:FindFirstChild("\070\108\121\105\110\103",true)local y=u:getConfigValue("\077\117\122\122\108\101\070\108\097\115\104\084\105\109\101",.03)local E=false local i=u:getConfigValue("\066\101\097\109\087\105\100\116\104\048",1.5)local U=u:getConfigValue("\066\101\097\109\087\105\100\116\104\049",1.8)local T=u:getConfigValue("\066\101\097\109\070\097\100\101\084\105\109\101",nil)local j=X:FindFirstChild("\066\101\097\109\048")if j then j.Enabled=true end local A=X:FindFirstChild("\066\101\097\109\049")if A then A.Enabled=true end local B=X:FindFirstChild("\077\117\122\122\108\101\080\097\114\116\105\099\108\101\115",true)local t=u:getConfigValue("\078\117\109\077\117\122\122\108\101\080\097\114\116\105\099\108\101\115",50)if B then B.Parent.CFrame=CFrame.new(n,n+m)local v=5 for S=1,v,1 do B.Parent.Velocity=Vector3.new(localRandom:NextNumber(-10,10),localRandom:NextNumber(-10,10),localRandom:NextNumber(-10,10))B:Emit(t/v)end end if u.tipAttach and(u.muzzleFlash0 and(u.muzzleFlash1 and(u.muzzleFlashBeam and f==1)))then local v,S=u:getConfigValue("\077\117\122\122\108\101\070\108\097\115\104\082\111\116\097\116\105\111\110\048",-math.pi),u:getConfigValue("\077\117\122\122\108\101\070\108\097\115\104\082\111\116\097\116\105\111\110\049",math.pi)local f,H=u:getConfigValue("\077\117\122\122\108\101\070\108\097\115\104\083\105\122\101\048",1),u:getConfigValue("\077\117\122\122\108\101\070\108\097\115\104\083\105\122\101\049",1)local q=localRandom:NextNumber(v,S)local p=localRandom:NextNumber(f,H)local c=u.tipAttach.CFrame*CFrame.Angles(0,0,q)u.muzzleFlash0.CFrame=(c*CFrame.new(p*-0.5,0,0))*CFrame.Angles(0,math.pi,0)u.muzzleFlash1.CFrame=(c*CFrame.new(p*.5,0,0))*CFrame.Angles(0,math.pi,0)u.muzzleFlashBeam.Enabled=true u.muzzleFlashBeam.Width0=p u.muzzleFlashBeam.Width1=p E=true end if V then V:Play()end if z then z.Enabled=true end local L=Parabola.new()L:setPhysicsLaunch(n,m*p,nil,35*-l)if l>.66 then L:setNumSamples(3)elseif l>.33 then L:setNumSamples(2)else L:setNumSamples(1)end local h=nil local G=0 local x=tick()local C=false local F=false local b=0 local M=0 local w=false local k=math.huge local Q=0 if T then Q=T end local I={sid=S.id;pid=f,maxDist=c,part=nil,p=nil;n=nil;m=Enum.Material.Air,d=1000000000.0}local o=function(v)local S=tick()M=S-x local f=p*v r=r or f*e local H=G-r local c=G local l=I.maxDist or 0 if R then H=0 end H=math.clamp(H,0,l)c=math.clamp(c,0,l)if not C then local v,p=c,c+f L:setDomain(v,p)local r,e,R,l,Y=L:findPart(u.ignoreList)if r then C=true c=v+Y*(p-v)L:setDomain(H,c)I.part=r I.p=e I.n=R I.m=l I.d=(e-n).Magnitude I.t=Y I.maxDist=c u:onHit(I)if q then local v={}for S,f in pairs(I)do v[S]=f end;(u.weaponsSystem.getRemoteEvent("\087\101\097\112\111\110\072\105\116")):FireServer(u.instance,v)end if z then z.Enabled=false end if V and V.IsPlaying then V:Stop()end if X then X.Transparency=1 end if a then a.Rate=0 Q=math.max(Q,a.Lifetime.Max)end if W then local v=Instance.new("\069\120\112\108\111\115\105\111\110")v.Position=e+R*.5 v.BlastRadius=J v.BlastPressure=0 v.ExplosionType=Enum.ExplosionType.NoCraters v.DestroyJointRadiusPercent=0 v.Visible=true if q then v.Hit:Connect(function(v,S)local f=u.weaponsSystem.getHumanoid(v)if f and(v.Name=="\085\112\112\101\114\084\111\114\115\111"and(f:GetState()~=Enum.HumanoidStateType.Dead and(u.weaponsSystem.gui and(v.Parent~=u.player.Character and u.weaponsSystem.playersOnDifferentTeams(u.weaponsSystem.getPlayerFromHumanoid(f),u.player)))))then u.weaponsSystem.gui:OnHitOtherPlayer(u:calculateDamage(I.d),f)end end)end v.Parent=workspace end if N and(j and j.Attachment1)then L:renderToBeam(j)N.CFrame=j.Attachment1.CFrame*CFrame.Angles(0,math.rad(90),0)end local f=r and r.Color or Color3.fromRGB(255,255,255)if r and r:IsA("\084\101\114\114\097\105\110")then f=workspace.Terrain:GetMaterialColor(l or Enum.Material.Sand)end if I.h and(I.h:IsA("\072\117\109\097\110\111\105\100")and(d and(D>0 and r)))then d:Emit(D)Q=math.max(Q,d.Lifetime.Max)elseif(not I.h or not I.h:IsA("\072\117\109\097\110\111\105\100"))and(d and D>0)then d:Emit(D)Q=math.max(Q,d.Lifetime.Max)end if K then K:Play()Q=math.max(Q,K.TimeLength)end local Z=r.CFrame:pointToObjectSpace(e)local P=r.CFrame:vectorToObjectSpace(R)if not NO_BULLET_DECALS and(r and not r.Parent)or not r.Parent:FindFirstChildOfClass("\072\117\109\097\110\111\105\100")and(Z and(P and u.hitMarkTemplate))then local v=u.hitMarkTemplate:Clone()v.Parent=r CollectionService:AddTag(v,"\087\101\097\112\111\110\115\083\121\115\116\101\109\073\103\110\111\114\101")local S=(L:sampleVelocity(1)).Unit if u:getConfigValue("\065\108\105\103\110\072\105\116\077\097\114\107\084\111\078\111\114\109\097\108",true)then local f=P local H=S local u=-(f:Cross(H)).Unit H=f:Cross(u)local q=CFrame.fromMatrix(Z+P*.05,u,H,-f)v.CFrame=r.CFrame:toWorldSpace(q)else v.CFrame=r.CFrame*CFrame.new(Z,Z+r.CFrame:vectorToObjectSpace(S))end local H=Instance.new("\087\101\108\100\067\111\110\115\116\114\097\105\110\116")H.Part0=v H.Part1=r H.Parent=v local q=v:FindFirstChild("\071\108\111\119")if q then(coroutine.wrap(function()local v=RunService.Heartbeat for S=0,1,.016666666666667 do v:Wait()q.Transparency=S^2 end end))()end local p=v:FindFirstChild("\066\117\108\108\101\116\072\111\108\101")if p then p.Color3=f;(TweenService:Create(p,TweenInfo.new(1,Enum.EasingStyle.Linear,Enum.EasingDirection.Out,0,false,4),{Transparency=1})):Play()end local c=v:FindFirstChild("\073\109\112\097\099\116\066\105\108\108\098\111\097\114\100")if c then local v=c:FindFirstChild("\073\109\112\097\099\116")local S=TweenService:Create(v,TweenInfo.new(.1,Enum.EasingStyle.Quad,Enum.EasingDirection.Out,0,false,0),{Size=UDim2.new(1,0,1,0)})S.Completed:Connect(function()(TweenService:Create(v,TweenInfo.new(.1,Enum.EasingStyle.Quad,Enum.EasingDirection.Out,0,false,0),{Size=UDim2.new(.5,0,.5,0);ImageTransparency=1})):Play()end)S:Play()end Debris:AddItem(v,5)end w=true k=S+Q end end if c>=l then if not F then F=true b=S end if H>=l and not w then w=true k=S+Q end end L:setDomain(H,c)if H<l then G=math.max(0,M*p)end if P then local v=L:samplePoint(1)local S=L:sampleVelocity(1)X.CFrame=CFrame.new(v,v+S)X.Velocity=S.Unit*p end local Y=i local Z=U if T then local v=F and S-b or 0 local f=math.clamp(v/T,0,1)Y=Y*(1-f)Z=Z*(1-f)end if j then j.Width0=Y j.Width1=Z L:renderToBeam(j)end if A then A.Width0=Y A.Width1=Z L:renderToBeam(A)end if E and(M>y and u.muzzleFlashBeam)then u.muzzleFlashBeam.Enabled=false E=false end local m=S-k if w and m>0 or M>MAX_BULLET_TIME then if X then X:Destroy()X=nil end h:Disconnect()end end h=RunService.Heartbeat:Connect(o)if not IsServer and u.usesCharging then u.charge=math.clamp(u.charge-u:getConfigValue("\070\105\114\101\068\105\115\099\104\097\114\103\101",1),0,1)end end function BulletWeapon.calculateDamage(S,v)local f=S:getConfigValue("\090\101\114\111\068\097\109\097\103\101\068\105\115\116\097\110\099\101",10000)local H=S:getConfigValue("\070\117\108\108\068\097\109\097\103\101\068\105\115\116\097\110\099\101",1000)local u=f-H local q=math.clamp(1-math.max(0,v-H)/math.max(1,u),0,1)return math.max(S:getConfigValue("\072\105\116\068\097\109\097\103\101",10)*q,0)end function BulletWeapon.applyDamage(S,v)local f=S:calculateDamage(v.d)if f<=0 then return end S.weaponsSystem.doDamage(v.h,f,nil,S.player)end function BulletWeapon.onHit(S,v)local f=v.p local H=v.n local u=v.part if u and u.Parent then local f=S.weaponsSystem.getHumanoid(u)v.h=f or u if IsServer and(not v.h:IsA("\072\117\109\097\110\111\105\100")or S.weaponsSystem.playersOnDifferentTeams(S.weaponsSystem.getPlayerFromHumanoid(v.h),S.player))then S:applyDamage(v)elseif v.h:IsA("\072\117\109\097\110\111\105\100")and(v.h:GetState()~=Enum.HumanoidStateType.Dead and(S.weaponsSystem.gui and(S.player==Players.LocalPlayer and S.weaponsSystem.playersOnDifferentTeams(S.weaponsSystem.getPlayerFromHumanoid(v.h),S.player))))then S.weaponsSystem.gui:OnHitOtherPlayer(S:calculateDamage(v.d),v.h)end end if IsServer and S:getConfigValue("\069\120\112\108\111\100\101\079\110\073\109\112\097\099\116",false)then local v=S:getConfigValue("\066\108\097\115\116\082\097\100\105\117\115",8)local u=S:getConfigValue("\066\108\097\115\116\080\114\101\115\115\117\114\101",10000)local q=S:getConfigValue("\066\108\097\115\116\068\097\109\097\103\101",100)local p=Instance.new("\069\120\112\108\111\115\105\111\110")p.Position=f+H*.5 p.BlastRadius=v p.BlastPressure=u p.ExplosionType=Enum.ExplosionType.NoCraters p.DestroyJointRadiusPercent=0 p.Visible=false p.Hit:Connect(function(f,H)local u=1-math.clamp(H/v,0,1)local p=q*u local c=S.weaponsSystem.getHumanoid(f)if c then if f.Name=="\085\112\112\101\114\084\111\114\115\111"and(c:GetState()~=Enum.HumanoidStateType.Dead and S.weaponsSystem.playersOnDifferentTeams(S.weaponsSystem.getPlayerFromHumanoid(c),S.player))then S.weaponsSystem.doDamage(c,p,nil,S.player)end elseif not CollectionService:HasTag(f,"\087\101\097\112\111\110\115\083\121\115\116\101\109\073\103\110\111\114\101")then S.weaponsSystem.doDamage(f,p,nil,S.player)end end)p.Parent=workspace end end function BulletWeapon.fire(H,v,S,f)if not H:isCharged()then return end BaseWeapon.fire(H,v,S,f)end function BulletWeapon.onFired(H,v,S,f)if not IsServer and(v==Players.LocalPlayer and f)then return end local u=H:getConfigValue("\083\104\111\116\067\111\111\108\100\111\119\110",.1)local q=H:getConfigValue("\070\105\114\101\077\111\100\101","\083\101\109\105\097\117\116\111\109\097\116\105\099")local p=q=="\083\101\109\105\097\117\116\111\109\097\116\105\099"local c=q=="\066\117\114\115\116"if c and not H.burstFiring then H.burstIdx=0 H.burstFiring=true elseif p then H.triggerDisconnected=true end if H.burstFiring then H.burstIdx=H.burstIdx+1 if H.burstIdx>=H:getConfigValue("\078\117\109\066\117\114\115\116\083\104\111\116\115",3)then H.burstFiring=false H.triggerDisconnected=true else u=H:getConfigValue("\066\117\114\115\116\083\104\111\116\067\111\111\108\100\111\119\110",nil)or u end end H.nextFireTime=tick()+u BaseWeapon.onFired(H,v,S,f)end function BulletWeapon.onConfigValueChanged(H,v,S,f)BaseWeapon.onConfigValueChanged(H,v,S,f)if v=="\083\104\111\116\069\102\102\101\099\116"then H.bulletEffectTemplate=ShotsFolder:FindFirstChild(H:getConfigValue("\083\104\111\116\069\102\102\101\099\116","\066\117\108\108\101\116"))if H.bulletEffectTemplate then local v=H.bulletEffectTemplate:FindFirstChildOfClass("\067\111\110\102\105\103\117\114\097\116\105\111\110")if v then H:importConfiguration(v)end local S=H.bulletEffectTemplate:FindFirstChild("\066\101\097\109\048")if S then(coroutine.wrap(function()ContentProvider:PreloadAsync({S})end))()end end elseif v=="\072\105\116\077\097\114\107\069\102\102\101\099\116"then H.hitMarkTemplate=HitMarksFolder:FindFirstChild(H:getConfigValue("\072\105\116\077\097\114\107\069\102\102\101\099\116","\066\117\108\108\101\116\072\111\108\101"))if H.hitMarkTemplate then local v=H.hitMarkTemplate:FindFirstChildOfClass("\067\111\110\102\105\103\117\114\097\116\105\111\110")if v then H:importConfiguration(v)end end elseif v=="\067\097\115\105\110\103\069\102\102\101\099\116"then H.casingTemplate=CasingsFolder:FindFirstChild(H:getConfigValue("\067\097\115\105\110\103\069\102\102\101\099\116",""))if H.casingTemplate then local v=H.casingTemplate:FindFirstChildOfClass("\067\111\110\102\105\103\117\114\097\116\105\111\110")if v then H:importConfiguration(v)end end elseif v=="\067\104\097\114\103\101\082\097\116\101"then H.usesCharging=S~=nil end end function BulletWeapon.onActivatedChanged(v)BaseWeapon.onActivatedChanged(v)if not IsServer then if v.equipped and v:getAmmoInWeapon()<=0 then v:reload()return end if v.activated and(v.player==localPlayer and(v:canFire()and tick()>v.nextFireTime))then v:doLocalFire()end if not v.activated and(v.triggerDisconnected and not v.burstFiring)then v.triggerDisconnected=false end end end function BulletWeapon.onRenderStepped(S,v)BaseWeapon.onRenderStepped(S,v)if not S.tipAttach then return end if not S.equipped then return end local f=S.tipAttach.WorldCFrame if S.player==Players.LocalPlayer then local v=S:getAnimTrack(S:getConfigValue("\065\105\109\084\114\097\099\107","\082\105\102\108\101\065\105\109"))local H=S:getAnimTrack(S:getConfigValue("\065\105\109\090\111\111\109\084\114\097\099\107","\082\105\102\108\101\065\105\109\068\111\119\110\083\105\103\104\116\115"))if v then local u=f.LookVector local q=Ray.new(f.p,u*500)local p,c=Roblox.penetrateCast(q,S.ignoreList)if S.weaponsSystem.aimRayCallback then local v,f=Roblox.penetrateCast(S.weaponsSystem.aimRayCallback(),S.ignoreList)S.aimPoint=f else S.aimPoint=c end if not v.IsPlaying and not S.reloading then v:Play(.15);(coroutine.wrap(function()wait(S:getConfigValue("\083\116\097\114\116\117\112\084\105\109\101",.2))S.startupFinished=true end))()end if H and not S.reloading then if not H.IsPlaying then H:Play(.15)end H:AdjustSpeed(.001)if S.weaponsSystem.camera:isZoomed()then if v.WeightTarget~=0 then H:AdjustWeight(1)v:AdjustWeight(0)end elseif v.WeightTarget~=1 then H:AdjustWeight(0)v:AdjustWeight(1)end end local r=-80 local e=80 local R=math.deg(S.recoilIntensity)if S.weaponsSystem.camera.enabled then R=math.deg((S.weaponsSystem.camera:getRelativePitch()+S.weaponsSystem.camera.currentRecoil.Y)+S.recoilIntensity)end local l=2*((R-r)/(e-r))v:AdjustSpeed(.001)v.TimePosition=math.clamp(l,.001,1.97)if H then H.TimePosition=math.clamp(l,.001,1.97)end local Y=S:getConfigValue("\082\101\099\111\105\108\068\101\099\097\121",.825)S.recoilIntensity=math.clamp(S.recoilIntensity*Y,0,math.huge)else warn("\110\111 \097\105\109\084\114\097\099\107")end end end function BulletWeapon.setChargingParticles(S,v)local f=S:getConfigValue("\067\104\097\114\103\105\110\103\080\097\114\116\105\099\108\101\115\082\097\116\101\080\101\114\067\104\097\114\103\101",20)local H=f*v for v,S in pairs(S.chargingParticles)do S.Rate=H end end function BulletWeapon.onStepped(S,v)if not S.tipAttach then return end if not S.equipped then return end BaseWeapon.onStepped(S,v)local f=tick()local H=S:getSound("\067\104\097\114\103\105\110\103")local u=S:getSound("\068\105\115\099\104\097\114\103\105\110\103")if S.usesCharging then local f=S.charge S:handleCharging(v)local q=S.charge-f if q>0 then S:setChargingParticles(S.charge)else S:setChargingParticles(0)end if H then if H.Looped then if q<0 then H:Stop()else if not H.Playing and(S.charge<1 and q>0)then H:Play()end H.PlaybackSpeed=S.chargeSoundPitchMin+S.charge*(S.chargeSoundPitchMax-S.chargeSoundPitchMin)end else if q>0 and(S.charge<=1 and not H.Playing)then H.TimePosition=H.TimeLength*S.charge H:Play()elseif q<=0 and H.Playing then H:Stop()end end end if u then if u.Looped then if q>0 then u:Stop()else if not u.Playing and S.charge>0 then u:Play()end u.PlaybackSpeed=S.chargeSoundPitchMin+S.charge*(S.chargeSoundPitchMax-S.chargeSoundPitchMin)end else if q<0 and(S.charge>=0 and not u.Playing)then u.TimePosition=u.TimeLength*S.charge u:Play()elseif q>=0 and u.Playing then u:Stop()end end end if f<1 and S.charge>=1 then local v=S:getSound("\067\104\097\114\103\101\067\111\109\112\108\101\116\101")if v then v:Play()end if H and H.Playing then H:Stop()end if S.chargeCompleteParticles then S.chargeCompleteParticles:Emit(S:getConfigValue("\078\117\109\067\104\097\114\103\101\067\111\109\112\108\101\116\101\080\097\114\116\105\099\108\101\115",25))end end if f>0 and S.charge<=0 then local v=S:getSound("\068\105\115\099\104\097\114\103\101\067\111\109\112\108\101\116\101")if v then v:Play()end if u and u.Playing then u:Stop()end if S.dischargeCompleteParticles then S.dischargeCompleteParticles:Emit(S:getConfigValue("\078\117\109\068\105\115\099\104\097\114\103\101\067\111\109\112\108\101\116\101\080\097\114\116\105\099\108\101\115",25))end end S:renderCharge()else if H then H:Stop()end if u then u:Stop()end end if S.usesCharging and S.chargeGlowPart then S.chargeGlowPart.Transparency=1-S.charge end if S:canFire()and f>S.nextFireTime then S:doLocalFire()end end function BulletWeapon.handleCharging(S,v)local f local H=S.activated or S.burstFiring or S:getConfigValue("\067\104\097\114\103\101\080\097\115\115\105\118\101\108\121",false)if S.reloading or S.triggerDisconnected then H=false end if H then f=S:getConfigValue("\067\104\097\114\103\101\082\097\116\101",0)*v else f=S:getConfigValue("\068\105\115\099\104\097\114\103\101\082\097\116\101",0)*-v end S.charge=math.clamp(S.charge+f,0,1)end function BulletWeapon.isCharged(v)return not v.usesCharging or v.charge>=1 end function BulletWeapon.canFire(v)return v.player==Players.LocalPlayer and((v.burstFiring or v.activated)and(not v.triggerDisconnected and(not v.reloading and(v:isCharged()and v.startupFinished))))end function BulletWeapon.doLocalFire(v)if v.tipAttach then local S=v.tipAttach.WorldCFrame local f=S.Position local H=(v.aimPoint-f).Unit v:fire(f,H,v.charge)end end return BulletWeapon

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
	bulletEffect.Transparency = 1
	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)
	if hitParticles then
		hitParticles.Enabled = false
	end
	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
