I recently ran into this issue where I needed to figure out the best way to implement my abstract Weapon class without creating too many dependencies (especially on the client side). Here’s what my current code looks like:
Server Controller
local class = script.Parent.Parent
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Assets = ReplicatedStorage.Assets
local Event = ReplicatedStorage.Event
local Utils = require(ReplicatedStorage.Module.Utils)
local idCounter = Utils.IdCounter.new()
local Controller = {}
Controller.__index = Controller
export type Weapon = typeof(setmetatable(
{} :: {
tool: Tool,
cooldown: number,
took: number,
owner: Player,
},
{} :: typeof(Controller)
))
function Controller.Activate(self: Weapon, ...)
print("Weapon has used")
end
function Controller.Cooldown(self: Weapon)
self.took = os.clock()
end
function Controller.CanActivate(self: Weapon): boolean
return os.clock() - self.took > self.cooldown
end
function Controller.IsOwner(self: Weapon,
player: Player): boolean
return self.owner.UserId == player.UserId
end
local function Init(weapon: Weapon)
local weaponId = idCounter:Add()
class.ClientInit:FireClient(
weapon.owner, weapon.tool,
weaponId, weapon.cooldown
)
class.Activation.OnServerEvent:Connect(function(plr, id)
if id ~= weaponId then return end
if not weapon:IsOwner(plr) then return end
if weapon:CanActivate() then weapon:Activate(); weapon:Cooldown() end
end)
local plrChar = weapon.owner.Character or weapon.owner.CharacterAdded:Wait()
local playerHumanoid: Humanoid = plrChar.Humanoid
playerHumanoid:EquipTool(weapon.tool)
end
Controller.new = function(name: string,
owner: Player, cooldown: number): Weapon
local tool: Tool = Assets.Weapons[name]
local self = {
tool = tool,
owner = owner,
cooldown = cooldown,
took = 0
}
Init(setmetatable(self, Controller))
return self
end
return Controller
Client Controller
local class = script.Parent.Parent
local Controller = {}
Controller.__index = Controller
export type Weapon = typeof(setmetatable(
{} :: {
tool: Tool,
cooldown: number,
id: number,
},
{} :: typeof(Controller)
))
function Controller.BuildActivationData(self: Weapon): any...
return 0
end
function Controller.Activate(self: Weapon): boolean
class.Activation:FireServer(self.id, self:BuildActivationData())
end
Controller.new = function(tool: Tool,
id: number, cooldown: number): Weapon
local self = {
tool = tool,
cooldown = cooldown,
id = id,
}
return setmetatable(self, Controller)
end
return Controller
Client Handler
local class = script.Parent.Parent
local Players = game:GetService("Players")
local LocalPlayer = Players.LocalPlayer
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Weapon = require(script.Parent.Controller)
local Handler = {}
Handler.Init = function()
class.ClientInit.OnClientEvent:Connect(
function(tool: Tool, id, cooldwon)
local weapon = Weapon.new(tool, id, cooldwon)
tool.Activated:Connect(function()
weapon:Activate()
end)
end)
Handler.Init = nil
end
return Handler
What I’m really aiming for is to be able to easily create child classes and override the BuildActivationData method without everything falling apart. You know how it goes - you start with what seems like a simple system, and before you know it, you’ve got dependency spaghetti!
I want to make it super straightforward to extend the base weapon functionality. Like, if I need to add a shotgun, or rocket launcher, I should just be able to create a new class that inherits from Weapon and only worry about the specific behavior that makes that weapon unique.
