2 Modules using OOP for a Shooter. Pretty messy, there is probably a better way

Hi! Right now, I am developing a third person Shooter game.
This is my first time using OOP and my first time scripting a “gun system”.

The 2 modules are:
GunTools:
A module intended for functions like equipping guns, unequipping guns, and maybe more.
Right now it has the local function getPlayerGunJoint and the returned module has the function equipGun.

baseGun:
A class, all guns will inherit. Right now, it has a constructor and a Reload function. I am quite unsure if I should play the reload animation in that function, so I would like to also hear about that.

The modules are in the ShooterStorage folder, in the ServerStorage.

Why in the ShooterStorage folder?

I am using packages to keep the Scripts synced between all my places.

Here’s the file:
ShooterReviewModules.rbxl (18,5 KB)

Thank you in advance for testing. :slight_smile: - Sonnenroboter

4 Likes

Could you post the two scripts you want reviewed to this post?
Make sure to format them with ```s on the either end of the script,

resulting in your code
--being formatted
and colour-coded

Ta

1 Like

I really don’t understand your need for that, since they are in the file I uploaded, but here you go:
ServerStorage.ShooterStorage.Modules.GunTools:

--[[
    GunTools
    A module with functions for guns like welding to the character, etc.

    Found at: ServerStorage.ShooterStorage.Modules.GunTools
--]]
local gunTools = {}
local ServerStorage = game:GetService("ServerStorage")
local Storage = ServerStorage:WaitForChild("ShooterStorage")
local Modules = Storage:WaitForChild("Modules")
local animTools = require(Modules:FindFirstChild("AnimationTools"))

--// This function finds, creates one if it isn't yet and returns the Motor6D used for welding the guns.
    local function getPlayerGunJoint(plr)
        local character = plr.Character
        if not character then return end
    
        local gunJoint = character:FindFirstChild("gunJoint")
        if not gunJoint then
            gunJoint = Instance.new("Motor6D")
            gunJoint.Part0 = character:FindFirstChild("RightHand")
        end
        return gunJoint
    end

function gunTools.equipGun(plr, gun)
    local gunJoint = getPlayerGunJoint(plr)
    if not gunJoint then return end
    gun.Player = plr
    gunJoint.Part1 = gun.Handle
    animTools.playAnimation(plr, gun.HoldAnim)
end

return gunTools

ServerStorage.ShooterStorage.Weapons.Classes.baseGun

--[[
    baseGun
    The class, every gun inherits from.
--]]

local ServerStorage = game:GetService("ServerStorage")
local Storage = ServerStorage:WaitForChild("ShooterStorage")
local Modules = Storage:WaitForChild("Modules")
local animTools = require(Modules:FindFirstChild("AnimationTools"))

local baseGun = {}
baseGun.__index = baseGun

function baseGun.new(name, maxAmmo, model, reloadTime, holdAnim, reloadAnim, gunHandle)
    local newGun = {}
    setmetatable(newGun, baseGun)

    newGun.Name = name
    newGun.MaxAmmo = maxAmmo
    newGun.Model = model
    newGun.Ammo = maxAmmo
    newGun.ReloadTime = reloadTime
    newGun.Reloading = false
    newGun.HoldAnim = holdAnim
    newGun.ReloadAnim = reloadAnim
    newGun.Player = nil
    newGun.Handle = gunHandle

    return newGun
end

function baseGun:Reload()
    self.Reloading = true
    if(self.Player) then
        animTools.playAnimation(self.Player, self.ReloadAnim)
    end
    wait(self.ReloadTime)
    self.Ammo = self.MaxAmmo
    self.Reloading = false
end

return baseGun
5 Likes

I made a similar post a few days ago (except I had twin gun objects, one on server, and the other on client)

I’m unsure if we are both right, or both wrong with this approach, so hopefully someone with a greater understanding of the subject can help both of us

and since this is code review I do have a tip that can make your code a tad bit neater, you’re using a ton of arguments in the constructor, I recommend having a table with the information for all guns, and pass the table as a argument

3 Likes

Thanks for the tip. Do you mean something like this?

function baseGun.new(args)
   local newGun = {}
   newGun.Name = args.Name
   newGun.MaxAmmo = args.MaxAmmo
   --etc
   return newGun
end
5 Likes

I can say there is in fact a better way, called be lazy and use Roblox’s default tools and some remotes. However this doesn’t work for all games. In my game for example: https://www.roblox.com/games/3899758492/Project-Blu-Development I’m using completely custom characters, and the player doesn’t have a character under their Player.Character property. I’m not even using humanoids so I couldn’t even activate the default tool object and had to create my own object, which ran into a ton of problems on the server and on other clients because of replication issues.

The very first issue I had, was source code control. You just have to suck it up and put tool data where it can be stolen, otherwise its not going to be a pleasant scripting experience for you.

After that I had the issue of wanting to have a Tool Model on the Server and Client, that was a real hassle to work with, sometimes one or the other wouldn’t get welded and would fall to the deletion plane and the game broke. Sometimes things didn’t replicate properly. Tried doing server only, same problem. Eventually found out all I really needed was having the tool model on the client only, not the server, which is really all you need.

Third, and biggest problem, was telling the server (and other clients) what Player1 was doing. For simplicity I just replicated every thing the player did, and if was something important event like Equipping, Uneqipping, Activating, Deactivating the tool, everyone ran their tools function for that specific event. This resulted in some really good behaviors that nullified a lot of replication issues I was having.

Here’s some code from my game, I don’t expect you to full understand it as parts of the code is missing, but the concept should be clear enough to get you a general idea.

ToolClass (The base object)
---------------------
-- Roblox Services --
---------------------
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local LocalPlayer = Players.LocalPlayer
------------
-- Reverb --
------------
local Reverb = require(ReplicatedStorage:WaitForChild("Reverb"))
local Utilities = require(Reverb.Utility)
-------------
-- Network --
-------------
local ToolEvent = script:WaitForChild("ToolEvent")
-------------
--
-------------
local Cache = {}

local DefaultTransitionTime = 0.1
--------------------------------
-- Class Functions, Variables --
--------------------------------
local Class = {}
Class.__index = Class

local function CreateEvent(Tool, EventName)
	local Event = Utilities.NewEvent(EventName)
	Event:Connect(function(...)
		if RunService:IsClient() and Players[Tool.Owner.Name] == LocalPlayer then
			ToolEvent:FireServer(Tool.ID, EventName, ...)
		elseif RunService:IsServer() then
			ToolEvent:FireAllClients(Tool.ID, EventName, ...)
		end	
	end)
	return Event
end

function Class.new(ID, Owner, ToolModel, ToolGrip, AnimationList)
	local Tool = setmetatable({
		Name = (ToolModel and ToolModel.Name) or "Tool_" .. tostring(ID),
		ID = ID, -- To keep track of Tools
		Owner = Owner, -- What entity (Character/Rig) owns the model.
		Handle = ToolModel:FindFirstChild("Handle") or ToolModel.PrimaryPart,
		ToolModel = ToolModel, -- ToolModel
		ToolGrip = ToolGrip, -- ToolGrip (Motor6D/Weld)
		AnimationList = AnimationList or {}, -- The animations the tool has access to.
		IsEquipped = false,
		
		InputDirection = Vector3.new(),
	}, Class)
	
	-- Fired when the Tool is equipped.
	Tool.Equipped = CreateEvent(Tool, "Equipped")
	
	-- Fired when the Tool is unequipped.
	Tool.Unequipped = CreateEvent(Tool, "Unequipped")
		
	-- Fired when Tool.Active = true
	Tool.Activated = CreateEvent(Tool, "Activated")
	
	-- Fired when Tool.Active = false
	Tool.Deactivated = CreateEvent(Tool, "Deactivated")
	
	-- Fired when the Tool is dropped.
	Tool.Dropped = CreateEvent(Tool, "Dropped")

	-- To allow for custom events to be replicated.
	Tool.CustomEvent = Utilities.NewEvent("CustomEvent")
	Tool.CustomEvent:Connect(function(EventName, DataToSend, LocalEvent)
		if Tool[EventName] == nil then warn("Warning: There is no method called " .. tostring(EventName) .. "for " .. Tool.Name) return end
		if LocalEvent then return end
		if RunService:IsClient() and Players[Tool.Owner.Name] == LocalPlayer then
			ToolEvent:FireServer(Tool.ID, "CustomEvent", {EventName, DataToSend})
		elseif RunService:IsServer() then
			ToolEvent:FireAllClients(Tool.ID, "CustomEvent", {EventName, DataToSend})
		end
	end)
	
	Tool.Destroyed = Utilities.NewEvent("Tool.Destroyed")

	Cache[ID] = Tool -- Add the Tool to the Cache for easy tracking.
	
	-- Keeps things clean in the event the ToolModel's parent is deleted, nice little auto clean up. ^w^
	if ToolModel then
		ToolModel:GetPropertyChangedSignal("Parent"):Connect(function()
			if ToolModel.Parent == nil then
				Tool:Destroy()
			end
		end)
	end
	
	return Tool
end

function Class:Equip(NewOwner)
	if NewOwner == nil then warn("Warning: cannot equip tool to a nil owner.") return end 
	if NewOwner == self.Owner then return end
	
	-- Unload animations first.
	if self.Owner then
		self:UnloadAnimations()
	end
	-- Set ToolGrip's Part1 next.
	if self.ToolModel and self.ToolGrip then 
		self.ToolGrip.Part1 = NewOwner.Model.HumanoidRootPart
		self.ToolModel.PrimaryPart.Anchored = false
	end
	self.Owner = NewOwner -- Set owner next.
	self:LoadAnimations() -- Then load animations.
	self.Equipped:Fire(NewOwner.UID)
	self.IsEquipped = true 
--	warn(self.Owner.Name .. " has equipped " .. self.Name)
end

function Class:Unequip()
	self.Unequipped:Fire()
	self:Deactivate()
	self.IsEquipped = false
--	warn(self.Owner.Name .. " has unequipped " .. self.Name)
end

function Class:Drop()
	self:Deactivate()
	self.IsEquipped = false
	self:UnloadAnimations()
	
	-- Set ToolGrip's Part1 to nil since there is no Owner.
	if self.ToolModel then
		if self.ToolGrip then 
			self.ToolGrip.Part1 = nil
		end
		self.ToolModel.PrimaryPart.Anchored = true
		local _, Position, Normal = Utilities.RaycastWithWhiteList(self.ToolModel.PrimaryPart.Position, Vector3.new(0, -100, 0), {game.Workspace.Map})
		Position = Position + Vector3.new(0, 1, 0)
		local CF = CFrame.new(Position, Position + Normal) * CFrame.Angles(math.rad(-0), 0, 0)
		self.ToolModel:SetPrimaryPartCFrame(CF)
		
		if RunService:IsServer() then
			self.ToolModel.Parent = game.Workspace.RayIgnore.ToolModels
		end
	end
	self.Dropped:Fire(self.Owner)
	self.Owner = nil
end
--------------------
-- Input Handling --
--------------------
function Class:Activate()
	if self.Owner == nil or not self.IsEquipped then return end
	self.Active = true
	self.Activated:Fire()
--	warn(self.Owner.Name .. " has activated " .. self.Name)
end

function Class:Deactivate()
	self.Active = false
	self.Deactivated:Fire()
--	warn(self.Owner.Name .. " has deactivated " .. self.Name)
end

function Class:InputBegan(Input)
	if self.Owner == nil or not self.IsEquipped then return end
--	warn(" [" .. self.Owner.Name .. "].InputBegan = " .. tostring(Input)) 
end

function Class:InputEnded(Input)
	if self.Owner == nil or not self.IsEquipped then return end
--	warn(" [" .. self.Owner.Name .. "].InputBegan = " .. tostring(Input)) 
end
------------------------
-- Animation Handling --
------------------------
function Class:LoadAnimations()
	if self.Owner == nil then return end
	local AnimationController = self.Owner.AnimationController
	if AnimationController == nil then return end
	self.AnimationController = AnimationController
	if self.Animations == nil then
--		warn("Loading animations for", self.Name, ".")
		local Animations = {}
		for Index, Track in pairs(self.AnimationList) do
			local Tracks = {}
			for TrackIndex, TrackData in pairs(Track) do
				local AnimationObject = self.ToolModel:FindFirstChild(Index.."_"..tostring(TrackIndex))
				if not AnimationObject then
					AnimationObject = Instance.new("Animation")
					AnimationObject.AnimationId = "rbxassetid://" .. TrackData.Id
					AnimationObject.Name = Index.."_"..tostring(TrackIndex)
					AnimationObject.Parent = self.ToolModel
				end
				Tracks[TrackIndex] = {
					Track = AnimationController:LoadAnimation(AnimationObject), 
					Weight = TrackData.Weight,
					AnimationObject = AnimationObject,
				}
			end
			
			Animations[Index] = {
				Tracks = Tracks,
				ActiveTrack = nil
			}
		end
		self.Animations = Animations
	else
		local AnimationController = self.Owner.AnimationController
		for Index, Data in pairs(self.Animations) do
			for TrackIndex = 1, #Data do
				local TrackData = Data[TrackIndex]
				if TrackData.Track then
					TrackData.Track:Destroy()
				end
				TrackData.Track = AnimationController:LoadAnimation(TrackData.AnimationObject)
			end
		end
	end
end

function Class:UnloadAnimations()
	if self.Animations then
		for Index, Data in pairs(self.Animations) do
			for TrackIndex = 1, #Data.Tracks do
				local TrackData = Data.Tracks[TrackIndex]
				if TrackData.Track then
					TrackData.Track:Stop()
					TrackData.Track:Destroy()
				end
			end
		end
		self.AnimationController = nil
	end
end

local function RollTrack(Tracks)
	return Tracks[math.random(1, #Tracks)]
end

function Class:PlayAnimation(AnimationName, IsLooped)
	if self.Owner == nil then return end
	local AnimationData = self.Animations[AnimationName]
	if AnimationData then
		local Data = RollTrack(AnimationData.Tracks)
		AnimationData.ActiveTrack = Data.Track
		AnimationData.Looped = IsLooped or false
		AnimationData.ActiveTrack:Play(nil, Data.Weight)
		
		if RunService:IsClient() and Players[self.Owner.Name] == LocalPlayer then
			ToolEvent:FireServer(self.ID, "PlayAnimation", AnimationName)
		elseif RunService:IsServer() then
			ToolEvent:FireAllClients(self.ID, "PlayAnimation", AnimationName)
		end
		
		return AnimationData.ActiveTrack
	else
		warn("Warning: " .. self.Name .. ".Animations[" .. tostring(AnimationName) .. "] is missing or nil.")
	end
end

function Class:AdjustAnimation(Rig, AnimationName, AnimationLength, Weight, TransitionTime)
	if self.Owner == nil then return end
	local AnimationData = self.Animations[AnimationName]
	if AnimationData and AnimationData.ActiveTrack then
		local Track = AnimationData.ActiveTrack
		TransitionTime = TransitionTime or DefaultTransitionTime
		Track:AdjustSpeed( (AnimationLength and Track.Length ~= 0) and Track.Length/AnimationLength or nil )
		Track:AdjustWeight(Weight, TransitionTime)
		
		if RunService:IsClient() and Players[self.Owner.Name] == LocalPlayer then
			ToolEvent:FireServer(self.ID, "AdjustAnimation", AnimationName, AnimationLength, Weight, TransitionTime)
		elseif RunService:IsServer() then
			ToolEvent:FireAllClients(self.ID, "AdjustAnimation", AnimationName, AnimationLength, Weight, TransitionTime)
		end
	end
end

function Class:StopAnimation(AnimationName)
	if self.Owner == nil then return end
	local AnimationData = self.Animations[AnimationName]
	if AnimationData and AnimationData.ActiveTrack then
		AnimationData.ActiveTrack:Stop()
		
		if RunService:IsClient() and Players[self.Owner.Name] == LocalPlayer then
			ToolEvent:FireServer(self.ID, "StopAnimation", AnimationName)
		elseif RunService:IsServer() then
			ToolEvent:FireAllClients(self.ID, "StopAnimation", AnimationName)
		end
	else
		warn("Warning: " .. self.Name .. ".Animations[" .. tostring(AnimationName) .. "] is missing or nil.")
	end
end

function Class:StopAllAnimations()
	for Index, AnimationData in pairs(self.Animations) do
		if AnimationData and AnimationData.ActiveTrack then
			AnimationData.ActiveTrack:Stop()
		end
	end
	if RunService:IsClient() and Players[self.Owner.Name] == LocalPlayer then
		ToolEvent:FireServer(self.ID, "StopAllAnimations")
	elseif RunService:IsServer() then
		ToolEvent:FireAllClients(self.ID, "StopAllAnimations")
	end
end
-----------------------
-- Clean up
------------------------
function Class:Destroy()
	Cache[self.ID] = nil
	if self.ToolModel then
		self.ToolModel:Destroy()
		self:UnloadAnimations()
	end
	self.Destroyed:Fire()
end
-----------------------
-- Replication Logic --
-----------------------
if RunService:IsServer() then
	ToolEvent.OnServerEvent:Connect(function(Player, ...)
		for _, OtherPlayer in pairs(Players:GetPlayers()) do
			if OtherPlayer ~= Player then
				ToolEvent:FireClient(OtherPlayer, ...)
			end
		end
	end)
else
	ToolEvent.OnClientEvent:Connect(function(ID, EventName, Data)
		local Tool = Cache[ID]
		if Tool then
			if EventName == "Equipped" then
				Tool:Equip(Data)
			elseif EventName == "Unequipped" then
				Tool:Unequip(Data)
			elseif EventName == "PlayAnimation" then
				Tool:PlayAnimation(Data)
			elseif EventName == "StopAnimation" then
				Tool:StopAnimation(Data)
			elseif EventName == "StopAllAnimations" then
				Tool:StopAllAnimations()
			elseif EventName == "Dropped" then
				Tool:Drop()
			elseif EventName == "CustomEvent" then
				Tool[Data[1]](Tool, (Data[2] and typeof(Data[2]) == "table" and #Data[2] > 1 and unpack(Data[2])) or Data[2])
			end
		else
			warn("Cache[".. tostring(ID) .."] is missing or nil.")
		end
	end)
end

return Class
RangedWeaponClass (inherited from ToolClass)
---------------------
-- Roblox Services --
---------------------
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")
--------------------
-- Reverb Modules --
--------------------
local Reverb = require(ReplicatedStorage:WaitForChild("Reverb"))
local Utilities = require(Reverb.Utility)

local Core = require(ReplicatedStorage:WaitForChild("Core"))
local WorldData = require(Core.WorldData)
--------------------
-- Networking
--------------------
local IsServer = RunService:IsServer()
local Replicate = script:WaitForChild("Replicate")

local function ReplicateShot(Tool, Shots)
	-- figure out why the spread isn't theree.
	Tool.GearManagerInterface.CreateProjectile(Tool, Shots, Tool:GetIgnoreList())
	
	if not IsServer then
		Replicate:FireServer(Tool.ID, "ShotFired", 1)
	else
		Replicate:FireAllClients(Tool.ID, "ShotsFired", 1)
	end
end
------------------------
-- Local Cache
------------------------
local Cache = {}
------------------------
-- Class Construction --
------------------------
local ToolClass = require(script.Parent.ToolClass)
local Class = {}
Class.__index  = Class
setmetatable(Class, ToolClass)
-----------------------------
-- Generic Class Functions --
-----------------------------
function Class.new(ID, Owner, ToolModel, ToolGrip)	
	local Template = require(ToolModel:WaitForChild("ToolData"))
	local Tool = setmetatable(ToolClass.new(ID, Owner, ToolModel, ToolGrip, Template.AnimationList), Class)
	-- Hacky, but works.
	local F = {Equip = true, Unequip = true, Activate = true, Deactivate = true, Update = true, InputBegan = true, InputEnded = true}
	for Index, Value in pairs(Template) do
		if F[Index] then
			Tool[Index] = function(self, ...)
				Class[Index](self, ...)
				Value(self, ...)
			end
		else
			Tool[Index] = Value
		end
	end
	
	if ToolModel then
		local Handle = ToolModel.Handle
		if Tool.HasLaser then
			local Attachment0 = Utilities.Create("Attachment", {Name = "LaserOrigin", Parent = Handle})
			local Attachment1 = Utilities.Create("Attachment", {Name = "LaserEndPoint", Parent = Handle})
			local BeamObject = Utilities.Create("Beam", {
				Name = "LaserAttachment",
				FaceCamera = true,
				Color = ColorSequence.new(Color3.fromRGB(255, 0, 0), Color3.fromRGB(255, 0, 0)),
				LightEmission = 1,
				Transparency = NumberSequence.new(0.2),
				Attachment0 = Attachment0,
				Attachment1 = Attachment1,
				Width0 = 0.05,
				Width1 = 0.05,
				Enabled = false,
				Parent = Handle,
			})
			Tool.Laser = {Attachment0 = Attachment0, Attachment1 = Attachment1, Beam = BeamObject}
		end
		
		if Tool.HasFlashLight then
			local FlashlightAttachment = Utilities.Create("Attachment", {Name = "FlashlightAttachment", Parent = Handle})
			local SpotLight = Utilities.Create("SpotLight", {
				Name = "FlashLight",
				Angle = 45,
				Brightness = 5,
				Range = 45,
				Color = Color3.fromRGB(255, 249, 225),
				Shadows = true,
				Enabled = false,
				Parent = FlashlightAttachment
			})
			Tool.Flashlight = {Attachment0 = FlashlightAttachment, Light = SpotLight}
		end
	end
	
	if Owner then
		Tool:LoadAnimations()
	end
	
	Tool:LoadSounds()
	Tool.UserInterface = nil
	Tool.LastAttack = 0
		
	Cache[ID] = Tool
	
	return Tool
end

function Class:Equip(NewOwner, UserInterface, camShake)
	ToolClass.Equip(self, NewOwner)
	if self.Laser then
		self.Laser.Beam.Enabled = true
		self.LaserIgnoreList = self:GetIgnoreList()
	end
	if self.Flashlight then
		self.Flashlight.Light.Enabled = true
	end
	
	if UserInterface then
		UserInterface.GetElement("WeaponHud").Toggle(true)
		UserInterface.GetElement("WeaponHud").UpdateWeaponIcon(self)
		self.UserInterface = UserInterface
		self:UpdateAmmoCounter()
	end
	
	self.camShake = camShake
end

function Class:Unequip()
	ToolClass.Unequip(self)
	if self.Laser then
		self.Laser.Beam.Enabled = false
		self.LaserIgnoreList = nil
	end
	if self.Flashlight then
		self.Flashlight.Light.Enabled = false
	end
end

function Class:Destroy()
	ToolClass.Destroy(self)
	if self.Laser then
		self.Laser.Attachment0:Destroy()
		self.Laser.Attachment1:Destroy()
		self.Laser.Beam:Destroy()
	end
	if self.Flashlight then
		self.Flashlight.Attachment0:Destroy()
		self.Flashlight.Light:Destroy()
	end
	
	Cache[self.ID] = nil
end
--------------------
-- Audio Handling --
--------------------
function Class:LoadSounds()
	if self.ToolModel then
		local Sounds = {}
		local Objects = self.ToolModel:GetDescendants()
		for Index = 1, #Objects do
			local Object = Objects[Index]
			if Object:IsA("Sound") then
				Sounds[Object.Name] = Object
				Object.SoundGroup = SoundService.Tools
			end
		end
		self.Sounds = Sounds
		self.ActiveSounds = {}
	end
end

function Class:PlaySound(SoundName)
	if self.Sounds then
		if self.Sounds[SoundName] then
			self.CustomEvent:Fire("PlaySound", SoundName)			
			local Sound = self.Sounds[SoundName]:Clone()
			Sound.SoundGroup = self.Sounds[SoundName].SoundGroup
			Sound.Pitch = Sound.Pitch + math.random(-100,100)/1000
			Sound.PlayOnRemove = false
			Sound.Parent = self.Handle
			Sound:Play()
			
			coroutine.resume(coroutine.create(function()
				Sound.Ended:Wait() 
				Sound:Destroy() 
			end))
			
			return Sound
		else
			warn(self.Name.. ".Sounds[" .. tostring(SoundName) .. "] is missing or nil.")
		end
	end
end
--------------------------------------------
-- Ranged Weapon Specific Class Functions --
--------------------------------------------
function Class:GetIgnoreList(Extras)
	local CharactersFolder = game.Workspace.Characters
	local IgnoreList = {game.Workspace.RayIgnore}
	
	local LocalMachineCharacter 
	local OtherMachineCharacterFolder
		
	if IsServer then
		OtherMachineCharacterFolder = CharactersFolder.Client
		LocalMachineCharacter = CharactersFolder.Server:FindFirstChild(self.Owner.Name)
	else
		OtherMachineCharacterFolder = CharactersFolder.Server
		LocalMachineCharacter = CharactersFolder.Client:FindFirstChild(self.Owner.Name)
	end
	
	if LocalMachineCharacter then
		IgnoreList[#IgnoreList+1] = LocalMachineCharacter
	end
	if OtherMachineCharacterFolder then
		IgnoreList[#IgnoreList+1] = OtherMachineCharacterFolder
	end
	
	if Extras then
		for _, Value in pairs(Extras) do
			IgnoreList[#IgnoreList+1] = Value
		end
	end
	return IgnoreList
end

function Class:UpdateAmmoCounter()
	if self.UserInterface then
		self.UserInterface.GetElement("WeaponHud").UpdateAmmo(self)
	end
end

function Class:Restock(Amount)
	self.TotalAmmo = math.min(self.TotalAmmo + Amount, self.MaxAmmo)
	if IsServer then
		Replicate:FireAllClients(self.ID, "AmmoPickup", Amount)
	else
		self:UpdateAmmoCounter()
	end
end

function Class:Reload()
	if self.TotalAmmo == "inf" or self.TotalAmmo == 0 then return end
	print("Reloading")
	self.IsReloading = true
	
	local AmountToReload = math.min(self.MagSize, self.TotalAmmo)
	while self.TotalAmmo >= 1 and self.Ammo < AmountToReload and self.IsReloading do
		self:PlaySound("ReloadSound")
		local Track = self:PlayAnimation("Reload")
		Track.Stopped:Wait()
		if not self.IsReloading then
			return
		end
		if self.IsShotgun then
			self.Ammo = self.Ammo+1
			print(Reverb.RunMode, "TotalAmmo:", self.TotalAmmo)
		else
			self.Ammo = AmountToReload
		end
		self:UpdateAmmoCounter()
	end
	self.IsReloading = false
	print("Reloading finished")
end

function Class:HandleRecoil()
	if self.camShake then
		local Recoil = self.Recoil
		self.camShake:ShakeOnce(
			Recoil.Magnitude,
			Recoil.Roughness,
			Recoil.FadeIn,
			Recoil.FadeOut
		)
	end
end

function Class:EjectShell(HumanoidRootPart)
	if RunService:IsServer() or self.ToolModel == nil or self.ShellCasingType == nil then return end
	
	local Handle = self.Handle
--	ParticleManager.CreateShellCasing(
--		self.ShellCasingType, -- ShellCasingType
--		Handle.ShellEmitter.WorldCFrame, -- CF
--		Handle.CFrame.RightVector * 7, -- Velocity
--		Vector3.new(math.random() * 90, 0, 0) -- RotVelocity
--	)
	
	local Ids = {
		953031970,
		961361674,
		961369740,
	}
	
	local Sound = Instance.new("Sound")
	Sound.SoundId = "rbxassetid://"..Ids[math.random(1, #Ids)]
	Sound.Volume = 0.8
	Sound.PlaybackSpeed = 0.8
	Sound.PlayOnRemove = true
	Sound.EmitterSize = 30
	Sound.Parent = HumanoidRootPart
	Sound:Destroy()
end

function Class:ShotgunBlast(HumanoidRootPart, Direction)
	local Shots = {}	
	for Pellet_Index = 1, self.NumberOfPellets do
		local Spread = -(self.Spread.Max/2) + (Pellet_Index/self.NumberOfPellets) * self.Spread.Max
		Direction = (CFrame.new(HumanoidRootPart.Position, HumanoidRootPart.Position + Direction) * CFrame.Angles(0, math.rad(Spread), 0)).LookVector
		Shots[#Shots+1] = {HumanoidRootPart.Position, Direction}
	end
	return Shots	
end

function Class:SingleShot(HumanoidRootPart, Direction)
	local Shots = {}
	local N = math.random(-1, 1)
	if N == 0 then N = 1 end
	self.Spread.Current = math.min(self.Spread.Current + self.Spread.Step, 1)
	local Spread = math.random() * (self.Spread.Max * self.Spread.Current)
	Direction = (CFrame.new(HumanoidRootPart.Position, HumanoidRootPart.Position + Direction) * CFrame.Angles(0, math.rad(Spread * N), 0)).LookVector
	
	Shots[#Shots+1] = {HumanoidRootPart.Position, Direction}
	

	return Shots
end

function Class:Attack()
	if self.IsAttacking then return end
	if self and self.Owner.Model.Parent then
		if self.Ammo < 1 then
			return
		end
		if not self.IsShotgun and self.IsReloading then
			return
		else
			self.IsReloading = false
		end
		
		local HumanoidRootPart = self.Owner.Model:FindFirstChild("HumanoidRootPart")		
		local ToolModel = self.ToolModel
		local Handle = self.Handle
		self.IsAttacking = true
		self.LastAttack = tick()
		if self.IsBurst then
			local BurstAmount = self.NumberOfRoundsInBurst
			if self.Ammo < BurstAmount then
				BurstAmount = self.Ammo
			end
			for _ = 1, BurstAmount do
				self.LastAttack = tick()
				self:PlayAnimation("Shoot")
				
				local Shots = {}
				if self.IsShotgun then
					Shots = self:ShotgunBlast(HumanoidRootPart, self.InputDirection)
				else
					Shots = self:SingleShot(HumanoidRootPart, self.InputDirection)
				end
				
				ReplicateShot(self, Shots)
				
				self:PlaySound("FireSound", true, true)
				self.Ammo = math.max(self.Ammo-1, 0)
				self.TotalAmmo = math.max(self.TotalAmmo-1, 0)
				self:EjectShell(HumanoidRootPart)
				self:UpdateAmmoCounter()
				self:HandleRecoil()
			
				wait(1/self.FireRate)
			end
			if self.UsePumpAnimation then
				local Track = self:PlayAnimation("Pump")
				if self.Sounds["PumpSound"] then
					self:PlaySound("PumpSound")
				end
				--Track.Stopped:wait()
			end
			if self.BurstDelay > 0 then
				wait(self.BurstDelay)
			end
		elseif self.IsShotgun then
			self:PlayAnimation("Shoot")
			local Shots = self:ShotgunBlast(HumanoidRootPart, self.InputDirection)
			
			ReplicateShot(self, Shots)
			
			self:PlaySound("FireSound", true, true)		
			self.Ammo = math.max(self.Ammo-1, 0)
			self.TotalAmmo = math.max(self.TotalAmmo-1, 0)
			self:EjectShell(HumanoidRootPart)
			self:UpdateAmmoCounter()
			self:HandleRecoil()	
			if self.UsePumpAnimation then
				local Track = self:PlayAnimation("Pump")
				if self.Sounds["PumpSound"] then
					self:PlaySound("PumpSound")
				end
				--Track.Stopped:wait()
			end
			wait(1/self.FireRate)
		else
			local Track = self:PlayAnimation("Shoot")
			local Shots = self:SingleShot(HumanoidRootPart, self.InputDirection)
			
			ReplicateShot(self, Shots)

			self:PlaySound("FireSound", true, true)	
			self.Ammo = math.max(self.Ammo-1, 0)
			self.TotalAmmo = math.max(self.TotalAmmo-1, 0)
			self:EjectShell(HumanoidRootPart)
			self:UpdateAmmoCounter()
			self:HandleRecoil()
			wait(1/self.FireRate)
		end
		
		self.IsAttacking = false
		if self.IsAutomatic and self.Active then
			self:Attack()
		end
	end
end

function Class:Update(TimeElapsed)
	if self.Owner == nil then return end
--	local Character = self.Owner.Model
--	local HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart")
--	if HumanoidRootPart == nil then return end
--	if self.ToolModel then
--		if self.Laser then 
--			local _, HitPosition = Utilities.RaycastWithIgnoreList(HumanoidRootPart.Position, HumanoidRootPart.CFrame.LookVector * 999, self.LaserIgnoreList)
--			--self.Laser.Attachment0.CFrame = self.ToolModel.PrimaryPart.CFrame
--			self.Laser.Attachment1.WorldPosition = (HitPosition)
--		end
--	end
	
	if tick() - self.LastAttack >= 0.1 then
		self.Spread.Current = math.max(0, self.Spread.Current - (self.Spread.Recovery * TimeElapsed))
	end
end
--------------------------------------------
--
--------------------------------------------
local function OnReplicatedDo(ID, EventName, Extra)
	local Tool = Cache[ID]
	if Tool then
--		warn(EventName)
		if EventName == "ShotFired" then
			if Tool.TotalAmmo == "inf" then
				return
			end
			Tool.TotalAmmo = Tool.TotalAmmo - Extra
--			warn(Tool.Name .. "[" .. tostring(Tool.ID) .. "].TotalAmmo left: " .. tostring(Tool.TotalAmmo))			
		elseif EventName == "AmmoPickup" then
			Tool:Restock(Extra)
		end
	end	
end

if IsServer then
	Replicate.OnServerEvent:Connect(function(Player, ID, EventName, Extra)
		OnReplicatedDo(ID, EventName, Extra)
	end)
else
	Replicate.OnClientEvent:Connect(function(ID, EventName, Extra)
		OnReplicatedDo(ID, EventName, Extra)
	end)
end

return Class
2 Likes

Oh and that code both Server and Client can use, this is intentional so that both Player and AI can (and will) have the same base stats per weapon, This also helps ensure that all important events like Equip, Activated and reloading are properly replicated to all clients/the server.

Its a lot of work to setup, takes some planning and big brain juice moments, but in the end you get a nicely working system with some experience to take outside of Roblox with you for later in the future.

And as a side note, don’t do bullet replication inside of the Tool or RangedWeaponClass modules, do it inside of a manager that the tools interface with. That’ll make coding a lot more managable. Oh and local events are just BindableEvents, you’ll need them.

2 Likes