Help with modular ability system


--CLIENT
UserInputService.InputBegan:Connect(function(input, processed)
	if not processed then
		for slot, ability in pairs(abilities) do
			local keybind = slotsToKeybinds[slot]
			if input.KeyCode == keybind then
				ability.Functions.Activate(ability)
				activateRE:FireServer(ability.Index)
			end
		end
	end
end)

--SERVER
activateRE.OnServerEvent:Connect(function(player, index)
	local abilities = AbilityService.GetPlayerAbilities(player)
	local ability = abilities[index]
	if ability then
		ability.Functions.Activate(ability)
	end
end)

local Sword = {}

Sword.Name = "Sword"

Sword.Functions = {
	Equip = function(ability)
		--print("Equipping Sword")
	end,
	Unequip = function(ability)
		--print("Unequipping Sword")
	end,
	Activate = function(ability)
		print("Swinging Sword")
	end
}

return Sword

im calling the same activate function on both server and client (at the same time) and i was wondering whats the smartest way to go about separating them, so i can handle my vfx, animation, etc… on the client and then handle damage and blah blah blah on the server but im stuck and cant think of the right way to go about this

Can you share code for AbilityService

1 Like

Well, I mean, try having two seperate module scripts? Also, exploiters can read and steal ModuleScripts if they’re not in a server-only objects (like ServerScriptStorage, ServerStorage) so you’ll probably want to do that anyways.

Also-also, you should fire the remote event before calling the ability on the client so it’s not delayed by any wait()s the function might have.

1 Like

You could instead make the Active function do the remote firing

--// Client
Activate = function(skillName)
    local clientVFX = require(ReplicatedStorage.Client.VFX[skillName])(localCharacter)
    useSkill:FireServer(skillName)
end


--// Server
useSkill.OnServerEvent:Connect(player, skillName)
   --// Validate everything \\
   local Skill = require(ServerStorage.Skills[skillName]
   Skill.ActivateBase(player)
end)

--// Hierarchy
ServerStorage
  • Skills (Folder)   
       • Punch (ModuleScript) --// Basic damage, animation playing perhaps, timing and etc

ReplicatedStorage
  • Client (Folder)
       • VFX (Folder)
            • Punch (ModuleScript) --// Handles VFX, make every client activate this on their side

It doesn’t matter if they can change module scripts, if you validate everything correctly they will only ruin their gameplay (such as deleting VFX completely or changing it)

1 Like

The concern is theft, not using the scripts to their advantage in-game.

Theft? that’s your concern?.. Don’t want to be rude but no one is going to steal anything, there are far better games and open features to the client and those games don’t care.

1 Like

It takes practically no effort to secure your game in that way, so there’s no reason not to do it.

Smaller games are also a little more at risk of theft since they have less of a reputation, allowing a stolen game to potentially surpass the original. This actually has happened before with Undead Nation, though I’ll admit that was way back when exploiters could also steal server scripts.

Ever heard o Miners Haven (or sum like that). The game is huge, but it’s literally open to studio.

Think of this situation like this. Let’s say you have a chess game, and you have a module script that calculates all legal moves based on Matrix (2D array). Now, the client can select a piece to preview all legal moves he can play, which requires for him to access that module script. But at the same time you don’t want to share your chess system. Would you rather have good experience playing or make everything server-sided?

Personally in the situation above, I wouldn’t care about the module being accessed or viewed by exploiters. I’d prefer a good experience while playing the game.

1 Like

Also I gave you an example where you can make main module which is ran on server only server-sided.
The only thing you are exposing is VFX module, theres no other way if you want VFX client-sided. And no one really cares how you do VFX.

No, not really. I imagine they opened the game to studio after obtaining a significant reputation, not as soon as the game was open to be played.

I’m not saying everything must be server-sided, but if the client is never going to use something anyways, why allow it? It only makes the game easier to steal, and it’s very easy to secure it.

1 Like

but if the client is never going to use something anyways, why allow it?

Then don’t? You are going against your word imo.

I gave a perfect example, even mentioned it again.

image

You can see the main skill module being server-sided. That’s where all the magic happens (damage, maybe some server-sided VFX? etc)

What was “my word”? Quote me. You’re strawmanning right now.

Nevermind you are not the OP, I thought you was the OP and it sounded like you were questioning about securing module scripts but then right away answering your own question

1 Like
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Maid = require(ReplicatedStorage.Maid)

local RemoteEvents = ReplicatedStorage:WaitForChild("RemoteEvents")

local replicateEquipRE = RemoteEvents:WaitForChild("ReplicateEquipRE")
local replicateUnequipRE = RemoteEvents:WaitForChild("ReplicateUnequipRE")
local activateRE = RemoteEvents:WaitForChild("ActivateRE")

local players = {}

-- Load all abilities from the Abilities folder
local AbilitiesFolder = ReplicatedStorage:WaitForChild("Abilities")
local AbilityService = {
	Abilities = {}
}

for _, moduleScript in pairs(AbilitiesFolder:GetChildren()) do
	if moduleScript:IsA("ModuleScript") then
		local ability = require(moduleScript)
		AbilityService.Abilities[ability.Name] = ability
	end
end

-- Get or create a player's abilities table
function AbilityService.GetPlayerAbilities(player)
	local abilities = players[player] or {}
	players[player] = abilities
	return abilities
end

-- Equip an ability for a player
function AbilityService.EquipAbility(player, character, abilityName, index)
	local abilities = AbilityService.GetPlayerAbilities(player)
	local abilityInfo = AbilityService.Abilities[abilityName]
	assert(abilityInfo, "Ability not found: " .. abilityName)
	assert(not abilities[index], "Slot is already taken")

	local ability = setmetatable({
		Player = player,
		Character = character,
		Index = index,
		Maid = Maid.new(),
		Active = true,
	}, { __index = abilityInfo })

	abilities[index] = ability
	abilityInfo.Functions.Equip(ability)
	replicateEquipRE:FireClient(player, abilityName, index)
end

-- Unequip an ability for a player
function AbilityService.UnequipAbility(player, index)
	local abilities = AbilityService.GetPlayerAbilities(player)
	local ability = abilities[index]
	if ability then
		ability.Functions.Unequip(ability)
		ability.Maid:Destroy()
		abilities[index] = nil
		replicateUnequipRE:FireClient(player, index)
	end
end

-- Clear all abilities for a player
function AbilityService.ClearAbilities(player)
	local abilities = AbilityService.GetPlayerAbilities(player)
	for index in pairs(abilities) do
		AbilityService.UnequipAbility(player, index)
	end
end

-- Listen for ability activation requests from the client
activateRE.OnServerEvent:Connect(function(player, index)
	local abilities = AbilityService.GetPlayerAbilities(player)
	local ability = abilities[index]
	if ability then
		ability.Functions.ServerActivate(ability)
	end
end)

-- Clean up abilities when a player leaves
Players.PlayerRemoving:Connect(function(player)
	AbilityService.ClearAbilities(player)
end)

return AbilityService

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local Maid = require(ReplicatedStorage.Maid)

local player = Players.LocalPlayer

local RemoteEvents = ReplicatedStorage:WaitForChild("RemoteEvents")

local replicateEquipRE = RemoteEvents:WaitForChild("ReplicateEquipRE")
local replicateUnequipRE = RemoteEvents:WaitForChild("ReplicateUnequipRE")
local activateRE = RemoteEvents:WaitForChild("ActivateRE")

local abilities = {}

local slotsToKeybinds = {
	[1] = Enum.KeyCode.One,
	[2] = Enum.KeyCode.Two,
	[3] = Enum.KeyCode.Three,
	[4] = Enum.KeyCode.Four,
	[5] = Enum.KeyCode.Five,
}

-- Load all abilities from the Abilities folder
local AbilitiesFolder = ReplicatedStorage:WaitForChild("Abilities")
local AbilityController = {
	Abilities = {}
}

for _, moduleScript in pairs(AbilitiesFolder:GetChildren()) do
	if moduleScript:IsA("ModuleScript") then
		local ability = require(moduleScript)
		AbilityController.Abilities[ability.Name] = ability
	end
end

-- Equip an ability
function AbilityController.EquipAbility(abilityName, index)
	local abilityInfo = AbilityController.Abilities[abilityName]
	assert(abilityInfo, "Ability not found: " .. abilityName)

	local ability = setmetatable({
		Player = player,
		Character = player.Character,
		Index = index,
		Maid = Maid.new(),
		Active = true,
	}, { __index = abilityInfo })

	abilities[index] = ability
	abilityInfo.Functions.Equip(ability)
end

-- Unequip an ability
function AbilityController.UnequipAbility(index)
	local ability = abilities[index]
	if ability then
		ability.Functions.Unequip(ability)
		ability.Maid:Destroy()
		abilities[index] = nil
	end
end

-- Listen for server events to equip/unequip abilities
replicateEquipRE.OnClientEvent:Connect(AbilityController.EquipAbility)
replicateUnequipRE.OnClientEvent:Connect(AbilityController.UnequipAbility)

-- Listen for player input to activate abilities
UserInputService.InputBegan:Connect(function(input, processed)
	if not processed then
		for slot, ability in pairs(abilities) do
			local keybind = slotsToKeybinds[slot]
			if input.KeyCode == keybind then
				ability.Functions.ClientActivate(ability)
				activateRE:FireServer(ability.Index)
			end
		end
	end
end)

return AbilityController

here is AbilityController and AbilityService

if you mean to make the server activate function different from the client activate function then you can

1-seperate them into 2 module scripts 1 for client and 1 for server

2-use RunService:IsServer() to seperate the logic of 1 function (less recommended than 1 since exploiters will be able to see the server sided code of the function Activate)