How to keep Stunning after getting Hit?

Hi I’m currently making my own PvP Combat game and I was wondering if anyone knows how to have the players stay stunned while still being hit multiple times by another player.

I’m using a module script (That is Server-Sided) to perform this task. This is currently what I have written:

local StunModule = {}

function StunModule:Stun(Character:Instance, Duration: number, StunWalkSpeed: number, StunJumpPower: number)
	local Humanoid = Character:WaitForChild("Humanoid")
	
	if Humanoid then
		if Humanoid.Health <= 0 then
			return
		end
	else
		return
	end
	
	local WalkSpeed = Humanoid.WalkSpeed
	local JumpPower = Humanoid.JumpPower
	
	task.spawn(function()

		Humanoid:SetAttribute("Stunned", true)
		Humanoid.WalkSpeed = StunWalkSpeed
		Humanoid.JumpPower = StunJumpPower
		
		wait(Duration) --// This is the issue.

		Humanoid:SetAttribute("Stunned", false)
		Humanoid.WalkSpeed = WalkSpeed
		Humanoid.JumpPower = JumpPower
	end)
end

return StunModule

The stun works, but the issue is that after the player has been stunned once, the wait function will override the next time a player gets stunned again causing the player to be not stunned and get out of a combo.

How do I make it so that the after the player gets stunned again, the task.spawn function restarts from the beginning and makes the player stunned completely, while also ignoring the wait function?

Please help, thanks!

8 Likes

You can utilize a repeat... until loop.

repeat task.wait(1) Duration -= 1 until Duration <= 0 

That way you can just add to the duration, causing a “stacking” effect. You would of course, need to store the duration somewhere else and add to it as a different variable.

4 Likes

can the duration be an attribute?

1 Like

Yes, you could use an attribute to keep track of the duration, you just need to accommodate the code to utilize it. You could utilize the :GetAttributeChangedSignal() function too, if you really wanted to. In that case, you could just check if the time is greater than 0 and set their WalkSpeed accordingly.

2 Likes

How would I do this in a repeat function??

1 Like

I suggest you change it into values instead of attributes,

make a stunned boolvalue and tick numbervalue in it,

whenever you apply the stun set the tick value to tick() of when the stun was applied

and whenver you want to remove the stun just check if tick same as the tick value in stun and remove it smth like this

local tikk = tick()

Stunned.Value = true
Stunned.Tick.Value = tikk


-- stuff here


if Stunned.Tick.Value == tikk then
    Stunned.Value = false
end
2 Likes

I had this problem some time ago and found the solution

local StunModule = {}
local StunTimes = {}

local function SetDefaultHumanoidAttributes(Character : Instance)
	local Humanoid = Character:FindFirstChild("Humanoid")
	
	Humanoid:SetAttribute("WalkSpeed", Humanoid.WalkSpeed)
	Humanoid:SetAttribute("JumpPower", Humanoid.JumpPower)
end

function StunModule:Stun(Character : Model, Duration: number, StunWalkSpeed: number, StunJumpPower: number)
	local Humanoid = Character:FindFirstChild("Humanoid")
	
	if not Humanoid or Humanoid.Health <= 0 then return end
	
	local WalkSpeed = Humanoid:GetAttribute("WalkSpeed")
	local JumpPower = Humanoid:GetAttribute("JumpPower")
	
	if not WalkSpeed or not JumpPower then
		SetDefaultHumanoidAttributes(Character)
		
		WalkSpeed = Humanoid:GetAttribute("WalkSpeed")
		JumpPower = Humanoid:GetAttribute("JumpPower")
	end
	
	StunTimes[Character] = os.clock()
	
	Humanoid:SetAttribute("Stunned", true)
	
	Humanoid.WalkSpeed = StunWalkSpeed
	Humanoid.JumpPower = StunJumpPower
	
	task.delay(Duration, function()
		if os.clock() - StunTimes[Character] >= Duration then
			Humanoid:SetAttribute("Stunned", false)
			
			Humanoid.WalkSpeed = WalkSpeed
			Humanoid.JumpPower = JumpPower
		end
	end)
end

return StunModule
3 Likes

So the problem is that after waiting, we don’t know if the target was stunned again. To do this, you can use newproxy(), which generates a random but unique value each time it is called. It’s not that much more efficient than math.random() or tick(), but it’s the best option.

There’s a lot of limitations to having a loop to check for stun, such as stun having to be a multiple of how often the stun loop checks and task.wait()/wait() being inneffective at very low delays.

Anyways, here’s the code using newproxy()

StunModule.StunProxy = newproxy() --//Also, you should use a variable in the module instead of an attribute.

function bestunned()
	local CurrentProxy = newproxy() --//Declare new proxy.
	StunModule.StunProxy = CurrentProxy --//If this function is called again, the module's StunProxy variable will change.
	task.spawn(function()

		Humanoid:SetAttribute("Stunned", true)
		Humanoid.WalkSpeed = StunWalkSpeed
		Humanoid.JumpPower = StunJumpPower
		
		task.wait(Duration) --// Use task.wait() instead of wait.
		if Module.StunProxy == CurrentProxy then

			Humanoid:SetAttribute("Stunned", false)
			Humanoid.WalkSpeed = WalkSpeed
			Humanoid.JumpPower = JumpPower

		end
	end)
end

Oh, and compared to AkaNub’s code, this is closer to what you’d see in a fighting game where new stun takes priority over old stun. If someone stunned for 10s is hit at 5 seconds for 1 second of stun, they’ll recover in 1 second, not 5.

3 Likes

I had a similar problem with disabling movement/jumping, so I ended up using integer attributes instead of booleans to prevent the issue of the player being able to move when they shouldn’t.
I also used attributes for offsets and multipliers, to make sure nothing weird happens when increasing/decreasing walkspeed.

Your method seems interesting, however. It might be worthwhile for me to look into using newproxy() if I run into a similar issue for future projects.
It could also potentially work for an attribute system similar to Minecraft’s, where stat modifiers are given unique ids.

2 Likes

Ok I tried using this, but sometimes the player gets stun locked permanently except if you hit them again. How do you fix this and should the script be a local or server script?

1 Like

What’s the code look like? The version I’ve written here shouldn’t have any logic errors.

1 Like

Oh, I assumed that you had one of these module scripts per character, not one module for everyone. You should use a dictionary to store who’s CurrentProxy is who’s.

Is it recommended to have these module scripts per character or one module for everyone? Also how do I use a dictionary to store who’s CurrentProxy is who’s? I still don’t know how to use meta tables…

In my honest opinion, there is no need to complicate things if you just want something simple. Here is a script that will run every hearbeat that will do the calculations and applying for you.

--!strict

local Players = game:GetService("Players")
local StarterPlayer = game:GetService("StarterPlayer")
local RunService = game:GetService("RunService")

-- stun settings
local STUN_DURATION_ATTRIBUTE_NAME: string = "StunDuration"
local NORMAL_WALK_SPEED: number = StarterPlayer.CharacterWalkSpeed
local STUN_WALK_SPEED: number = NORMAL_WALK_SPEED / 4

RunService.Heartbeat:Connect(function(deltaTime: number): ()
	-- loop through every player every heartbeat
	for _: number, player: Player in Players:GetPlayers() do
		-- check if the player has a character, a humanoid, and a valid attribute
		local humanoid: Humanoid? = if player.Character then player.Character:FindFirstChildOfClass("Humanoid") else nil
		local hasAttribute: boolean = if humanoid ~= nil then typeof(humanoid:GetAttribute(STUN_DURATION_ATTRIBUTE_NAME)) == "number" else false
		
		if humanoid ~= nil and hasAttribute == true then
			-- calculate the new duration by subtracting the current duration by deltaTime
			local newDuration: number = math.clamp(humanoid:GetAttribute(STUN_DURATION_ATTRIBUTE_NAME) - deltaTime, 0, math.huge)
			
			-- apply the new duration and walkspeed
			humanoid:SetAttribute(STUN_DURATION_ATTRIBUTE_NAME, newDuration)
			humanoid.WalkSpeed = if newDuration > 0 then STUN_WALK_SPEED else NORMAL_WALK_SPEED
		end
	end
end)

Modify as needed.

1 Like

You could do one modulescript per character, or one modulescript for all characters. One global modulescript is more preformant, but also harder to make and debug.

How a global modulescript would work is by storing everyone’s stats, such as health and stun, as a dictionary in a dictionary. Then when using a function related to a characters stats, we grab that entry from the dictionary.
Like this:

CharacterStats = { --//First dictionary layer
	Player1 = { --//Second dictionary layer
		Health = 100;
		Speed = 16
		StunProxy = newproxy()
	};
	Player2 = { --//Still the second dictionary layer
		Health = 100;
		Speed = 16
		StunProxy = newproxy()
	}
}

--[[
If we needed to hit Player1, we'd get their stats with CharacterStats["Player1"]
]]

We’re not using newproxy() for metatables, but as an identifier. You could do the same with a number that increments by 1, or any type of other value as long as it’s guarenteed to be unique.

Besides the unnecessary use of RunService.Heartbeat(), all this does is handle the stun slowing down the player, not actually apply the stun status nor prevent stun ending when it shouldn’t.

1 Like
local StunModule = {}

function StunModule:Stun(Character:Instance, Duration: number, StunWalkSpeed: number, StunJumpPower: number)
    local Humanoid = Character:WaitForChild("Humanoid")
    
    if Humanoid then
        if Humanoid.Health <= 0 then
            return
        end
    else
        return
    end
    
    local WalkSpeed = Humanoid.WalkSpeed
    local JumpPower = Humanoid.JumpPower
    
    -- Create a coroutine to manage the stun effect
    local stunCoroutine = coroutine.create(function()
        Humanoid:SetAttribute("Stunned", true)
        Humanoid.WalkSpeed = StunWalkSpeed
        Humanoid.JumpPower = StunJumpPower
        
        -- Wait for the stun duration
        coroutine.yield(Duration)
        
        Humanoid:SetAttribute("Stunned", false)
        Humanoid.WalkSpeed = WalkSpeed
        Humanoid.JumpPower = JumpPower
    end)
    
    -- Resume the coroutine, which will yield and resume when the stun duration is over
    coroutine.resume(stunCoroutine)
end

return StunModule
1 Like

do I put this in sever script service or replicated storage??

ReplicatedStorage is accessible to both clients and server, while server script storage is server only. If you don’t need to put something in ReplicatedStorage, it should go in ServerStorage/ServerScriptStorage.

2 Likes

is it more optimal in the server script service?

No, it just can’t be stolen there. Exploiters can read and steal client/module scripts that aren’t in server-sided storage.

1 Like