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: Project Blu Development - Roblox 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