Stats Handler - Fix overlapping WalkSpeed & Stack stat buffs, debuffs & modifiers easily

Stats Handler System

☆⸻●⸻☆


A flexible and efficient stat handler system for characters. Supports flat and additive or multiplicative stacking. Easily apply, remove, or clear modifiers by source or type. Perfect for applying and stacking buffs, debuffs and status effects. Has a lot of examples within the module
Basically allows stacking buffs/debuffs on players/NPCs with many active buffs at once. New buffs won't overlap/override old buffs. Removing a buff will not reset the stat and will maintain running buffs

📦 Roblox Model | ✨ Showcase game (Uncopylocked)

Introduction

Hi, it’s me, that one modeler again! Thunder

One day, I came across this post by Lightlimn which could fix overlapped walkspeed issue
It worked perfectly, but there weren’t many features and you could only modify walkspeed so I decided to improve the original work
I’m quite surprised that a lot of people stumble upon this same problem but all they choose is a short-term solution lol
This module is released to prepare for my upcoming Status Effects
I hope this module will help you!

How this system works:
  • When a stat modifier is applied to a character, it’s inserted into a table

  • Stats like MaxHealth, WalkSpeed or JumpPower will be applied to the corresponding Humanoid property

  • Otherwise, they are stored as Attributes on the character

  • The same stat can be stacked as many times as you want as long as the sources (ModifierName argument) are unique, or else it will just override with the new value. You can also choose to add, subtract, multiply or divide, just take a look at the examples, I showed the calculations as well

  • How it calculates the final stat: First it runs a loop through all the active modifiers in the table we just mentioned (Or you can call them buffs) to get the buff amount and add them up. Flat value and Multiplier value are separated. Then it’s calculated like below:

  • Flat = Flat1 + Flat2 + FlatX

  • Multiplier = Multiplier1 + Multiplier2 + MultiplierX (MultiplyMode setting = false)

  • Multiplier = Multiplier1 x Multiplier2 x MultiplierX (MultiplyMode setting = true)

  • Final number = (Base/Default + Flat value) x Multiplier

  • Confusing? Again, check examples there are detailed calculations

  • Character: The target that will receive the stat change (Model with Humanoid obviously)

  • Type: The stat name. For instance, WalkSpeed, JumpPower, MaxHealth, Defense, RegenerateRate, RegeneratePercent, DamageBoost, FireImmunity, StunImmunity, etc…

  • ModifierName: The source of the modifier like “Tool” or “Armor” or “Boost”, basically a unique string to help identify where it’s located from. If you put the same mod name it will override/reapply on the already existing stat mod of that same mod name (Hope this makes sense)

  • Amount: The value to change, can be a negative (-) number if you want to subtract for a debuff

  • Multiplier: The percentage (%) to multiply a stat by, can be negative to decrease a stat by % amount


How To Use


Source Code

Full script:
--[[
Original module by @Lightlimn (WalkSpeedHandler)
Improved by @ThunderDaNub (Multiple stats, various modes, memory leak fixed + cleanups, more functions, etc...)
]]

local Players = game:GetService("Players")

local MultiplyMode = false
-- false = Additive multipliers: Each multiplier is added together before being applied
--    Example:
--      Base = 10
--      +25% and +50% --> (1 + 0.25 + 0.5) = 1.75
--      Final = 10 * 1.75 = 17.5

-- true = Multiplicative multipliers: Each multiplier is applied one after another
--    Example:
--      Base = 10
--      +25% and +50% --> (1 + 0.25) * (1 + 0.5) = 1.25 * 1.5 = 1.875
--      Final = 10 * 1.875 = 18.75

-- The amount can be a negative number to decrease the final number as well

local Module = {}
local Modifiers = {}

-- This table defines which stats directly affect the Humanoid object
-- If a modified stat is listed here, its value will be applied to the corresponding Humanoid property (Ex: Humanoid.WalkSpeed = Stat)
-- For all other stats, values will be stored as attributes on the Character instead (Change the attribute location if you want)
local HumanoidProperties = {
	MaxHealth = true,
	WalkSpeed = true,
	JumpPower = true,
}

-- This is just an example, I put [""] because it looks cooler this way
-- For stats that are set as attributes on the characters you gotta implement your own code to use them
local DefaultStats = {
	["MaxHealth"] = 100,
	["WalkSpeed"] = 16,
	["JumpPower"] = 50,
	["RegenerateRate"] = 1,
	["RegeneratePercent"] = 1,
	
	["Defense"] = 0,
	["DamageBoost"] = 0,

	["FireImmunity"] = 0,
	["IQ"] = 100,
}

function Module.UpdateModifiers(Character: Model, Type: "StatName")
	local Humanoid = Character:FindFirstChildOfClass("Humanoid")
	local Stat = DefaultStats[Type]
	assert(Stat, "Stat named " .. Type ..  " is not included in DefaultStats table")

	if not Players:GetPlayerFromCharacter(Character) and Humanoid and HumanoidProperties[Type] then
		local Initial = Character:GetAttribute("Initial_" .. Type)
		if not Initial then
			Character:SetAttribute("Initial_" .. Type, Humanoid[Type])
		else
			Stat = Initial
		end
	end

	local TotalFlat = 0
	local TotalMultiplier = MultiplyMode and 1 or 0
	local Mods = Modifiers[Character] and Modifiers[Character][Type]

	if Mods then
		for _, Mod in pairs(Mods) do
			TotalFlat += Mod.Flat or 0
			
			if Mod.Multiplier then
				if MultiplyMode then
					TotalMultiplier *= (1 + Mod.Multiplier)
				else
					TotalMultiplier += Mod.Multiplier
				end
			end
		end
	end

	local FinalStat = (Stat + TotalFlat) * TotalMultiplier
	local FinalStat = (Stat + TotalFlat) * (MultiplyMode and TotalMultiplier or (1 + TotalMultiplier))
	
	if Humanoid and HumanoidProperties[Type] then
		Humanoid[Type] = FinalStat

		if Type == "MaxHealth" and Humanoid.Health > Humanoid.MaxHealth then
			Humanoid.Health = Humanoid.MaxHealth
		elseif Type == "JumpPower" and Humanoid.UseJumpPower == false then
			Humanoid.UseJumpPower = true
		end
	elseif Mods then
		Character:SetAttribute(Type, FinalStat)
	else
		Character:SetAttribute(Type, nil)
	end
end

--[[
	You can use this in two ways:
	local Handler = require(game.Path_To_This_Module)
	local Character = Your_Target
	
	1. Pass a single stat name and an amount:
	   Handler.AddModifier(Character, "WalkSpeed", "Wow", 16)

	2. Pass a table:
	   Handler.AddModifier(Character, {
	       ["WalkSpeed"] = {Flat = -10, Multiplier = -0.25}, -- (Optional) Specify if it's incremental or multiplicative
	       ["JumpPower"] = -25, -- Or simply put a number unlike above, for Flat value by default
	       ["RegeneratePercent"] = -1, -- This one will be set as an attribute because it isn't a Humanoid property (More info above)
	   }, "DebuffEffect")
	   
	This function below will automatically check for the type you send
--]]
function Module.AddModifier(Character: Model, Type: "StatName/StatTable", ModifierName: "Source", Amount: number?, Multiplier: number?)
	if typeof(Type) == "table" then
		for Stat, Value in pairs(Type) do
			if typeof(Value) == "table" then
				Module.AddModifier(Character, Stat, ModifierName, Value.Flat or 0, Value.Multiplier or 0)
			else
				Module.AddModifier(Character, Stat, ModifierName, Value, 0)
			end
		end
		return
	end

	if not Modifiers[Character] then
		Modifiers[Character] = {}
		local DestroyingConnection, AncestryChangedConnection

		AncestryChangedConnection = Character.AncestryChanged:Connect(function()
			if not Character:IsDescendantOf(game) then
				DestroyingConnection:Disconnect()
				AncestryChangedConnection:Disconnect()
				Modifiers[Character] = nil
			end
		end)

		DestroyingConnection = Character.Destroying:Once(function()
			AncestryChangedConnection:Disconnect()
			Modifiers[Character] = nil
		end)
	end

	if not Modifiers[Character][Type] then
		Modifiers[Character][Type] = {}
	end

	Modifiers[Character][Type][ModifierName] = {
		Flat = Amount or 0,
		Multiplier = Multiplier or 0,
	}

	Module.UpdateModifiers(Character, Type)
end

-- Use when you want to remove only one source of modification (Ex: A speed boost from a specific tool)
function Module.RemoveModifier(Character: Model, Type: "StatName", ModifierName: "Source")
	if Modifiers[Character] and Modifiers[Character][Type] then
		Modifiers[Character][Type][ModifierName] = nil
	end
	
	Module.UpdateModifiers(Character, Type)
end

-- Use for bulk removal, you can clear:
--   - All modifiers affecting a character by passing just the Character
--   - Type: All modifiers of a specific stat Type (Pass "WalkSpeed", 3 active WalkSpeed buffs --> None, keep other stats)
--   - ModifierName: A specific modifier source (Pass "Armor" to remove only mods from "Armor", keep the rest)
--   - In short, pass only Character, or Character & Type, or Character & ModifierName, or all 3 for different cases
function Module.ClearModifiers(Character: Model, Type: "StatName", ModifierName: "Source")
	if not Modifiers[Character] then return end
	
	if ModifierName and not Type then
		for Stat, Sources in pairs(Modifiers[Character]) do
			Sources[ModifierName] = nil
			if not next(Sources) then
				Modifiers[Character][Stat] = nil
			end
			Module.UpdateModifiers(Character, Stat)
		end
		
		if not next(Modifiers[Character]) then
			Modifiers[Character] = nil
		end
	elseif Type then
		if Modifiers[Character][Type] then
			if ModifierName then
				Modifiers[Character][Type][ModifierName] = nil
				
				if not next(Modifiers[Character][Type]) then
					Modifiers[Character][Type] = nil
				end
			else
				Modifiers[Character][Type] = nil
			end
			
			Module.UpdateModifiers(Character, Type)
		end
	else
		Modifiers[Character] = nil
		
		for stat in pairs(DefaultStats) do
			Module.UpdateModifiers(Character, stat)
		end
	end
end

return Module

Code examples from the model:
-- Put this in a Part or anything that can use Touched event and modify it to what you want
local StatsHandler = require(game:GetService("ServerScriptService").StatsHandler)

script.Parent.Touched:Connect(function(Hit)
	if Hit and Hit.Parent and Hit.Parent:IsA("Model") then
		local Character = Hit.Parent
		
		if Character:FindFirstChildOfClass("Humanoid") then
			StatsHandler.AddModifier(Character, "WalkSpeed", "Boost", 32)
			StatsHandler.AddModifier(Character, "JumpPower", "Boost", 100)
			-- This doesn't stack btw, to do so you need unique ModifierNames
			
			--[[
			Too many function calls? Use a table
			
			StatsHandler.AddModifier(Character, {
				WalkSpeed = 32,
				JumpPower = 100,
				Defense = 50,
				IQ = -199,
			}, "Boost")
			
			Want to remove?
			
			StatsHandler.RemoveModifier(Character, "WalkSpeed", "Boost")
			StatsHandler.RemoveModifier(Character, "JumpPower", "Boost")
			
			
			Or simply do this
			To remove all mods granted by this "Boost" modifier source
			
			StatsHandler.ClearModifiers(Character, nil, "Boost")
			
			
			Don't wanna do it like others?
			This will remove ALL active WalkSpeed and JumpPower boosts no matter the sources
			
			StatsHandler.RemoveModifier(Character, "WalkSpeed", nil)
			StatsHandler.RemoveModifier(Character, "JumpPower", nil)
			]]
		end
	end
end)
local StatsHandler = require(game:GetService("ServerScriptService").StatsHandler)

while task.wait(10) do
	for _, v in pairs(workspace:GetChildren()) do
		if v:IsA("Model") and v:FindFirstChildOfClass("Humanoid") then
			task.spawn(function()
				StatsHandler.AddModifier(v, {
					WalkSpeed = 32,
					JumpPower = 100,
					Defense = 50,
					IQ = -199, -- This is not a Humanoid property so it will be set as an attribute (Info in module)
				}, "Boost2")
				
				task.wait(2)
				StatsHandler.AddModifier(v, "WalkSpeed", "Boost2", 64) -- Change the current WalkSpeed boost
				
				task.wait(2)
				StatsHandler.AddModifier(v, "WalkSpeed", "Boost3", 20) -- Add 1 extra WalkSpeed boost
				StatsHandler.RemoveModifier(v, "JumpPower", "Boost2") -- Remove JumpPower boost
				
				task.wait(3)
				StatsHandler.ClearModifiers(v, nil, "Boost2") -- You can either use Remove like above
				StatsHandler.ClearModifiers(v, nil, "Boost3") -- or use Clear to remove in bulk
			end)
		end
	end
end
local StatsHandler = require(game:GetService("ServerScriptService").StatsHandler)
local Character = nil -- Choose a target to test

-- MultiplyMode is a setting in the main module (More info there)

StatsHandler.AddModifier(Character, "WalkSpeed", "Boost", 10)
-- Default = 16
-- Flat = +10
-- Multiplier = 1
-- Final = (16 + 10) * 1 = 26

-- MultiplyMode = false
StatsHandler.AddModifier(Character, "WalkSpeed", "Boost", 0, 0.25)
-- Default = 16
-- Flat = 0
-- Multiplier = 1 + 0.25 = 1.25
-- Final = 16 * 1.25 = 20

-- MultiplyMode = true
StatsHandler.AddModifier(Character, "WalkSpeed", "Boost", 0, 0.25)
StatsHandler.AddModifier(Character, "WalkSpeed", "Boost2", 0, 0.5)
-- Default = 16
-- Flat = 0
-- Multipliers = (1 + 0.25) * (1 + 0.5) = 1.25 * 1.5 = 1.875
-- Final = 16 * 1.875 = 30

-- MultiplyMode = false
StatsHandler.AddModifier(Character, "WalkSpeed", "Boost", 10)
StatsHandler.AddModifier(Character, "WalkSpeed", "Boost2", 0, 0.5)
-- Default = 16
-- Flat = +10 → 26
-- Multiplier = 1 + 0.5 = 1.5
-- Final = 26 * 1.5 = 39

-- MultiplyMode = true
StatsHandler.AddModifier(Character, "WalkSpeed", "Boost", 10)
StatsHandler.AddModifier(Character, "WalkSpeed", "Boost2", 0, 0.5)
-- Default = 16
-- Flat = +10 → 26
-- Multiplier = 1.5
-- Final = 26 * 1.5 = 39

StatsHandler.AddModifier(Character, {
	["WalkSpeed"] = {Flat = -10, Multiplier = -0.25}, -- (Optional) Specify if it's incremental or multiplicative
	["JumpPower"] = -25, -- Or simply put a number unlike above, for Flat value by default
	["RegeneratePercent"] = -1, -- This one will be set as an attribute because it isn't a Humanoid property (More info above)
}, "DebuffEffect")
local StatsHandler = require(game:GetService("ServerScriptService").StatsHandler)

local Tool = script.Parent

local Character

Tool.Equipped:Connect(function()
	Character = Tool.Parent
	local Humanoid:Humanoid = Character and Character:FindFirstChildOfClass("Humanoid")
	if Humanoid ~= nil and Humanoid.Health > 0 and Humanoid.RootPart ~= nil then
		StatsHandler.AddModifier(Character, "WalkSpeed", Tool.Name, Tool:GetAttribute("SpeedBoost"), Tool:GetAttribute("SpeedMulti"))
	end
end)

Tool.Unequipped:Connect(function()
	StatsHandler.RemoveModifier(Character, "WalkSpeed", Tool.Name)
end)

Features

  • Performance: Only runs once when a function is called
  • Automatically cleans up when characters are destroyed
  • Support stacking, all rig types, either player or NPC is fine
  • Active modifiers don’t overlap with each other. Adding or removing stat mods won’t affect the other mods, just the removed ones, final values are correctly calculated
  • Explanations and examples so detailed you won’t be reading allat

Update Logs


  • Cricket noises

Credits

@Lightlimn - Thanks to the original creator! Here’s the post


📦 Roblox Model | ✨ Showcase game (Uncopylocked)
9 Likes

Feel free to ask me if you have any questions or feedbacks

Also you can see the stat calculations for many cases in the 3rd example script

1 Like

Awesome work, this is much needed. Far better than my original one for walkspeed. Thanks for the credit :slight_smile:

1 Like

Thank you too!
Your original module helped me so much because I had no idea about what to do back when I faced this problem

1 Like

  • Added boost parts (Buff & Debuff) and a freezing part (Stops movement completely) to the test place
  • Updated the module to include “IQ” stat in the DefaultStats table. Using the examples resulted in an error because I forgot about this
  • Now shows an error message to explain in case you’re modifying a stat that isn’t defined in the DefaultStats table

  • Changing the JumpPower didn’t do anything because some Humanoids use JumpHeight for some reasons, so I added these lines to enable JumpPower by default
    In case you wonder, “JumpHeight is the height the character will jump in studs. This calculates the force used to jump to reach that height for you. JumpPower is the force that will be applied to the character when they jump, which is influenced by gravity.” - From this post