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