[11 Effects!] Thunder's Status Effect System | Apply unique effects to deal damage over time, restrict movement, alter behavior and more!

Thunder's Status Effect System

☆⸻●⸻☆


Apply status effects such as 🔥 Fire, 🧊 Ice, ⚡ Stun, ❓ Confusion, etc... to targets and have them affected by various conditions. These effects can deal damage over time, restrict movement, alter behavior, reduce defense or apply other unique mechanics depending on the effect type.

Introduction

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

Last time I made this very cool FreezeModule

, I also revamped Roblox’s TagHumanoid function and made this Stats Handler system

So this time I combined the both of them in order to make a system that lets you apply certain elemental status effects on characters (Players/NPCs). I hope this might help you learn something new even if you don’t use it!

Besides, this system allows you to easily set Defense and Immunities for characters using Attributes. You can add an effect from a script whose parent might be destroyed later, doing so does not stop the ongoing effect.

I’ll provide updates once in a while to fix bugs or improve the system (Any help would be appreciated!), especially during special occasions like Halloween or Christmas, you can expect to see even more premade status effects themed around those events, which are free for use (Basically similar to game events)

How this system works:
  1. When an effect is applied to a target, the said target is tagged using CollectionService and some additional Attributes are set on a Configuration object, which is parented in the target.
  2. In the main script, when there's a new target added to to the CollectionService, it checks for the Humanoid object, HumanoidRootPart, type of status effect, damage, tick, variant as well as the defense amount and immunities of the target before applying the effect.
  3. The status effect is applied with visual effects using a repeat until loop until the duration runs out, at which point the target is untagged. Duration is check every frame using a loop, this allows us to change the duration while an effect is active, unlike my previous method using Debris Service and ChildAdded() event
  4. If a target has Defense attribute, it will receive less damage from an effect, same goes for its Immunities, but an immunity can also shorten the effective duration. Ex: You apply 🔥 Fire with 100 damage per tick for 5 seconds to a Noob who has Defense = 50(%), FireImmunity = 50, he will only take 25 damage every tick for 2.5 seconds.

Free Status Effect Examples (10 Presets/Templates)

Free status effects: I tried making each effect as unique as possible, take a look here to see what they do. These examples are placed within the folder or one of the main scripts.
  • :high_voltage: Stun: Stuns and stops the target from moving with a strong force. For heavy targets or those with Stun Immunity, the target is not stunned but rather slowed down along with their animation speed. Can set tick, can set stunning type (Zap/Shock/Paralyze) or color with variant. Effect emits once every tick. Tick, Force and AnimationSpeed factor are affected by Stun Immunity

  • :fire: Fire: Burns to deal DoT and melt defense

  • :test_tube: Acid: Corrodes to deal DoT and melt defense

  • :radioactive: Radiation: Deals DoT, also melts and ignores defense

  • :skull: Poison: Deals DoT while bypassing defense, damage (%) depends on the MaxHealth of the target

  • :drop_of_blood: Bleed: Deals DoT, damage grows after each tick until the limit is reached, also reduces the WalkSpeed and JumpPower of the target based on the % of Health remaining

  • :revolving_hearts: Heal: Heals target

  • :ice: Ice: Freezes the target to reduces defense, stops any movements or animations while retaining physics, creates a thin ice layer on the character. Utilizes my FreezeModule. If the target is 100% immune to this then :snowflake: Chill will be applied instead

  • :snowflake: Chill: Slows down chilled targets, freeze amount depends on FreezeAmount setting and the IceImmunity of the target

  • :red_question_mark: Confusion: If the target is a Player, their control is inverted (all devices), otherwise, the movement of the target is reversed

  • :milky_way: Void: Halves the damage that the target can deal when applied

  • :man_vampire:Lifesteal: Takes HP from the target to heal the attacker. Lifesteal amount is affected by Defense, Void immunity and is multiplied by LifestealBoost stat. Must provide Creator in the settings

  • :link: Love-Link: (Will be added one day): Electrocuts targets near the main target who is being affected by this effect, lightning strikes will link the affected targets together

  • :collision: Explosion: (Will be added on New Year Eve): Instantly kills the target and then creates an explosion afterwards

  • :nazar_amulet: Teleportation: (Will be added on ??? event): Randomly teleports the target to a random location at random time

Explanation:
  1. DoT: Damage over time. Damage is applied every Tick

  2. Defense: How many % of damage that a character can block. The effective damage dealt towards a character is decreased if its defense amount is higher than 0. However, Poison & Radiation can both bypass/ignore defense

  3. Immunity:: How many % of damage that a character can block from the same type of effect they are immune to, can stack with Defense (Full Damage → Reduced by Defense → Reduced by Immunity)

  4. Defense-melting: Reduce defense, each type of elemental status effect can only drop up to a certain amount of defense and does not stack with the same type of status effect

  5. Tick: The rate at which the effect deals damage or activates. Basically firerate

  6. Duration: How long the effect will be applied for. This timespan can be shorten by an immunity (0%-100%). If the immunity is higher than 100%, then the duration is not changed. Else if the immunity is lower than 0%, then the duration is increased. Duration is also stackable if an effect allows it

  7. Creator: The Player instance or the character of a player (Module auto checks), is used to tag targets and recognize total damage contributed and the final hit dealer. Does not tag if the Creator is not a player

  8. Variant: Customize an effect or give it a different mechanic. This is because you can’t send info to the CollectionService

Examples:
  1. You apply :fire: Fire with 100 damage per tick for 5 seconds to a Noob who has Defense = 50(%), FireImmunity = 50, he will only take 25 damage every tick for 2.5 seconds.
  2. You apply :ice: Ice for 2 seconds to a Snowman that has IceImmunity = 100, it will not be frozen at all.
  3. You apply :skull: Poison with 1% MaxHealth as damage per tick for 10 seconds to a Zombie who has Defense = 90, PoisonImmunity = 150, it will not take any damage, but heal up by 1.5% of its MaxHealth per tick for 10 seconds. Poison ignores Defense
  4. You apply :test_tube: Acid with 100 damage per tick for 5 seconds to a Robot that has Defense = 50, AcidImmunity = -50, it will take 75 damage every tick for 7.5 seconds.
  5. You apply :biohazard: Radiation with 100 damage per tick for 5 seconds to a Golem that has Defense = 100, no RadiationImmunity, it will take 100 damage every tick for 5 seconds. Radiation ignores Defense
  6. You apply :red_question_mark: Confusion for 5 seconds to a Player without ConfusionImmunity, their control will be inverted during that time (WASD → SDWA on PC, opposite movement direction on all devices)
  7. You apply :sob: Failure with infinite emotional damage per tick to a Player forever, they will not be happy and will most likely dislike your game
Code examples from the model:
local FunctionBank = require(script.Parent.Parent.FunctionBank)
local Effects = FunctionBank.Effects

local Effects = {}
for Effect in pairs(FunctionBank.Effects) do
	table.insert(Effects, Effect)
end

for i, v in pairs(workspace:GetChildren()) do
	if v:IsA("Model") and v:FindFirstChild("HumanoidRootPart") then
		local RandomEffect = Effects[math.random(1, #Effects)]
		
		FunctionBank.AddEffect(v, RandomEffect, 10, {
			Damage = 5, -- Some settings are not needed. But you must give it a valid Duration (Above ⬆)
			Creator = script.Parent,
		})
	end
end

--[[

-- There are 2 ways to add an effect as shown below

-- #1

-- Arguments in this order:
-- Target, Effect, Duration, {Settings: {Damage: number?, Creator: Instance | string, Tick: number?, Variant: any?, Stackable: boolean?, MaxStack: number?, StackBehavior: string?}}
-- If settings like Damage or Tick are not included, then the default values in the Effects script will be used instead

FunctionBank.AddEffect(Target, "Fire", 5, {
	Damage = 5,
	Tick = 1,
	Creator = Creator,
	Variant = Color3.fromRGB(85, 0, 255),
	Stackable = true,
	MaxStack = 5,
	StackBehavior = "Overlap"
})

-- #2

local EffectEvent = script.Parent.Parent.Handler:FindFirstChild("EffectEvent")

EffectEvent:Fire(Target, "Ice", 5, {
	Creator = Creator,
})

]]
-- An example from the Status Effect system
local FunctionBank = require(game:GetService("ServerScriptService")["Thunder's Status Effect System"]:FindFirstChild("FunctionBank"))

local Effects = {}
for Effect in pairs(FunctionBank.Effects) do
	table.insert(Effects, Effect)
end

local Debounce = {}

script.Parent.Touched:Connect(function(Hit)
	if Hit and Hit.Parent and Hit.Parent:IsA("Model") then
		local Target = Hit.Parent
		if Debounce[Target] then return end
		
		local RandomEffect = Effects[math.random(1, #Effects)]
		local Duration =  math.random(2, 8)
		
		if Target:FindFirstChildOfClass("Humanoid") then
			Debounce[Target] = true
			
			FunctionBank.AddEffect(Target, RandomEffect, Duration, {
				Damage = 5,
				Creator = script.Parent,
			})
			
			task.delay(Duration, function()
				Debounce[Target] = nil
			end)
		end
	end
end)

Source code

Script 1 (All available status effects):
--!strict
local CS = game:GetService("CollectionService")
local Players = game:GetService("Players")

local EffectFolder = script:FindFirstChild("EffectFolder")
local Utility = script:FindFirstChild("Utility")

local FunctionBank = require(script.Parent.FunctionBank)

local Setup = require(Utility.Setup)
local StatsHandler = require(Utility.StatsHandler)
local FreezeModule = require(Utility.FreezeModuleModified)

local Effects = FunctionBank.Effects

local function CheckForEffectConfig(Target: Instance, Effect: string): Configuration
	local Config
	
	for i, v in ipairs(Target:GetChildren()) do
		if v.Name == Effect and v:IsA("Configuration") then
			Config = v
			break
		end
	end
	
	return Config
end

local function GetAttributeOrValue(Config: Instance, Key: string): any?
	local Value = Config:GetAttribute(Key)
	if Value ~= nil then
		return Value
	end
	
	local ObjectValue = Config:FindFirstChild(Key)
	if ObjectValue and ObjectValue:IsA("ObjectValue") then
		return ObjectValue.Value
	end

	return nil
end

local function Emit(VisualEffect: any?)
	if VisualEffect then
		if VisualEffect:IsA("Attachment") then
			for i, v in ipairs(VisualEffect:GetChildren()) do
				if v:IsA("ParticleEmitter") then
					v:Emit(v:GetAttribute("EmitCount") or 1)
				end
			end
		elseif VisualEffect:IsA("ParticleEmitter") then
			VisualEffect:Emit(VisualEffect:GetAttribute("EmitCount") or 1)
		end
	end
end

local function PlaySound(Sound: Sound?)
	if Sound then
		Sound.TimePosition = 0
		Sound:Play()
	end
end

Setup.Setup({
	Effect = "Stun",
	VisualTemplate = EffectFolder:FindFirstChild("Electricity"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect, Sound)
		local Effect = "Stun"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart

		local Damage = EffectConfig:GetAttribute("Damage") or 0
		local Tick = EffectConfig:GetAttribute("Tick") or 0.1
		local Creator = GetAttributeOrValue(EffectConfig, "Creator")

		local Force = Vector3.one * (1e7 - 1e7 * (Immunity / 100))
		local MinAnimationSpeed = 0.2 + (1 - 0.2) * (Immunity / 100)
		Tick = math.max(Tick + Tick * (Immunity / 100), 0.05)

		local BV: BodyVelocity
		if not Root:FindFirstChild("Stunned") then
			BV = Instance.new("BodyVelocity")
			BV.Name = "Stunned"
			BV.MaxForce = Force
			BV.Velocity = Root.Position.Unit * 0 + Vector3.new(0, -0.1, 0)
			BV.Parent = Root
		end

		repeat
			if Character and Character.Parent and Humanoid and Humanoid.Health > 0 then
				--FunctionBank:Damage(Creator, Character, Damage, false, false, Effect, nil, nil)
				Emit(VisualEffect)
				PlaySound(Sound)

				if BV then
					BV.Velocity = Root.Position.Unit * 0 + Vector3.new(0, -0.1, 0)
				end

				if Immunity < 100 then
					FunctionBank:MeltDefense(Character, Effect, 1)
				end

				local Animator = Humanoid:FindFirstChildOfClass("Animator")
				if Humanoid and Animator then
					for i, v in ipairs(Animator:GetPlayingAnimationTracks()) do
						v:AdjustSpeed(MinAnimationSpeed)
					end
				end

				task.wait(Tick)
			else
				break
			end
		until not CS:HasTag(Character, Effect) or not EffectConfig.Parent

		local Animator = Humanoid:FindFirstChildOfClass("Animator")
		if Humanoid and Animator then
			for i, v in ipairs(Animator:GetPlayingAnimationTracks()) do
				v:AdjustSpeed(1)
			end
		end

		Humanoid:ChangeState(Enum.HumanoidStateType.Freefall)

		if BV then
			BV:Destroy()
		end
	end
})

Setup.Setup({
	Effect = "Fire",
	VisualTemplate = EffectFolder:FindFirstChild("Fire"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect)
		local Effect = "Fire"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart

		local Damage = EffectConfig:GetAttribute("Damage") or 5
		local Tick = EffectConfig:GetAttribute("Tick") or 1
		local Creator = GetAttributeOrValue(EffectConfig, "Creator")

		repeat
			if Character and Character.Parent and Humanoid and Humanoid.Health > 0 then
				FunctionBank:Damage(Creator, Character, Damage, Effects[Effect].IgnoreDefense, Effects[Effect].IgnoreImmunity, Effect, nil, "Burn")
				FunctionBank:MeltDefense(Character, Effect, 2)
				task.wait(Tick)
			else
				break
			end
		until not CS:HasTag(Character, Effect) or not EffectConfig.Parent
	end
})

Setup.Setup({
	Effect = "Acid",
	VisualTemplate = EffectFolder:FindFirstChild("Acid"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect)
		local Effect = "Acid"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart

		local Damage = EffectConfig:GetAttribute("Damage") or 5
		local Tick = EffectConfig:GetAttribute("Tick") or 1
		local Creator = GetAttributeOrValue(EffectConfig, "Creator")

		repeat
			if Character and Character.Parent and Humanoid and Humanoid.Health > 0 then
				FunctionBank:Damage(Creator, Character, Damage, Effects[Effect].IgnoreDefense, Effects[Effect].IgnoreImmunity, Effect, nil, "Fade")
				FunctionBank:MeltDefense(Character, Effect, 2)
				task.wait(Tick)
			else
				break
			end
		until not CS:HasTag(Character, Effect) or not EffectConfig.Parent
	end
})

Setup.Setup({
	Effect = "Radiation",
	VisualTemplate = EffectFolder:FindFirstChild("Radiation"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect)
		local Effect = "Radiation"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart

		local Damage = EffectConfig:GetAttribute("Damage") or 5
		local Tick = EffectConfig:GetAttribute("Tick") or 1
		local Creator = GetAttributeOrValue(EffectConfig, "Creator")

		repeat
			if Character and Character.Parent and Humanoid and Humanoid.Health > 0 then
				FunctionBank:Damage(Creator, Character, Damage, Effects[Effect].IgnoreDefense, Effects[Effect].IgnoreImmunity, Effect, nil, "JointBreak")
				FunctionBank:MeltDefense(Character, Effect, 2)
				task.wait(Tick)
			else
				break
			end
		until not CS:HasTag(Character, Effect) or not EffectConfig.Parent
	end
})

Setup.Setup({
	Effect = "Poison",
	VisualTemplate = EffectFolder:FindFirstChild("Poison"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect)
		local Effect = "Poison"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart

		local Damage = EffectConfig:GetAttribute("Damage")
		Damage = math.round((Damage and Humanoid.MaxHealth * (Damage / 100)) or (Humanoid.MaxHealth * (2.5 / 100)))
		local Tick = EffectConfig:GetAttribute("Tick") or 0.5
		local Creator = GetAttributeOrValue(EffectConfig, "Creator")
		
		repeat
			if Character and Character.Parent and Humanoid and Humanoid.Health > 0 then
				FunctionBank:Damage(Creator, Character, Damage, Effects[Effect].IgnoreDefense, Effects[Effect].IgnoreImmunity, Effect, nil, nil)
				task.wait(Tick)
			else
				break
			end
		until not CS:HasTag(Character, Effect) or not EffectConfig.Parent
	end
})

Setup.Setup({
	Effect = "Bleed",
	VisualTemplate = EffectFolder:FindFirstChild("Bleed"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect, Sound)
		local Effect = "Bleed"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart

		local Damage = EffectConfig:GetAttribute("Damage") or 5
		local Tick = EffectConfig:GetAttribute("Tick") or 1
		local Creator = GetAttributeOrValue(EffectConfig, "Creator")

		local TickCount = 0
		local MaxDebuff = EffectConfig:GetAttribute("MaxDebuff") or 0.5
		local DamageGrowth = EffectConfig:GetAttribute("DamageGrowth") or 0.5
		local MaxDamage = EffectConfig:GetAttribute("MaxDamage") or Damage * 10
		
		repeat
			if Character and Character.Parent and Humanoid and Humanoid.Health > 0 then
				FunctionBank:Damage(Creator, Character, math.min(Damage + (TickCount^1.5) * DamageGrowth, MaxDamage), Effects[Effect].IgnoreDefense, Effects[Effect].IgnoreImmunity, Effect, nil, nil)
				TickCount += 1
				
				local DebuffMultiplier = -(1 - Humanoid.Health / Humanoid.MaxHealth) ^ 2 * MaxDebuff
				
				StatsHandler.AddModifier(Character, {
					WalkSpeed = {Multiplier = DebuffMultiplier},
					JumpPower = {Multiplier = DebuffMultiplier},
					RegenerateRate = 1,
					RegeneratePercent = -1,
				}, "BleedEffect")
				
				Emit(VisualEffect)
				PlaySound(Sound)
				
				task.wait(Tick)
			else
				break
			end
		until not CS:HasTag(Character, Effect) or not EffectConfig.Parent

		StatsHandler.ClearModifiers(Character, nil, "BleedEffect")
	end
})

Setup.Setup({
	Effect = "Heal",
	VisualTemplate = EffectFolder:FindFirstChild("Heal"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect)
		local Effect = "Heal"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart

		local Damage = EffectConfig:GetAttribute("Damage") or 5
		local Tick = EffectConfig:GetAttribute("Tick") or 0.5
		local Creator = GetAttributeOrValue(EffectConfig, "Creator")

		repeat
			if Character and Character.Parent and Humanoid and Humanoid.Health > 0 then
				if Humanoid.Health < Humanoid.MaxHealth then
					Humanoid.Health += Damage
				elseif Humanoid.Health > Humanoid.MaxHealth then
					Humanoid.Health = Humanoid.MaxHealth
				end
				task.wait(Tick)
			else
				break
			end
		until not CS:HasTag(Character, Effect) or not EffectConfig.Parent
	end
})

Setup.Setup({
	Effect = "Ice",
	VisualTemplate = EffectFolder:FindFirstChild("Chill"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect)
		local Effect = "Ice"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart

		if Immunity >= 100 then
			if Humanoid then
				local FreezeAmount = EffectConfig and EffectConfig:GetAttribute("Damage") or 100
				local ResistanceFactor = 1 / (1 + (Immunity / 100))
				local DebuffAmount = (FreezeAmount / 100) * ResistanceFactor
				local DebuffMultiplier = -math.clamp((FreezeAmount / 100) * ResistanceFactor, 0, 1)

				local Animator = Humanoid:FindFirstChildOfClass("Animator")
				if Humanoid and Animator then
					local DebuffPercent = math.clamp(Humanoid.WalkSpeed * DebuffAmount / Humanoid.WalkSpeed, 0, 1)
					local AnimationSpeed = 1 - (DebuffPercent)

					for i, v in ipairs(Animator:GetPlayingAnimationTracks()) do
						v:AdjustSpeed(AnimationSpeed)
					end
				end

				StatsHandler.AddModifier(Character, {
					WalkSpeed = {Multiplier = DebuffMultiplier},
					JumpPower = {Multiplier = DebuffMultiplier},
				}, "ChillEffect")
			end
			return
		end

		if Character and Character:FindFirstChildOfClass("Humanoid") then
			FreezeModule.Freeze(Character)
			Character:SetAttribute("Frozen", true)
			FunctionBank:MeltDefense(Character, Effect, 5)
		end
	end,
	OnRemoveFunction = function(Character)
		local Effect = "Ice" :: string
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart

		if Character and Humanoid then
			if Immunity >= 100 then
				StatsHandler.ClearModifiers(Character, nil, "ChillEffect")

				local Animator = Humanoid:FindFirstChildOfClass("Animator")
				if Humanoid and Animator then
					for i, v in ipairs(Animator:GetPlayingAnimationTracks()) do
						v:AdjustSpeed(1)
					end
				end
			end
			for i, v in ipairs(Character:GetDescendants()) do
				if v:GetAttribute("EffectName") and v:GetAttribute("EffectName") == Effect then
					v:Destroy()
				end
			end

			FreezeModule.Unfreeze(Character)
			Character:SetAttribute("Frozen", nil)
		end
	end,
})

Setup.Setup({
	Effect = "Void",
	VisualTemplate = EffectFolder:FindFirstChild("Void"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect)
		local Effect = "Void"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		if Immunity >= 100 then
			return
		end
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart
		
		local Tick = EffectConfig:GetAttribute("Tick") or 1

		repeat
			if Character and Character.Parent and Humanoid and Humanoid.Health > 0 then
				if not Character:GetAttribute("Voided") then
					Character:SetAttribute("Voided", true)
				end
				FunctionBank:MeltDefense(Character, Effect, 5)
				task.wait(Tick)
			else
				break
			end
		until not CS:HasTag(Character, Effect) or not EffectConfig.Parent
		
		if Character and Character:GetAttribute("Voided") then
			Character:SetAttribute("Voided", nil)
		end
	end
})

Setup.Setup({
	Effect = "Confusion",
	VisualTemplate = EffectFolder:FindFirstChild("Confusion"),
	OnTickFunction = function(Character, EffectConfig, VisualEffect)
		local Effect = "Confusion"
		local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0 :: number
		if Immunity >= 100 then
			return
		end
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart
		
		local Tick = EffectConfig:GetAttribute("Tick") or 0.01
		local Creator = GetAttributeOrValue(EffectConfig, "Creator")

		local InvertControls = nil
		local BV = nil

		if Character and Humanoid then
			local Player = Players:GetPlayerFromCharacter(Character)
			if Player and not Character:FindFirstChild("InvertControls") then
				InvertControls = Utility.InvertControls:Clone()
				InvertControls.Parent = Character
				InvertControls.Enabled = true

				local Reset = Utility.Reset:Clone()
				Reset.Parent = InvertControls
				Reset.Enabled = false
			end

			repeat
				if not Players:GetPlayerFromCharacter(Character) and Root and Humanoid and Humanoid.Health > 0 then
					if not Root:FindFirstChild("Reverse") then
						BV = Instance.new("BodyVelocity")
						BV.Name = "Reverse"
						BV.MaxForce = Vector3.one * 1e7
						BV.Velocity = Root.AssemblyLinearVelocity * -1
						BV.Parent = Root
					end
					BV.Velocity = Root.AssemblyLinearVelocity * -1
				end
				
				task.wait(Tick)
			until not CS:HasTag(Character, Effect)
		end

		if BV then
			BV:Destroy()
		end
		if VisualEffect then
			VisualEffect:Destroy()
		end
		if InvertControls then
			InvertControls:Destroy()

			local Reset = Utility.Reset:Clone()
			Reset.Parent = Character
			Reset.Enabled = true
		end
	end
})
Script 2 (Handler):
--!strict
local CS = game:GetService("CollectionService")
local Running = game:GetService("RunService")

local EffectEvent = script:FindFirstChild("EffectEvent")

local FunctionBank = require(script.Parent.FunctionBank)
local Effects = FunctionBank.Effects

type ActiveEffect = {
	Target: Instance,
	Effect: string,
	EffectConfig: Configuration,
	StartTime: number,
	Duration: number,
}

local ActiveEffects: {ActiveEffect} = {}

local Signal: RBXScriptConnection?
local function StartEffectLoop()
	if Signal then return end

	Signal = Running.Heartbeat:Connect(function()
		local CurrentTime = os.clock()

		for i = #ActiveEffects, 1, -1 do
			local v = ActiveEffects[i]

			if v then
				local EffectConfig = v.EffectConfig
				local Target = v.Target
				local Effect = v.Effect

				if (CurrentTime - v.StartTime) >= v.Duration or (EffectConfig and (not EffectConfig.Parent or not EffectConfig.Parent.Parent)) then
					local stillHasStacks = false
					for j = 1, #ActiveEffects do
						local other = ActiveEffects[j]
						if other and other ~= v and other.Target == Target and other.Effect == Effect then
							stillHasStacks = true
							break
						end
					end
					
					if not stillHasStacks and CS:HasTag(Target, Effect) then
						CS:RemoveTag(Target, Effect)
					end
					if EffectConfig then
						EffectConfig:Destroy()
					end
					table.remove(ActiveEffects, i)
				end
			end
		end

		-- Stop the loop if no effects remain
		if #ActiveEffects == 0 then
			if Signal then
				Signal:Disconnect()
				Signal = nil
			end
		end
	end)
end

local function AddNewEffect(Arg)
	table.insert(ActiveEffects, Arg)
	StartEffectLoop()
end

local function CountActiveStacks(Target: Instance, Effect: string): number
	local Count = 0
	
	for i, v in ipairs(ActiveEffects) do
		if v.Target == Target and v.Effect == Effect then
			Count += 1
		end
	end

	return Count
end

local function GetNumber(Value: any): number?
	return typeof(Value) == "number" and Value or tonumber(Value)
end

local function HandleImmunity(Target: Instance, Effect: string, Duration: number?): number?
	if not Duration then
		return nil
	end

	local Immunity = Target:GetAttribute(Effect .. "Immunity")
	if not Immunity then
		return Duration
	end

	if Effect == "Ice" then
		local MinFactor = 0.1
		local ResistanceFactor = 1 / (1 + (Immunity / 100))
		return Duration * math.max(ResistanceFactor, MinFactor)
	else
		if Immunity <= 100 then
			return Duration - Duration * (Immunity / 100)
		else
			return 0
		end
	end
end

local function CreateEffectConfig(Effect: string, Settings, Damage: number?, Creator: Instance | string, Tick: number?, Variant: any?)
	local EffectConfig = script.EffectTemplate:Clone()
	EffectConfig.Name = Effect
	if Damage then
		EffectConfig:SetAttribute("Damage", Damage)
	end
	if Tick then
		EffectConfig:SetAttribute("Tick", Tick)
	end
	if Creator then
		if typeof(Creator) == "string" then
			EffectConfig:SetAttribute("Creator", Creator)
		else
			local Tag = Instance.new("ObjectValue")
			Tag.Name = "Creator"
			Tag.Value = Creator
			Tag.Parent = EffectConfig
		end
	end
	if Variant then
		EffectConfig:SetAttribute("Variant", Variant)
	end
	
	if Effects[Effect].CustomSettings then
		for i, Setting in ipairs(Effects[Effect].CustomSettings) do
			local Value = Settings and Settings[Setting]
			if Value then
				EffectConfig:SetAttribute(Setting, Value)
			end
		end
	end

	return EffectConfig
end

local function ClearExistingConfigs(EffectConfigs: {Configuration})
	for i, v in ipairs(EffectConfigs) do
		v:Destroy()
	end
	table.clear(EffectConfigs)
end

EffectEvent.Event:Connect(function(Target: Instance, Effect: string, Duration: number?, Settings: {Damage: number?, Creator: Instance | string, Tick: number?, Variant: any?, Stackable: boolean?, MaxStack: number?, StackBehavior: string?})
	if Target and Effect and Duration and typeof(Settings) == "table" then
		if not Effects[Effect] then return end
		
		Duration = GetNumber(Duration)
		Duration = HandleImmunity(Target, Effect, Duration)
		if not Duration or Duration <= 0 then return end
		
		local Damage = GetNumber(Settings.Damage)
		local Tick = GetNumber(Settings.Tick)
		local MaxStack = GetNumber(Settings.MaxStack)
		local Stackable = Settings.Stackable
		local StackBehavior = Settings.StackBehavior
		local Creator = Settings.Creator
		local Variant = Settings.Variant
		
		local StackCount = CountActiveStacks(Target, Effect)
		
		local ExistingConfigs = {}
		for i, v in ipairs(Target:GetChildren()) do
			if v:IsA("Configuration") and v.Name == Effect then
				table.insert(ExistingConfigs, v)
			end
		end
		
		local function AddEffectStack()
			local EffectConfig = CreateEffectConfig(Effect, Settings, Damage, Creator, Tick, Variant)
			AddNewEffect({Target = Target, Effect = Effect, EffectConfig = EffectConfig, StartTime = os.clock(), Duration = Duration})
			EffectConfig.Parent = Target
		end
		
		if Stackable or Effects[Effect].Stackable then
			if StackBehavior == "Replace" then
				ClearExistingConfigs(ExistingConfigs)
				AddEffectStack()
			elseif StackBehavior == "Overlap" then
				if not MaxStack or StackCount < MaxStack then
					AddEffectStack()
				end
			elseif StackBehavior == "RemoveAll" then
				ClearExistingConfigs(ExistingConfigs)
				return
			elseif StackBehavior == "ResetAll" then
				local StackCount = 0

				for i, v in ipairs(ActiveEffects) do
					if v.Target == Target and v.Effect == Effect then
						v.StartTime = os.clock()
						v.Duration = Duration
						StackCount += 1
					end
				end
				
				if not MaxStack or StackCount < MaxStack then
					AddEffectStack()
				end
			elseif StackBehavior == "OverwriteAll" then
				for i, v in ipairs(ActiveEffects) do
					if v.Target == Target and v.Effect == Effect then
						v.StartTime = os.clock()
						v.Duration = Duration
						if Damage then
							v.EffectConfig:SetAttribute("Damage", Damage)
						end
						if Tick then
							v.EffectConfig:SetAttribute("Tick", Tick)
						end
						if Variant then
							v.EffectConfig:SetAttribute("Variant", Variant)
						end
						if Creator then
							if typeof(Creator) == "string" then
								v.EffectConfig:SetAttribute("Creator", Creator)
							else
								local OldTag = v.EffectConfig:FindFirstChild("Creator")
								if OldTag then
									OldTag:Destroy()
								end

								local Tag = Instance.new("ObjectValue")
								Tag.Name = "Creator"
								Tag.Value = Creator
								Tag.Parent = v.EffectConfig
							end
						end
						return
					end
				end

				AddEffectStack()
			elseif StackBehavior == "Increase" then
				for i, v in ipairs(ActiveEffects) do
					if v.Target == Target and v.Effect == Effect then
						v.Duration += Duration

						local Attribute = v.EffectConfig:GetAttribute("Damage")
						if Damage and Attribute then
							v.EffectConfig:SetAttribute("Damage", Attribute + Damage)
						end
					end
				end
			elseif StackBehavior == "ReplaceOldest" then
				if not MaxStack or StackCount < MaxStack then
					AddEffectStack()
				else
					local Oldest, OldestIndex, OldestTime = nil, nil, math.huge
					for i, v in ipairs(ActiveEffects) do
						if v.Target == Target and v.Effect == Effect and v.StartTime < OldestTime then
							Oldest = v
							OldestIndex = i
							OldestTime = v.StartTime
						end
					end

					if Oldest and Oldest.EffectConfig then
						Oldest.EffectConfig:Destroy()
						table.remove(ActiveEffects, OldestIndex)
					end

					AddEffectStack()
				end
			elseif StackBehavior == "Queue" then
				while CountActiveStacks(Target, Effect) > 0 do
					task.wait(0.1)
				end
				AddEffectStack()
			else
				AddEffectStack()
			end
		else
			if #ExistingConfigs == 0 then
				AddEffectStack()
			end
		end

		if not CS:HasTag(Target, Effect) then
			CS:AddTag(Target, Effect)
		end
	end
end)
Module script (Setup):
--!strict
local RS = game:GetService("ReplicatedStorage")
local CS = game:GetService("CollectionService")

local FunctionBank = require(script.Parent.Parent.Parent.FunctionBank)
local Effects = FunctionBank.Effects

local Sounds = script.Parent.Parent:FindFirstChild("Sounds")

local EffectHandler = {}

function EffectHandler.Setup(Params: {
	Effect: string,
	VisualTemplate: any?,
	OnTickFunction: (Character: Instance, EffectConfig: Configuration, VisualEffect: any?, Sound: Sound?) -> (),
	OnRemoveFunction: ((Character: Instance) -> ())?
	})
	
	local Effect = Params.Effect
	local VisualTemplate = Params.VisualTemplate
	local OnTickFunction = Params.OnTickFunction
	local OnRemoveFunction = Params.OnRemoveFunction
	
	local List = {"ParticleEmitter", "Fire", "Smoke", "Sparkles"}
	
	CS:GetInstanceAddedSignal(Effect):Connect(function(Character: Instance)
		local Humanoid = Character:FindFirstChildOfClass("Humanoid") :: Humanoid
		local Root = Character:FindFirstChild("HumanoidRootPart") :: BasePart
		if not Humanoid then return end
		
		local VisualEffect = nil
		
		if VisualTemplate then
			if Effects[Effect].SpreadEffect then
				local Ignore = false
				local Immunity = Character:GetAttribute(Effect .. "Immunity") or 0
				if Effect == "Ice" and Immunity < 100 then
					Ignore = true
				end

				if not Ignore then
					for i, Part in ipairs(Character:GetChildren()) do
						if Part:IsA("BasePart") then
							for i, v in ipairs(VisualTemplate:GetChildren()) do
								if table.find(List, v.ClassName) then
									local VisualEffect = v:Clone()
									VisualEffect.Parent = Part
									VisualEffect.Enabled = true
									VisualEffect:SetAttribute("EffectName", Effect)
								end
							end
						end
					end
				end
			else
				if not Root then return end
				
				VisualEffect = VisualTemplate:Clone()

				if Effect ~= "Stun" and Effect ~= "Bleed" then
					if table.find(List, VisualEffect.ClassName) then
						VisualEffect.Enabled = true
						VisualEffect.Parent = Root
					else
						if VisualEffect:IsA("BasePart") then
							local Attachment = VisualEffect:FindFirstChildOfClass("Attachment")

							if Attachment then
								for i, v in ipairs(Attachment:GetChildren()) do
									if table.find(List, v.ClassName) then
										v.Enabled = true
									end
								end
								Attachment.Parent = Root
								Attachment:SetAttribute("EffectName", Effect)
							else
								for i, v in ipairs(VisualEffect:GetChildren()) do
									if table.find(List, v.ClassName) then
										v.Enabled = true
										v.Parent = Root
										v:SetAttribute("EffectName", Effect)
									end
								end
								VisualEffect:Destroy()
							end
						end
					end
				else
					if table.find(List, VisualEffect.ClassName) then
						VisualEffect.Parent = Root
					elseif VisualEffect:IsA("BasePart") then
						local Visual = VisualEffect:FindFirstChildOfClass("Attachment") or VisualEffect:FindFirstChildOfClass("ParticleEmitter")
						
						if Visual then
							VisualEffect = Visual:Clone()
							VisualEffect.Parent = Root
						end
					end
				end
			end
		end
		
		local Sound = Sounds:FindFirstChild(Effect)
		if Sound and Sound:IsA("Sound") then
			Sound = Sound:Clone()
			if Effect ~= "Stun" and Effect ~= "Bleed" and Effect ~= "Void" and Effect ~= "Confusion" then
				Sound.Looped = true
			end
			Sound.Parent = Root
			Sound:Play()
		end
		
		local function HandleConfig(EffectConfig: Configuration)
			if EffectConfig.Name ~= Effect then return end
			
			local Variant = EffectConfig:GetAttribute("Variant")
			
			if Variant and VisualEffect then
				if typeof(Variant) == "Color3" then
					VisualEffect.Color = ColorSequence.new{
						ColorSequenceKeypoint.new(0, Variant),
						ColorSequenceKeypoint.new(1, Variant)
					}
				elseif Variant == "OverseerBurn" then
					VisualEffect.Color = ColorSequence.new{
						ColorSequenceKeypoint.new(0, Color3.fromRGB(0, 85, 0)),
						ColorSequenceKeypoint.new(1, Color3.fromRGB(0, 170, 0))
					}
				elseif Variant == "Paralyze" then
					VisualEffect = script.Parent.Parent.EffectFolder:FindFirstChild("Stun")
					if VisualEffect then
						local Attachment = VisualEffect:FindFirstChildOfClass("Attachment")

						if Attachment then
							VisualEffect = Attachment:Clone()
							VisualEffect.Parent = Root
						end
					end
					Humanoid:ChangeState(Enum.HumanoidStateType.Ragdoll)
				elseif Variant == "Shock" then
					Humanoid:ChangeState(Enum.HumanoidStateType.FallingDown)
				elseif Variant == "TargetColor" and Root then
					VisualEffect.Color = ColorSequence.new{
						ColorSequenceKeypoint.new(0, Root.Color),
						ColorSequenceKeypoint.new(1, Root.Color)
					}
				end
			end

			task.spawn(function()
				OnTickFunction(Character, EffectConfig, VisualEffect, Sound)
				
				local StillActive = false
				for _, v in ipairs(Character:GetChildren()) do
					if v:IsA("Configuration") and v.Name == Effect then
						StillActive = true
						break
					end
				end
				
				if not StillActive then
					if VisualEffect then
						VisualEffect:Destroy()
					end
					if Sound then
						Sound:Destroy()
					end
					
					for i, v in ipairs(Character:GetDescendants()) do
						if v:GetAttribute("EffectName") and v:GetAttribute("EffectName") == Effect then
							v:Destroy()
						end
					end
				end
			end)
		end
		
		for _, v in ipairs(Character:GetChildren()) do
			if v:IsA("Configuration") and v.Name == Effect then
				HandleConfig(v)
			end
		end
		
		local AddedConnection, RemovedConnection
		
		AddedConnection = Character.ChildAdded:Connect(function(Object)
			if Object:IsA("Configuration") and Object.Name == Effect then
				HandleConfig(Object)
			end
		end)
		
		RemovedConnection = CS:GetInstanceRemovedSignal(Effect):Connect(function(Object)
			if Object == Character then
				if OnRemoveFunction then
					OnRemoveFunction(Character)
				end
				if VisualEffect then
					VisualEffect:Destroy()
				end
				if Sound then
					Sound:Destroy()
				end
				if AddedConnection then
					AddedConnection:Disconnect()
				end
				if RemovedConnection then
					RemovedConnection:Disconnect()
				end
			end
		end)
	end)
end

return EffectHandler

Features

  • Many free presets to choose from (Literally everything you will ever need)

  • Any targets can have any amount of status effects being applied at once

  • Status effects are stackable with various stacking options

  • The same type of effect may look or function differently by setting Variant (Green fire, ragdoll stun, etc…)


Limitations

  • I’m a modeler, so my coding skill is limited. HOWEVER, the models I make are also not really good (Jack of
    all trades, master of none moment)

  • No support for duration-stacking (Using Debris service) - Actually, I just found out an alternative way using BindableEvent as I was making this post

  • No colored text formatting here :sob:


Story

One day while looking through the code in free models to see if I could learn something interesting, I came across CollectionService and learnt how to use it

As I was developing my game, Mega Boss Battles, I made this open-sourced FreezeModule for everyone to use with my little scripting knowledge

After a while, I realized there was a problem. If I called this function to freeze players from a projectile script or any tools but they get destroyed before the effect is over, they would be stuck. That’s why I decided to spend months on making and improving this system


Update Logs


Support Me - Check the game below to see this system in effect!
No puns intended

Mega Boss Battles (Showcase) | Clothing Store | Youtube Channel

:package: Roblox Model | :wrench: (Uncopylocked) Test place

26 Likes

I’ll add GIF and video examples in the next few days.
In the meanwhile you can play the game I linked in the post to see what you can use these effects are. Note: You have to get certain gears, or get attacked by certain bosses with elemental attacks!

If you have any feedback, ideas or suggestions to improve this please tell me, I appreciate that!

2 Likes

Heya! long time no see, you made a very unique new stuff beside your freezing module :slight_smile:

2 Likes

Hi! Thanks, It’s been a while lol my scripting skill improved so much since then

2 Likes

I was searching up a little good effects for my RPG i’m currently developing and found it :smiley:

2 Likes

I added a simple showcase video, might make a more detailed GIF with all types of effects and targets with different attributes later
Edit: Omg typos :sob:

2 Likes

19/02/2025

Fixed a mistake of mine
It’s supposed to check for the immunity of the target, not the player dealing damage, sorry
I updated this model and the other damage module as well

if not IgnoreImmunity and AttackType and Target:GetAttribute(AttackType .. "Immunity") then
			Damage = Damage - Damage * (Target:GetAttribute(AttackType .. "Immunity") / 100)
		end
2 Likes

24/02/2025

  • Confusion effect will be reverted correctly upon dying
  • Removed the asset file download option from the post, please use the model and update it instead

I updated the model as well as the source code within the post

1 Like

26/02/2025

  • An update to the damage module within the model and the source code
1 Like

This is cool. Any plans to update it?

2 Likes

Yes, some effects will be added soon along with a test place
:snowflake:Chill, :drop_of_blood:Bleed and small optimizations

3 Likes

hows the update coming along? I forgot, does this also kick off sound effects?

2 Likes

Yeah im excited for the update!

2 Likes

Yes it’s finished
I’ll update my other Stats Handler module today and release the much needed improved version of this later this week
:snowflake:Chill, :drop_of_blood:Bleed, strict typing, max stacking options, much cleaner code for all effects, etc…

1 Like

17/05/2025

  • Added :snowflake:Chill, :revolving_hearts:Heal & :drop_of_blood:Bleed (10 effects in total now)
  • Cleaner code
  • Strict type checking (Just learnt whatever this is)
  • New stacking behaviors
  • New Stats Handler module for managing multiple boosts without overlapping or overwriting any
  • New uncopylocked test place
  • Defense no longer goes below 0 when melted
  • Fixed “Something unexpectedly tried to set the parent of Reset to NULL while trying to set the parent of Reset” error from Confusion effect
2 Likes

Thanks for the suggestion I also added sounds to all effects
Searching for the :drop_of_blood:Bleed sound effect was honestly a big mistake

1 Like

what does this one do?

I tested some of them…

then went to reset, then debuff… and I cannot move at all

also you could add a Heal effect…

also in the ICE block, should there be a debounce, so that after you hit it once, you can sorta move out of it quicker, with out it kicking off and freezing you again?

1 Like

The debuff is pretty strong, but walkspeed value can’t go negative so it stays at 0
Use the gear to gain little walkspeed to move in that case

You can see the scripts in these boost parts to see how much they actually boost
Debuff part, as its name suggest, reduces some of ur stats

You kinda missed the heal effect in my reply above lol

I’ll add a debounce for the ice block right now

  • Added a specific effect giver for each type of effect beside the random effect givers in the test place

I LOVE THIS MODULE SM! Thank you!!

1 Like