Jump to content

[TUTORIAL] Making Auras, using DoPeriodicTask and DoTaskInTime


Ultroman
 Share

Recommended Posts

This is going to be a long write-up about how you can make your own auras. I invite criticism, because I am not a master of Lua as such. I just know these approaches work, and I keep having to teach people how to do these, so I thought I'd just do a write-up, so they can educate themselves and try something out before asking. It's not simple stuff for newcomers, so I've tried to comment as much as I can about gotcha's and such. They may be a bit hard to follow, but I have tried to comment everything in them to help you understand how they work.

If something is very unclear or you have noticed something that would label me as a colossal idiot, please do point it out. I'm only trying to help, so if my efforts lead to the opposite of help, I would like to be notified :)

Nice to know about auras

There are a few ways to implement auras. One is to make a component, like the sanityaura component that you may have seen while looking for clues in the game code. While it is often a good thing to extrapolate such a thing as an aura into a separate component, there is a hidden catch to how the sanityaura component works, which makes it hard for modders to replicate.

It simply has a value indicating how much sanity is to be applied per second, but you can also instead give it a function which calculates that value by whatever influences you want. You may have noticed that the sanityaura component doesn't actually apply the sanity anywhere. This is because the effect of the sanityaura component is integrated into the frame-by-frame sanity-calculations done in the sanity component. If you were to use this method for your own component, you would have to somehow integrate the same kind of change to the calculations done in the component for whatever you want to change, e.g., health or hunger, and the game code is not very open for that kind of thing. The calculations are usually very large functions which do the calculations AND the application of the result in one function, so there's no way for you to hook directly into the calculations.

There is a way to do this as a component, though, and I will add a template and examples of this later, but for now I will focus on another way.

Another way to do these auras is to use periodic tasks, which will be the focus of the first couple of examples.
 

How to use periodic tasks
A periodic task is a task which we can start on an entity, with or without an initial delay, which then calls a given function every X seconds. The function declaration looks like this:

DoPeriodicTask(timeBetweenCallsToTheFunction, myFunction, initialDelayTime, ...)

So, first parameter is time in seconds between consecutive calls of the given function, myFunction. Setting it to 0 makes the function get called every frame. The myFunction parameter is the function we want to periodically call. The initialDelayTime parameter is the time in seconds we want to delay before starting the periodic task, i.e., the time to wait before calling myFunction the first time.

The ... means that it can take a number of extra parameters, and all these extra parameters are also sent to the given function when it is called. See example below.

This would be a simple periodic task, periodically calling the given parameterless function every 1.0 second:

local myFunction = function(inst)
	-- Do something
end

inst:DoPeriodicTask(1.0, myFunction)

Notice that the inst is always automatically passed as the first parameter to the given function, without us specifically telling it to. This is a convenience given to us by Klei and Lua, but it is also something you must remember to include when writing up your function parameters.

Here is an example of a periodic task, periodically calling the given function every 1.0 second, with an initial delay of 3.0 seconds and extra parameters:

local myFunction = function(inst, myVariable, myOtherVariable)
	-- Do something
end

local myVariable = "some string"
local myOtherVariable = 5.0

inst:DoPeriodicTask(1.0, myFunction, 3.0, myVariable, myOtherVariable)

If you do not want an initial delay, you can just write nil instead of a number for the initialDelayTime parameter.


WARNING: Periodic tasks should be used with care. Try not to make them call their given function too often. For example, to give 1 health every second, you can, e.g., give 1 health every 1 second, or you can give 0.1 health every 0.1 seconds. The latter is 10 more function calls and so it is 10 times heavier to run.
Also, the task-timing is not 100% accurate, since the tasks are called on a frame-by-frame basis, so they are never called EXACTLY at the amount of time you have given it, but simply at the first frame after the time has run out, so it may be delayed by 0-50+ ms, and that delay happens for each consecutive scheduled call of the given function. Keep that in mind, so you don't try to do several cooperating tasks requiring perfect timing.
 

So, now we know how to START a periodic task, but usually we also want to know how to END it again. For that, we can store the task we've made in a variable, and use that variable to cancel / stop the task later.

local myFunction = function(inst)
	-- Do something
end

-- You can save the task in a local variable, e.g., if the life-time of the task is controlled in the same file (or you know what you are doing)...
local myUniqueTaskVariableName = inst:DoPeriodicTask(1.0, myFunction)
-- OR you can save the task to a variable on your instance.
inst.myUniqueTaskVariableName = inst:DoPeriodicTask(1.0, myFunction)

-- Later, in some other part of the code, where you have access to the same inst or variable,
-- you can cancel the task by calling Cancel() on it, so depending on how you saved the task:
myUniqueTaskVariableName:Cancel()
-- OR
inst.myUniqueTaskVariableName:Cancel()

IMPORTANT NOTE:
It is advised to set your task-variable to nil after cancelling it, so you can do a nil-check before trying to cancel a task that might not be running (not that it would be catastrophic, but it is extra needless work for the CPU). This also makes the task-variable function as a sort of state-variable, so if the task is not nil, you know it is active. You might be able to use that for some checks elsewhere in your code.



Now we can start making auras :D

Let us start with a simple aura, where I can explain the whole concept without too much complexity. It introduces a few things, like the FindEntities function and entity tags, which are explained in further detail following this first example.

Basic constant stat regen aura

Below is an example of a never-ending aura which provides health regeneration to players around the entity with the aura (but you can make it affect anything you want, within any range/distance you want).

You can add the following code directly in your entity's Lua file, at the bottom of the fn() or master_postinit() function. I say "entity" because this aura can be added to any player, monster, item or whatever other entity. It runs forever, even when the entity is dead, so we have a check that makes sure it does nothing if it has a health-component and is dead. We could have taken care of this by cancelling the task when the entity died, and then started it again when the entity was revived (mostly useful for players). There are events you can listen to for both of those events, so when the entity dies you can access your task-variable on the entity to cancel the task. However, the task is automatically removed when the entity is removed from the world. Anyway, this is the simplified version with lots of comments.

Spoiler

inst:DoPeriodicTask(1.0, function(inst)
	-- Do nothing if the entity has a health component and is dead, or it is a playerghost.
	if inst.components.health and inst.components.health:IsDead() or inst:HasTag("playerghost") then
		return
	end
	
	-- Store the position of the entity in x, y, z variables.
	local x,y,z = inst.Transform:GetWorldPosition()
	
	-- Description of important function, which finds specific entities within a range:
	-- TheSim:FindEntities(x, y, z, radius, mustHaveTags, cantHaveTags, mustHaveOneOfTheseTags)
	
	-- I have set the radius to be 10. You can set it to whatever radius you want.
	-- They must not be ghosts, so "playerghost" goes in the cantHaveTags list, along with "INLIMBO".
	-- NOTE that if you don't have any mustHaveTags or mustHaveOneOfTheseTags, then you really
	-- need some strong cantHaveTags to make sure you only get exactly what you want. 
	local players = TheSim:FindEntities(x, y, z, 10, {"player"}, {"playerghost", "INLIMBO"}, nil)
	
	-- Run through all the players within range...
	for _,v in ipairs(players) do
		-- ...and if they have a health component, add 1 to their health.
		if v.components.health then
			v.components.health:DoDelta(1)
		end
	end
end)

 

Tags?

Each entity has a bunch of tags, labelling it as whatever it is. Think of it as post-its on all entities defining them as "lightsource", "flammable", "tree", "creaturehome" or whatever. They aren't functional in and of themselves, but are used in the code to categorize things which need to behave similarly, so we can use a function like FindEntities to find all entities within an area that are a light source or flammable or perhaps just all trees or all players. Tags are used for MUCH more than that, but this is one way to leverage the tag-system. All you have to do, is find out which tags you want to target, by examining the game code to see which tags the target entities have, or will have, if you're looking for dynamic tags, like "hurt" or "burnt" or whatever. The game is full of tags and I can't cover all of them.

What does the "INLIMBO" tag mean?

"INLIMBO" is a tag that is put on entities that aren't "active in the scene" but still exist in the game. Two examples are: items in containers/inventories, and entities which get deactivated when outside of a certain range of players. For example, when no players are near or when put into a container, rabbits and rocks and such "go to limbo", i.e., they deactivate, and then get this tag.

Stripping them from your search makes sure you don't accidentally get inactive entities in your results, and by the looks of how much Klei uses them, I think it also makes the search faster.

Other common tags

Other common tags to omit, are the "FX" tag which is found on things like fire and fireflies, and "DECOR" which covers decorative entities. There is also "NOCLICK", but I believe this is mostly used when dealing with interactions. For example, when a thing is used and it does an animation before it can be used again, that thing usually gets the "NOCLICK" tag put on it during the animation. But it is NOT just for player interactions. Quite a lot of animals also consider anything with the "NOCLICK" tag untargetable, which leads me to believe it is used for omitting the entity from not only player input interactions, but ANY interactions, kind of "hiding" them in plain sight. I've never used "NOCLICK" myself, but it seems to have many uses. Blowdarts get "NOCLICK" after being thrown *shrugs* I don't know its exact meaning.

FindEntities Performance Tip

When using TheSim:FindEntities continuously, like in our periodic tasks, you have to consider that every time we do our FindEntities-call, we create new lists for our tag-lists (mustHaveTags, cantHaveTags and mustHaveOneOfTheseTags). This is not ideal if they will be the same every time. To alleviate this, cache your tag-lists somewhere outside the DoPeriodicTask-call, and use those variables in your FindEntities-call.
Here is an example with the list-variables being declared outside the function call and then used inside of it. Watch out for the naming of these local variables if you use the same code multiple times within the same scope. If you find yourself reusing the same tag-lists again and again, you can also define some general tag-lists that are available throughout your mod, but that's outside the scope of this guide.

Spoiler

-- NOTE that if you don't have any mustHaveTags or mustHaveOneOfTheseTags, then you really
-- need some strong cantHaveTags to make sure you only get exactly what you want. 
local mustHaveTags = { "player" }
local cantHaveTags = { "playerghost", "INLIMBO" }
local mustHaveOneOfTheseTags = nil

inst:DoPeriodicTask(1.0, function(inst)
	-- Do nothing if the entity has a health component and is dead, or it is a playerghost.
	if inst.components.health and inst.components.health:IsDead() or inst:HasTag("playerghost") then
		return
	end
	
	local x,y,z = inst.Transform:GetWorldPosition()
	
	local players = TheSim:FindEntities(x, y, z, 10, mustHaveTags, cantHaveTags, mustHaveOneOfTheseTags)
	
	-- Run through all the players within range...
	for _,v in ipairs(players) do
		-- ...and if they have a health component, add 1 to their health.
		if v.components.health then
			v.components.health:DoDelta(1)
		end
	end
end)

 


Triggered, timed aura template

This template doesn't do anything out-of-the-box, but it keeps calling an application function, called onApplyAura, which has the ability to add an effect to entities within range of our entity, and then uses DoTaskInTime to schedule another function, onEndAura, to be called after X time. As long as the other entities stay within range of our entity when it triggers its aura (by calling the function applyAuraInRange), the duration is renewed on the affected entities. It then uses DoTaskInTime to end the task after a while, removing the effects of the aura.

Keep an eye on the comments! They indicate where you should add code to add an effect to this aura and where to remove it again.

See below for examples of how to use this template!

All the code between the "START OF AURA CODE" comment and the "END OF AURA CODE" comment should be added ABOVE the fn() or master_postinit() function of your prefab, and then one of the triggering mechanisms (described at the bottom of the example) should be added at the bottom of the fn() or master_postinit() function of your prefab.

Spoiler

-- START OF AURA CODE

local onEndAura = function(receiver, applierGUID)
	-- We do not use applierGUID in this example of onEndAura, but it is critical for other use cases.


	-- Fail-safe.
	if not receiver or not receiver:IsValid() then
		return
	end
	
	-- Do whatever you want here, removing your extra tasks, resetting variables and
	-- whatever else you did in the onApplyAura function below.
end

local onApplyAura = function(receiver, applier)
	-- "receiver" is the entity getting the aura.
	-- "applier" is whatever entity is applying the aura.

	-- Fail-safe.
	if not receiver or not receiver:IsValid() then
		return
	end
	

	-- Do whatever checks you want for NOT applying the aura here.


	-- Like, if you are doing a sanity aura and the receiver does not have
	-- a sanity component, return immediately, like so:
	-- if not receiver.components.sanity then
		-- return
	-- end

	
	-- All entities have a unique GUID (an ID). We can leverage that for unique variable names.
	-- We don't know what might happen between this starting point and starting the endAuraTask
	-- down below, so we make sure to save the name before something happens to the applier.
	local applierGUID = applier.GUID
	
	
	-- Do whatever you want here, adding extra tasks, setting variables or whatever.
	-- Make sure to undo all of that somehow in the onEndAura function above.
	
	
	--[=====[
	  
	  We want the aura to end after a while, so we need a task for that.
	  
	  We need unique names for our tasks, because they will be stored in a dictionary-like
	  structure on the entities, with the unique name (henceforth "uniquename") we give it.
	  This gives us power over how many of the same buff can be applied to the same entity.
	  
	  If you apply a buff with a uniquename that is not unique, then the buff will replace
	  an existing buff with the same uniquename (I think...or just not apply the new buff;
	  either way, only one of the buffs will remain). You can use this to limit the amount
	  of the same buff that can be applied on an entity. If each entity should only be able
	  to have one instance of the buff, then you make the uniquename the same for all of them,
	  e.g., "myUniqueName7548391". If you want to make it so the same entity cannot add more
	  than one of the same buff to the same target, BUT allow entities to have multiple instances
	  of the same buff from different sources (appliers), then you make your uniquename
	  "myUniqueName"..applier.GUID, as we do in the coming example.
	  
	--]=====]
	
	
	-- Create a uniquename to store the end-aura task on the receiver.
	-- We append the GUID of the entity that applied the aura, so we can support having
	-- several instances of our aura on from different entities.
	local endAuraTaskUniqueName = "myUniqueTaskIdForEntireAura"..applierGUID
	
	-- If the receiver already has an end-aura task, then we need to cancel it,
	-- so we can restart it with a refreshed end-time.
	if receiver[endAuraTaskUniqueName] then
		receiver[endAuraTaskUniqueName]:Cancel()
		receiver[endAuraTaskUniqueName] = nil
	end
	
	-- Use DoTaskInTime to schedule calling the function that ends the aura after a while without renewals.
	-- We store the task on the receiver using our unique variable-name.
	-- 
	-- Normally, DoTaskInTime only takes two parameters: a delay in seconds and a given function.
	-- The given function, in this case onEndAura, is only passed the parameter of the source, in this case
	-- the "receiver" variable, but whatever extra values you put on it afterwards, like applierGUID here,
	-- will also be passed into the function we give it, so onEndAura gets these values passed into it as
	-- separate parameters (see above).
	-- We do not use applierGUID in this example of onEndAura, but it is critical for other use cases.
	receiver[endAuraTaskUniqueName] = receiver:DoTaskInTime(10.0, onEndAura, applierGUID)
end

local applyAuraInRange = function(inst)
	-- Store the position of the aura-emitting entity in x, y, z variables.
	local x,y,z = inst.Transform:GetWorldPosition()
	
	-- Description of important function, which finds specific entities within a range:
	-- TheSim:FindEntities(x, y, z, radius, mustHaveTags, cantHaveTags, mustHaveOneOfTheseTags)
	
	-- For this particular use case, I have limited it to any player that is not a ghost or in limbo.
	-- You can change the tag-list parameters to fit whatever you need to affect and not affect.
	-- I have set the radius to be 12. You can set it to whatever radius you want.
	local ents = TheSim:FindEntities(x, y, z, 12, {"player"}, {"playerghost", "INLIMBO"}, nil)
	
	-- Apply the aura to any of the found entities within the radius, including the aura-emitting
	-- entity itself if it fits the tag-list parameters!
	for i, v in ipairs(ents) do
		onApplyAura(v, inst)
	end
end

-- END OF AURA CODE

-- You can trigger the aura application task however you want. You can hook it up to any event,
-- since the only parameter it takes is the inst of the entity applying the aura.
-- Example: inst:ListenForEvent("onattackother", applyAuraInRange)
--
-- You can also just have your entity always apply the aura to entities around them.
-- Example:
-- This calls the function, which reapplies the aura to surrounding entities, every 1.0 seconds.
-- inst:DoTaskInTime(1.0, applyAuraInRange)
--
-- Add one of the lines above, or something similar, directly in your entity's Lua file,
-- at the bottom of the fn() or master_postinit() function.

 

 

Triggered, timed sanity regen aura

This is an example of an implementation using the template above. I add a never-ending sanity regeneration task to the players that are within range, and remove it again when they have been out of range of the aura-emitting entity for more than 10 seconds. This is done using a combination of tasks: one DoPeriodicTask to continuously apply the sanity aura-effect and another similar type of task, DoTaskInTime, which ends the task after a while, cancelling the periodic sanity aura task.

All the code between the "START OF AURA CODE" comment and the "END OF AURA CODE" comment should be added ABOVE the fn() or master_postinit() function of your prefab, and then one of the triggering mechanisms (described at the bottom of the example) should be added at the bottom of the fn() or master_postinit() function of your prefab.

Spoiler

-- START OF AURA CODE

local onEndAura = function(receiver, applierGUID, sanityAuraTaskUniqueName)
	-- We do not use applierGUID in this example of onEndAura, but it is critical for other use cases.


	-- Fail-safe.
	if not receiver or not receiver:IsValid() then
		return
	end

	-- Do whatever you want here, removing your extra tasks, resetting variables and
	-- whatever else you did in the onApplyAura function below.

	-- If the receiver has the sanity aura task, cancel it.
	if receiver[sanityAuraTaskUniqueName] then
		receiver[sanityAuraTaskUniqueName]:Cancel()
		receiver[sanityAuraTaskUniqueName] = nil
	end
end

local onApplyAura = function(receiver, applier)
	-- "receiver" is the entity getting the aura.
	-- "applier" is whatever entity is applying the aura.

	-- Fail-safe.
	if not receiver or not receiver:IsValid() then
		return
	end
	
	
	-- Do whatever checks you want for NOT applying the aura here.
	
	
	-- We are doing a sanity aura, so if the receiver does not have
	-- a sanity component, we want to return immediately.
	if not receiver.components.sanity then
		return
	end
	
	
	-- All entities have a unique GUID (an ID). We can leverage that for unique variable names.
	local applierGUID = applier.GUID
	
	
	-- Do whatever you want here, adding extra tasks, setting variables or whatever.
	-- Make sure to undo all that somehow in the onEndAura function above.
	
	
	--[=====[
	  
	  We want the aura to end after a while, so we need a task for that.
	  
	  We need unique names for our tasks, because they will be stored in a dictionary-like
	  structure on the entities, with the unique name (henceforth "uniquename") we give it.
	  This gives us power over how many of the same buff can be applied to the same entity.
	  
	  If you apply a buff with a uniquename that is not unique, then the buff will replace
	  an existing buff with the same uniquename (I think...or just not apply the new buff;
	  either way, only one of the buffs will remain). You can use this to limit the amount
	  of the same buff that can be applied on an entity. If each entity should only be able
	  to have one instance of the buff, then you make the uniquename the same for all of them,
	  e.g., "myUniqueName7548391". If you want to make it so the same entity cannot add more
	  than one of the same buff to the same target, BUT allow entities to have multiple instances
	  of the same buff from different sources (appliers), then you make your uniquename
	  "myUniqueName"..applier.GUID, as we do in the coming example.
	  
	--]=====]
	
	
	-- Create a unique variable-name to store the sanity aura task in on the receiver.
	-- We append the GUID of the entity that applied the aura, so we can support having
	-- several instances of our aura on from different entities.
	local sanityAuraTaskUniqueName = "myUniqueTaskIdForTheAurasSanityEffect"..applierGUID
	
	-- If the receiver does not already have our endless sanity aura on from this particular
	-- applier, start the sanity-aura task, and store it in a variable on the receiver.
	if not receiver[sanityAuraTaskUniqueName] then
		-- We make a task which applies a sanity change of 1 every 1.0 seconds.
		receiver[sanityAuraTaskUniqueName] = receiver:DoPeriodicTask(1.0, function(receiver)
			-- The "true" here means that it will NOT make the sanity badge pulse and make a sound.
			receiver.components.sanity:DoDelta(1, true)
		end)
	end
	
	
	-- We want the aura to end after a while, so we need another task for that.
	--
	-- Create a unique variable-name to store the end-aura task on the receiver.
	-- We append the GUID of the entity that applied the aura, so we can support having
	-- several instances of our aura on from different entities.
	local endAuraTaskUniqueName = "myUniqueTaskIdForEntireAura"..applierGUID
	
	-- If the receiver already has an end-aura task, then we need to cancel it,
	-- so we can restart it with a refreshed end-time.
	if receiver[endAuraTaskUniqueName] then
		receiver[endAuraTaskUniqueName]:Cancel()
		receiver[endAuraTaskUniqueName] = nil
	end
	
	-- Use DoTaskInTime to schedule calling the function that ends the aura after a while without renewals.
	-- We store the task on the receiver using our unique variable-name.
	-- 
	-- Normally, DoTaskInTime only takes two parameters: a delay in seconds and a given function.
	-- The given function, in this case onEndAura, is only passed the parameter of the source, in this case
	-- the "receiver" variable, but whatever extra values you put on it afterwards, like applierGUID and
	-- sanityAuraTaskUniqueName here, will also be passed into the function we give it, so onEndAura gets
	-- these values passed into it as separate parameters (see above).
	-- We do not use applierGUID in this example of onEndAura, but it is critical for other use cases.
	receiver[endAuraTaskUniqueName] = receiver:DoTaskInTime(10.0, onEndAura, applierGUID, sanityAuraTaskUniqueName)
end

local applyAuraInRange = function(inst)
	-- Store the position of the aura-emitting entity in x, y, z variables.
	local x,y,z = inst.Transform:GetWorldPosition()
	
	-- Description of important function, which finds specific entities within a range:
	-- TheSim:FindEntities(x, y, z, radius, mustHaveTags, cantHaveTags, mustHaveOneOfTheseTags)
	
	-- For this particular use case, I have limited it to any player that is not a ghost or in limbo.
	-- You can change the tag-list parameters to fit whatever you need to affect and not affect.
	-- I have set the radius to be 12. You can set it to whatever radius you want.
	local ents = TheSim:FindEntities(x, y, z, 12, {"player"}, {"playerghost", "INLIMBO"}, nil)
	
	-- Apply the aura to any of the found entities within the radius, including the aura-emitting
	-- entity itself if it fits the tag-list parameters!
	for i, v in ipairs(ents) do
		onApplyAura(v, inst)
	end
end

-- END OF AURA CODE

-- You can trigger the aura application task however you want. You can hook it up to any event,
-- since the only parameter it takes is the inst of the entity applying the aura.
-- Example: inst:ListenForEvent("onattackother", applyAuraInRange)
--
-- You can also just have your entity always apply the aura to entities around them.
-- Example:
-- This calls the function, which reapplies the aura to surrounding entities, every 1.0 seconds.
-- inst:DoTaskInTime(1.0, applyAuraInRange)
--
-- Add one of the lines above, or something similar, directly in your entity's Lua file,
-- at the bottom of the fn() or master_postinit() function.

 

 

Triggered, timed sanity regen and damage reduction aura, triggered by attacking something

This is an example of an implementation using the template above. I add a never-ending sanity regeneration task to the players that are within range, and remove it again when they have been out of range of the aura-emitting entity for more than 10 seconds. For this example, I have also added a damage reduction of 15%. This is done using a combination of tasks: one DoPeriodicTask to continuously apply the sanity aura-effect and another similar type of task, DoTaskInTime, which ends the task after a while, cancelling the periodic sanity aura task. The damage reduction is simply applied and removed in onApplyAura and onEndAura, respectively.
Another change for this example, is that the aura is triggered by the entity attacking something.

All the code between the "START OF AURA CODE" comment and the "END OF AURA CODE" comment should be added ABOVE the fn() or master_postinit() function of your prefab, and then one of the triggering mechanisms (described at the bottom of the example) should be added at the bottom of the fn() or master_postinit() function of your prefab.

Spoiler

-- START OF AURA CODE

local onEndAura = function(receiver, applierGUID, sanityAuraTaskUniqueName)
	-- We do not use applierGUID in this example of onEndAura, but it is critical for other use cases.


	-- Fail-safe.
	if not receiver or not receiver:IsValid() then
		return
	end

	-- Do whatever you want here, removing your extra tasks, resetting variables and
	-- whatever else you did in the onApplyAura function below.

	-- If the receiver has the sanity aura task, cancel it.
	if receiver[sanityAuraTaskUniqueName] then
		receiver[sanityAuraTaskUniqueName]:Cancel()
		receiver[sanityAuraTaskUniqueName] = nil
	end
	
	-- Remove the 15% damage taken decrease modifier. Change the modifier key to something unique,
	-- and remember to also use the same key when you apply the modifier!
	if receiver.components.combat then
		receiver.components.combat.externaldamagetakenmultipliers:RemoveModifier(applierGUID, "myuniquemodifierkey")
	end
end

local onApplyAura = function(receiver, applier)
	-- "receiver" is the entity getting the aura.
	-- "applier" is whatever entity is applying the aura.

	-- Fail-safe.
	if not receiver or not receiver:IsValid() then
		return
	end
	
	
	-- Do whatever checks you want for NOT applying the aura here.
	
	
	-- We are doing a sanity AND damage reduction aura, so if the receiver
    -- has neither a sanity- nor a combat- component, then we want to return immediately.
	if not receiver.components.sanity and not receiver.components.combat then
		return
	end
	
	
	-- All entities have a unique GUID (an ID). We can leverage that for unique variable names.
	local applierGUID = applier.GUID
	
	
	-- Do whatever you want here, adding extra tasks, setting variables or whatever.
	-- Make sure to undo all that somehow in the onEndAura function above.
	
	
	--[=====[
	  
	  We want the aura to end after a while, so we need a task for that.
	  
	  We need unique names for our tasks, because they will be stored in a dictionary-like
	  structure on the entities, with the unique name (henceforth "uniquename") we give it.
	  This gives us power over how many of the same buff can be applied to the same entity.
	  
	  If you apply a buff with a uniquename that is not unique, then the buff will replace
	  an existing buff with the same uniquename (I think...or just not apply the new buff;
	  either way, only one of the buffs will remain). You can use this to limit the amount
	  of the same buff that can be applied on an entity. If each entity should only be able
	  to have one instance of the buff, then you make the uniquename the same for all of them,
	  e.g., "myUniqueName7548391". If you want to make it so the same entity cannot add more
	  than one of the same buff to the same target, BUT allow entities to have multiple instances
	  of the same buff from different sources (appliers), then you make your uniquename
	  "myUniqueName"..applier.GUID, as we do in the coming example.
	  
	--]=====]
	
	-- NOTE: A bit different from the previous version with only sanity regen;
    -- since we have two effects, both sanity regen and damage reduction,
    -- we need to only apply the parts of our buff that makes sense for the
    -- given entity. If it has not sanity component, then we only apply the
    -- damage reduction, and vice versa. If it is missing both components, then we have
    -- already returned.
    
    if receiver.components.sanity then
		-- Create a unique variable-name to store the sanity aura task in on the receiver.
		-- We append the GUID of the entity that applied the aura, so we can support having
		-- several instances of our aura on from different entities.
		local sanityAuraTaskUniqueName = "myUniqueTaskIdForTheAurasSanityEffect"..applierGUID
		
		-- If the receiver does not already have our endless sanity aura on from this particular
		-- applier, start the sanity-aura task, and store it in a variable on the receiver.
		if not receiver[sanityAuraTaskUniqueName] then
			-- We make a task which applies a sanity change of 1 every 1.0 seconds.
			receiver[sanityAuraTaskUniqueName] = receiver:DoPeriodicTask(1.0, function(receiver)
				-- The "true" here means that it will NOT make the sanity badge pulse and make a sound.
				receiver.components.sanity:DoDelta(1, true)
			end)
		end
	end
	
	-- If the entity has a combat component, add a 15% damage taken decrease modifier.
    -- Change the modifier key to something unique, and remember to also use the same key
    -- when you apply the modifier!
	-- Applying the same modifier with the same source and key just overwrites the current
	-- multiplier, and does not add a new one. Also, using the GUID as the source, two players
	-- can play your character and both can apply their separate multipliers simultaneously.
	if receiver.components.combat then
		receiver.components.combat.externaldamagetakenmultipliers:SetModifier(applierGUID, 0.85, "myuniquemodifierkey")
	end
	
	
	-- We want the aura to end after a while, so we need another task for that.
	--
	-- Create a unique variable-name to store the end-aura task on the receiver.
	-- We append the GUID of the entity that applied the aura, so we can support having
	-- several instances of our aura on from different entities.
	local endAuraTaskUniqueName = "myUniqueTaskIdForEntireAura"..applierGUID
	
	-- If the receiver already has an end-aura task, then we need to cancel it,
	-- so we can restart it with a refreshed end-time.
	if receiver[endAuraTaskUniqueName] then
		receiver[endAuraTaskUniqueName]:Cancel()
		receiver[endAuraTaskUniqueName] = nil
	end
	
	-- Use DoTaskInTime to schedule calling the function that ends the aura after a while without renewals.
	-- We store the task on the receiver using our unique variable-name.
	-- 
	-- Normally, DoTaskInTime only takes two parameters: a delay in seconds and a given function.
	-- The given function, in this case onEndAura, is only passed the parameter of the source, in this case
	-- the "receiver" variable, but whatever extra values you put on it afterwards, like applierGUID and
	-- sanityAuraTaskUniqueName here, will also be passed into the function we give it, so onEndAura gets
	-- these values passed into it as separate parameters (see above).
	-- We do not use applierGUID in this example of onEndAura, but it is critical for other use cases.
	receiver[endAuraTaskUniqueName] = receiver:DoTaskInTime(10.0, onEndAura, applierGUID, sanityAuraTaskUniqueName)
end

local applyAuraInRange = function(inst)
	-- Store the position of the aura-emitting entity in x, y, z variables.
	local x,y,z = inst.Transform:GetWorldPosition()
	
	-- Description of important function, which finds specific entities within a range:
	-- TheSim:FindEntities(x, y, z, radius, mustHaveTags, cantHaveTags, mustHaveOneOfTheseTags)
	
	-- For this particular use case, I have limited it to any player that is not a ghost or in limbo.
	-- You can change the tag-list parameters to fit whatever you need to affect and not affect.
	-- I have set the radius to be 12. You can set it to whatever radius you want.
	local ents = TheSim:FindEntities(x, y, z, 12, {"player"}, {"playerghost", "INLIMBO"}, nil)
	
	-- Apply the aura to any of the found entities within the radius, including the aura-emitting
	-- entity itself if it fits the tag-list parameters!
	for i, v in ipairs(ents) do
		onApplyAura(v, inst)
	end
end

-- END OF AURA CODE

-- You can trigger the aura application task however you want. You can hook it up to any event,
-- since the only parameter it takes is the inst of the entity applying the aura.
-- Example: inst:ListenForEvent("onattackother", applyAuraInRange)
--
-- You can also just have your entity always apply the aura to entities around them.
-- Example:
-- This calls the function, which reapplies the aura to surrounding entities, every 1.0 seconds.
-- inst:DoTaskInTime(1.0, applyAuraInRange)
--
-- Add one of the lines above, or something similar, directly in your entity's Lua file,
-- at the bottom of the fn() or master_postinit() function.


-- This is an example of triggering the aura using the "onattackother" event, serving as a sort of
-- leader-battlecry or inspirational aura.
-- Instead of directly hooking up the applyAuraInRange function to the event, we create an
-- intermediate function to make sure that the thing being attacked actually is a target with a
-- combat component and that it is not prey, so it does not trigger when attacking e.g. a rabbit.
-- This function should ALSO be added ABOVE the fn() or master_postinit() function of your prefab.
local onAttackOther = function(inst, data)
	-- data includes: target, weapon, projectile, stimuli
	
	-- If there is no target or it is no longer a valid entity or the target does not have a
	-- combat component or it is prey, then we do not want to count it as being an actual attack
	-- that triggers our combat aura.
	if not data.target or not data.target:IsValid() or data.target:HasTag("prey")
	or not data.target.components.combat then
		return
	end
	
	applyAuraInRange(inst)
end

-- All the code above should be added ABOVE the fn() or master_postinit() function of your prefab,
-- and the line below should be added at the bottom of the fn() or master_postinit() function of your prefab.
inst:ListenForEvent("onattackother", onAttackOther)

 

 

Edited by Ultroman
Better handling of dual-purpose aura in the last example.
  • Like 2
  • Thanks 1
Link to comment
Share on other sites

I've been reading your examples and looking into making some aura emitting items for a friend's project. I need a bit of advice in regards to the applierGUID. It has been awhile since I have done some coding, so the line "local applierGUID = applier.GUID" is somewhat unknown to me.

By ID, I am guessing this the ID assigned to items in game when spawned etc? If the entity's name (aka the applier) is "wyrdflask_health" for example, then the following would be:

	local wyrdflask_healthGUID = wyrdflask_health.GUID

which then leads to

	local healthAuraTaskUniqueName = "flaskHealthAura"..wyrdflask_healthGUID

etc

	local endAuraTaskUniqueName = "flaskEntireAura"..wyrdflask_healthGUID

Is this reasoning correct?

Also, if I were to remove the "..applierGUID", this would mean that only one instance of the aura is applied, rather than it being stacked if say several of these health aura emitters were placed in close proximity?

Thanks!

Link to comment
Share on other sites

Yes. That should work.

Exactly. If you apply a buff with a uniquename that is not unique, then the buff will replace an existing buff with the same uniquename (I think...or just not apply the new buff; either way, only one of the buffs will remain). You can use this to limit the amount of the same buff that can be applied on an entity. If each entity should only be able to have one instance of the buff, then you make the uniquename the same for all of them, e.g., simply "myUniqueName". If you want to make it so the same entity cannot add more than one of the same buff to the same target, BUT allow entities to have multiple instances of the same buff from different sources (appliers), then you make your uniquename "myUniqueName"..applier.GUID

Edited by Ultroman
Link to comment
Share on other sites

I just updated the OP a with an explanation of how to use unique names to adjust the amount of buffs per entity (it's in the Lua code for the template and the examples using it), and generally cleaned up some confusing and long-winded wording.

Edited by Ultroman
Link to comment
Share on other sites

On 5/12/2021 at 3:48 PM, Earthyburt said:

I think you should add the following note about X for DoPeriodicTask: "Setting the X value to zero calls the function every frame" (which I believe that is what happens).

That is true and good to know for people. I will add it. Thanks!
EDIT: I have added this tip and also did a pass cleaning up some naming discrepancies and added some more tips on periodic tasks.

Edited by Ultroman
  • Thanks 1
Link to comment
Share on other sites

1 hour ago, Ultroman said:

That is true and good to know for people. I will add it. Thanks!
EDIT: I have added this tip and also did a pass cleaning up some naming discrepancies and added some more tips on periodic tasks.

No problem, m8

Link to comment
Share on other sites

On 2/2/2022 at 10:14 PM, ScrewdriverLad said:

Okay, so if I were to implement this into a hat, it should give off a sanity aura when worn, right? How do I make sure it turns off when the hat isn't being worn?

All equipable items have "onequip" and "onunequip" events you can listen to using ListenForEvent. I can't remember if the keywords have the "on" on there, or if it's just "equip" and "unequip".

Link to comment
Share on other sites

Just wondering will this work?

function Poison(inst) if inst.components.health and inst.components.health:IsDead() or inst:HasTag("playerghost") then return end
inst.components.health:Health:DoDelta(-1) end ThePlayer:DoPeriodicTask(0.5, myFunction)

Edit: I'm so disappointed in my old self, I had to fix the command, imagine using ThePlayer in a function Lol. Plus I could always just use Start Regen if i wanted.

Edited by Boogiepop210
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
  • Create New...