Warcraft 3 documentation
vJASS & Zinc Documentation
For the latest documentation about how it works vJASS and Zinc language layers for Warcraft III, please follow these links:
Jasshelper documentation - Zinc documentation - WC3 Optimizer documentation

How to develop spells with effects over time No New Posts Jass Tutorials

Started by
Guest

0 Members and 1 Guest are viewing this topic.

How to develop spells with effects over time
on: January 14, 2011, 09:53:01 AM

How to Develop Spells with Effects over Time

By moyack - 2008
Introduction.

Ok, the purpose of this tutorial is to give a general idea about how to make spells with effects over time, with a focus in the usage of some of the new features of vJASS and as addition I will treat with some aspect related to spell stackability and optimization according to the situations. In order to follow it you MUST have some experience in JASS and hopefully vJASS.



Basic Concepts.

What are scopes and libraries?? well, this is not something easy to explain but you can understand it by seeing the examples of the JassHelper manual. Libraries - Scopes

What's a struct?? A struct is a way to "pack" several variables and functions, so they can be called as one object. This is the concept that we're going to work here more deeply.

With the new improvements made by Vexorian, structs can be set in several ways, but right now I'll start with something very easy, showing little by little more features that a struct can do.

Because an example is the best way to see how this technique works, I'll do it with a spell which deals damage over time to a unit. You can extrapolate this to other situations.



Starting the Spell Development.

Ok, we know how to start. Let's create a custom ability (in this case base it on slow), set the fields that you consider more appropriate, create a custom buff for that spell and assign it to that ability in the buff field. After this, create a new trigger, in GUI set the event to "Unit - Generic Unit Event" and then select "A unit Starts the effect of an Ability", in Conditions select an "Ability Comparison" select "ability being cast equal to <your custom ability name>". Then Select in the Edit menu "Convert to custom script" and the funny thing will start.

Ok, we get this:
Code: jass
  1. function Trig_Rabid_Bite_Conditions takes nothing returns boolean
  2.     if ( not ( GetSpellAbilityId() == 'A000' ) ) then
  3.         return false
  4.     endif
  5.     return true
  6. endfunction
  7.  
  8. function Trig_Rabid_Bite_Actions takes nothing returns nothing
  9. endfunction
  10.  
  11. //===========================================================================
  12. function InitTrig_Rabid_Bite takes nothing returns nothing
  13.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  14.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Trig_Rabid_Bite_Conditions ) )
  15.     call TriggerAddAction( gg_trg_Rabid_Bite, function Trig_Rabid_Bite_Actions )
  16. endfunction
  17.  

And we'll convert into this:
Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5. endglobals
  6.  
  7. private function Conditions takes nothing returns boolean
  8.     return GetSpellAbilityId() == SpellID
  9. endfunction
  10.  
  11. private function Actions takes nothing returns nothing
  12. endfunction
  13.  
  14. //===========================================================================
  15. function InitTrig_Rabid_Bite takes nothing returns nothing
  16.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  17.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  18.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  19. endfunction
  20.  
  21. endscope

As we can see, we've added the scope, we have shorten the functions names and we've started to apply good programming practice by setting the constant variables which will allow to a spell user customize the code (mandatory if you want to comply with the JESP standard).

With this we have part of the spell skeleton, now we need to define what information must be managed by the spell. In this specific case the ability needs to damage an enemy, so we'll need to store the unit who cast the spell and the target, therefore this preliminary info will be part of our spell struct.

Code: jass
  1. private struct Data
  2.     unit caster
  3.     unit target
  4. endstruct

Things to notice: Why private? because we don't want that other functions but the ones of the trigger can access to that struct, if you need that other functions can call that struct, then you should make it with a more appropriate name and remove the private keyword. You've noticed that I've used the name data, in this case there's no problem because this struct only has sence in this scope, so we can set them with short or very stardard names and JassHelper will do the ugly job of differentiation for us :P

Now the spell skeleton has grown and it could look in this way:

Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5. endglobals
  6.  
  7. private struct Data
  8.     unit caster
  9.     unit target
  10. endstruct
  11.  
  12. private function Conditions takes nothing returns boolean
  13.     return GetSpellAbilityId() == SpellID
  14. endfunction
  15.  
  16. private function Actions takes nothing returns nothing
  17.     local Data D = Data.create()
  18.     set D.caster = GetTriggerUnit()
  19.     set D.target = GetSpellTargetUnit()
  20.    
  21. endfunction
  22.  
  23. //===========================================================================
  24. function InitTrig_Rabid_Bite takes nothing returns nothing
  25.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  26.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  27.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  28. endfunction
  29.  
  30. endscope

Let's check the Actions function. We are calling a kind of function called Data.create() those special functions will be called from now on methods, because they're functions which only have sense inside the struct, and only can be called or used by making reference to the struct of which they belong. Now we're setting the variable components of this struct after it's created. Until now it's ok, but we can do the things better, in fact we can make this in only one line by defining a custom create method. So let's do it:

Code: jass
  1. private struct Data
  2.     unit caster
  3.     unit target
  4.  
  5.     #static# method create takes unit c, unit t returns Data
  6.         local Data D = Data.allocate() // this method is private, therefore it ONLY has sense and only can be used inside the struct.
  7.         set D.caster = c
  8.         set D.target = t
  9.         return D
  10.     endmethod
  11. endstruct

With this new struct, we can reduce the number of lines in the Action function to simply one line. There are other advantages of doing this, one important is to make it easy the debug process because you know in which methods you set the variables, where you destroy them, etc.

Static keyword??? what the hell is that??
Probably you noticed the static keyword, this word is used when we need to make a method or a component global (or independent) of the structs created. If we assign the static keyword to a component of the struct, it will behave exactly as a global variable, the difference will be in the way it can be called. In the case of methods, a static method does not depends of the struct variable, it's like a normal function.

Code: jass
  1. globals
  2.     private group G
  3. endglobals
  4.  
  5. function F takes unit u returns nothing
  6.     call GroupAddunit(G, u)
  7. endfunction
  8.  
  9. function Bla takes nothing returns nothing
  10.     set G = CreateGroup()
  11.     call F(GetTriggerUnit())
  12. endfunction
[c]
Code: jass
  1. struct Test
  2.     static group G
  3.  
  4.     static method F takes unit u returns nothing
  5.         call GroupAddunit(Test.G, u)
  6.     endmethod
  7. endstruct
  8.  
  9. function Bla takes nothing returns nothing
  10.     set Test.G = CreateGroup()
  11.     call Test.F(GetTriggerUnit())
  12. endfunction

Both codes are equivalent. So the first question to ask is: what is more convenient? My answer is: it depends. If you need that your functions could get access to any private method or component of the struct, then the static  methods and component are the way to go. I personally use this notation so I can separate the variables which can be modified by the user with the ones that shouldn't be touched. This notation is so powerful that if we want we can make totally this spell inside one struct, converting all the functions into static methods and all the globals into static components.

Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5. endglobals
  6.  
  7. private struct Data
  8.     unit caster
  9.     unit target
  10.  
  11.     static method create takes unit c, unit t returns Data // This method now will carry the responsibility of setting the variable components of the struct
  12.         local Data D = Data.allocate()
  13.         set D.caster = c
  14.         set D.target = t
  15.         return D
  16.     endmethod
  17. endstruct
  18.  
  19. private function Conditions takes nothing returns boolean
  20.     return GetSpellAbilityId() == SpellID
  21. endfunction
  22.  
  23. private function Actions takes nothing returns nothing
  24.     local Data D = Data.create(GetTriggerUnit(), GetSpellTargetUnit()) // we do all our creating process in only one line
  25.    
  26. endfunction
  27.  
  28. //===========================================================================
  29. function InitTrig_Rabid_Bite takes nothing returns nothing
  30.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  31.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  32.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  33. endfunction
  34.  
  35. endscope

With this the only pending thing to do is to do something with this stuff and add the timed effect. But before everything, let's remember what is the problem now: we need to create a periodic timer which will execute a function periodically, and this function must be able to get the information properly if the spell is casted by several units (AKA ensure the MUI of this spell). In order to achieve this, there are 2 ways that we'll discuss in detail.



Approach N° 1: Using timers and storage system to pass the struct data.

This procedure implies the usage of a timer recycler like TimerUtils and a storage system, or simply a storage system with timer recycler included. For this example I'll do this spell dependent of TimerUtils now that this system allows us recycle and attach data to timers. Note: This procedure can be adapted perfectly to other storage systems like ABC, HAIL, HSAS, Cool Coll.

Ok, let's start adding functionality to this baby. First let's add to the Actions function some stuff:

Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5.     private constant real    dt      = 0.1 //timer period
  6. endglobals
  7.  
  8. private struct Data
  9.     unit caster
  10.     unit target
  11.  
  12.     static method create takes unit c, unit t returns Data
  13.         local Data D = Data.allocate()
  14.         set D.caster = c
  15.         set D.target = t
  16.         return D
  17.     endmethod
  18. endstruct
  19.  
  20. private function Loop takes nothing returns nothing
  21.     //Our periodic stuff
  22. endfunction
  23.  
  24. private function Conditions takes nothing returns boolean
  25.     return GetSpellAbilityId() == SpellID
  26. endfunction
  27.  
  28. private function Actions takes nothing returns nothing
  29.     local Data D = Data.create(GetTriggerUnit(), GetSpellTargetUnit())
  30.     local timer t = NewTimer() //Creates a new timer...
  31.     call SetTimerData(t, integer(D)) //Attach the data to the timer
  32.     call TimerStart(t, dt, true, function Loop) // Start the created timer so it can periodically run the Loop function
  33.     set t = null // Set the local timer variable to null, we don't need ti more in this function
  34. endfunction
  35.  
  36. //===========================================================================
  37. function InitTrig_Rabid_Bite takes nothing returns nothing
  38.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  39.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  40.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  41. endfunction
  42.  
  43. endscope

Now let's put some work to the Loop function. We need that the effect in the target unit keeps on it until the buff vanishes or get removed by external sources (dispelling spells for instance), so the Looping function basically will do a check if the buff is on the unit, if so, it will deal the damage to that unit. To do that, them we need to add more variables to this spell, like the buff rawcode and the damage per second. Check the highlighted text in the next code:

Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5.     private constant integer BuffID = 'B000' //Buff Rawcode
  6.     private constant real    dt      = 0.1 //timer period
  7. endglobals
  8.  
  9. private constant function Damage takes integer level returns real
  10.     return 15. + 7. * (level - 1) //Damage proportional to the spell level so it complies with the JESP standard
  11. endfunction
  12.  
  13. private struct Data
  14.     unit caster
  15.     unit target
  16.  
  17.     static method create takes unit c, unit t returns Data
  18.         local Data D = Data.allocate()
  19.         set D.caster = c
  20.         set D.target = t
  21.         return D
  22.     endmethod
  23. endstruct
  24.  
  25. private function Loop takes nothing returns nothing
  26.     local timer t = GetExpiredTimer() // Gets the timer...
  27.     local Data D = Data(GetTimerData(t)) // Gets the struct attached to the timer...
  28.     local real Dam = Damage(GetUnitAbilityLevel(D.caster, SpellID)) // Gets the damage according to the level of the spell...
  29.     if GetUnitAbilityLevel(D.target, BuffID) > 0 then // Checks if the buff is on the target unit...
  30.         //If so, it will deal damage to the unit...
  31.         call UnitDamageTarget(D.caster, D.target, Dam * dt, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
  32.     else // There's no buff on the unit, so....
  33.         call D.destroy() // Recycle the struct for a later use...
  34.         call ReleaseTimer(t) // Release the timer, pausing it, and making it avaliable for a later use with other struct...
  35.     endif
  36.     set t = null
  37. endfunction
  38.  
  39. private function Conditions takes nothing returns boolean
  40.     return GetSpellAbilityId() == SpellID
  41. endfunction
  42.  
  43. private function Actions takes nothing returns nothing
  44.     local Data D = Data.create(GetTriggerUnit(), GetSpellTargetUnit())
  45.     local timer t = NewTimer()
  46.     call SetTimerData(t, integer(D))
  47.     call TimerStart(t, dt, true, function Loop)
  48.     set t = null
  49. endfunction
  50.  
  51. //===========================================================================
  52. function InitTrig_Rabid_Bite takes nothing returns nothing
  53.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  54.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  55.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  56. endfunction
  57.  
  58. endscope

As you can see, I'm doing like a template, I'm putting the configuration stuff in the first lines, including the constant functions, then the struct and then the looping function and finally the trigger functions. Other thing to notice is that the Damage is actually Damage per second, and therefore all the damage must be multiplied by the period of the timer in order to get an accurate value.

With this changes, we have now this spell working. But (there's always a but... :P ) what would happen if this spell uses a projectile (not instant, like one based on Acid bomb or Storm bolt)?? well, it simply won't start because it's non instant and the EVENT_PLAYER_UNIT_SPELL_EFFECT starts before the buff is set on the target unit, so the only if that will activate will be the one that destroys the recently created struct (buahhh!!! snif!!!). So in order to fix that, and ensure that those kinds of spells work with this situation we need to make some adjustments to our struct. Please check the highlighted code to see the new stuff.

Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5.     private constant integer BuffID = 'B000' //Buff Rawcode
  6.     private constant real    dt      = 0.1 //timer period
  7. endglobals
  8.  
  9. private constant function Damage takes integer level returns real
  10.     return 15. + 7. * (level - 1)
  11. endfunction
  12.  
  13. private struct Data
  14.     unit caster
  15.     unit target
  16.     boolean hasbuff = false //used to check if the buff is on the target unit...
  17.  
  18.     static method create takes unit c, unit t returns Data
  19.         local Data D = Data.allocate()
  20.         set D.caster = c
  21.         set D.target = t
  22.         return D
  23.     endmethod
  24.    
  25.     method onDestroy takes nothing returns nothing
  26.         set .hasbuff = false // this custom method will set the hasbuff variable to false, so it can start properly when the spell is casted again...
  27.     endmethod
  28. endstruct
  29.  
  30. private function Loop takes nothing returns nothing
  31.     local timer t = GetExpiredTimer()
  32.     local Data D = Data(GetTimerData(t))
  33.     local real Dam = Damage(GetUnitAbilityLevel(D.caster, SpellID))
  34.     // This conditional detects if the buff is on the target unit...
  35.     if not D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  36.         set D.hasbuff = true
  37.     endif
  38.     // If the buff is on the target unit, then do the effect
  39.     if D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  40.         call UnitDamageTarget(D.caster, D.target, Dam * dt, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
  41.     endif
  42.     // If the buff is not present anymore, then stop the spell
  43.     if D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) < 1 then
  44.         call D.destroy()
  45.         call ReleaseTimer(t)
  46.     endif
  47.     set t = null
  48. endfunction
  49.  
  50. private function Conditions takes nothing returns boolean
  51.     return GetSpellAbilityId() == SpellID
  52. endfunction
  53.  
  54. private function Actions takes nothing returns nothing
  55.     local Data D = Data.create(GetTriggerUnit(), GetSpellTargetUnit())
  56.     local timer t = NewTimer()
  57.     call SetTimerData(t, integer(D))
  58.     call TimerStart(t, dt, true, function Loop)
  59.     set t = null
  60. endfunction
  61.  
  62. //===========================================================================
  63. function InitTrig_Rabid_Bite takes nothing returns nothing
  64.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  65.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  66.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  67. endfunction
  68.  
  69. endscope

Wow!!! now this is becoming more complex :) . Things to notice:
  • In the struct you see that the hasbuff component is set to false, that's used when we need to set a value to the new struct created. This set is done when we call the allocate() private method and therefore is advisable to use it only with non handle variable types (booleans, reals, integers, strings). With handles, I suggest to set and manage them with create and destroy methods in order to control their respective creation and destruction.
  • Other thing is that we added to our struct a custom onDestroy method, this method will be executed when we call D.destroy() (This function can't have any arguments and it will generate syntax error if you set arguments to it).
  • The Loop function now does 3 verifications: Checks if the buff is present on the target unit, Checks if the hasbuff flag and the buff are present so it can do the effect and lastly it checks if the buff is gone, stopping the spell effect. With this this template can manage instant and not instant buff spells.
Stackable and not stackable effects.

Yay!!! our spell is working wonderfully.... hmmmm... actually not, there's one "problem" more to solve. What would happen if this spell is casted and 1 second later other unit cast this spell in the same unit?? well, the unit will be damaged by 2 and the worst thing is the buff duration has been extended, in other words: a stacked spell.

Sometimes the stackability is desirable and sometimes it doesn't but in the desirable situation we should balance this effect at least by detecting when the first cast should end, so one solution is to give to the spell the ability to detect if the duration has been reached, independently of the buff presence in the target unit. Here's the modification of this spell so it stacks but takes into account the duration of the spell but not necessarily the buff duration. (Note: There are other ways to balance a stackable spell, according of the effect type, this example is one way that works with the example)

Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5.     private constant integer BuffID = 'B000' //Buff Rawcode
  6.     private constant real    dt      = 0.1 //timer period
  7. endglobals
  8.  
  9. private constant function Damage takes integer level returns real
  10.     return 10. + 7. * (level - 1)
  11. endfunction
  12.  
  13. private constant function Duration takes integer level returns real
  14.     return 20. + 4. * (level - 1) // Returns the duration of this spell...
  15. endfunction
  16.  
  17. private struct Data
  18.     unit caster
  19.     unit target
  20.     boolean hasbuff = false
  21.     real counter // Yay!! a new component of this struct...
  22.  
  23.     static method create takes unit c, unit t returns Data
  24.         local Data D = Data.allocate()
  25.         set D.caster = c
  26.         set D.target = t
  27.         set D.counter = 0. // The counter component is set to 0 so it can count the time it should be active...
  28.         return D
  29.     endmethod
  30.    
  31.     method onDestroy takes nothing returns nothing
  32.         set .hasbuff = false
  33.     endmethod
  34. endstruct
  35.  
  36. private function Loop takes nothing returns nothing
  37.     local timer t = GetExpiredTimer()
  38.     local Data D = Data(GetTimerData(t))
  39.     local real Dam = Damage(GetUnitAbilityLevel(D.caster, SpellID))
  40.     local real Dur = Duration(GetUnitAbilityLevel(D.caster, SpellID)) //Gets the duration of the spell so it works properly and it can't be abusable
  41.     if not D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  42.         set D.hasbuff = true
  43.     endif
  44.     if D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  45.         call UnitDamageTarget(D.caster, D.target, Dam * dt, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
  46.         set D.counter = D.counter  + dt //Stores in the struct the elapsed time...
  47.     endif
  48.     if D.hasbuff and (D.counter > Dur or GetUnitAbilityLevel(D.target, BuffID) < 1) then // now any of those parameters will stop the spell
  49.         call D.destroy()
  50.         call ReleaseTimer(t)
  51.     endif
  52.     set t = null
  53. endfunction
  54.  
  55. private function Conditions takes nothing returns boolean
  56.     return GetSpellAbilityId() == SpellID
  57. endfunction
  58.  
  59. private function Actions takes nothing returns nothing
  60.     local Data D = Data.create(GetTriggerUnit(), GetSpellTargetUnit())
  61.     local timer t = NewTimer()
  62.     call SetTimerData(t, integer(D))
  63.     call TimerStart(t, dt, true, function Loop)
  64.     set t = null
  65. endfunction
  66.  
  67. //===========================================================================
  68. function InitTrig_Rabid_Bite takes nothing returns nothing
  69.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  70.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  71.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  72. endfunction
  73.  
  74. endscope

Now let's analyze the possibility of making this spell not stackable. In order to do this, the spell should deal the same damage over time and it should be able to detect if the target unit has the buff, and if it's the case, then update the respective struct with the new caster in order to ensure in case of the death of the unit, the bounty and/or credits for death get assigned properly to the last caster.

Here I'm going to use the usage of some static elements in order to allow the reader to check how they can be used. Let's see how the code should look:

Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5.     private constant integer BuffID = 'B000' //Buff Rawcode
  6.     private constant real    dt      = 0.1 //timer period
  7. endglobals
  8.  
  9. private constant function Damage takes integer level returns real
  10.     return 10. + 7. * (level - 1)
  11. endfunction
  12.  
  13. // the duration function is not needed anymore now that this spell is not stackable...
  14.  
  15. private struct Data
  16.     static group IsBitten //This group is used to store all the unit affected by the spell...
  17.     static integer index = 0 //This integer is used to keep a track of the size of the struct array.
  18.  
  19.     unit caster
  20.     unit target
  21.     boolean hasbuff = false
  22.     // The counter is not needed anymore because we don't want to do stackable this spell
  23.    
  24.     private static method onInit takes nothing returns nothing
  25.         set Data.IsBitten = CreateGroup() //Used to set the variable at map init
  26.     endmethod
  27.  
  28.     static method create takes unit c, unit t returns Data
  29.         local Data D = Data.allocate()
  30.         set D.caster = c
  31.         set D.target = t
  32.         call GroupAddUnit(Data.IsBitten, t) //Adds the unit to the affected units...
  33.         if integer(D) > Data.index then //updates the array size
  34.             set Data.index = integer(D)
  35.         endif
  36.         return D
  37.     endmethod
  38.    
  39.     method onDestroy takes nothing returns nothing
  40.         call GroupRemoveUnit(Data.IsBitten, .target) // Remove from the group the target unit, it's not affected anymore by the buff.
  41.         set .hasbuff = false
  42.         if integer(#this#) == Data.index then // Adjust the index size, so it doesn't search in inactive structs
  43.             set Data.index = Data.index - 1
  44.         endif
  45.     endmethod
  46.    
  47.     static method SetCaster takes unit caster, unit target returns nothing
  48.         // this method will search in all the active structs which of them has the  target unit, so it can update the caster properly...
  49.         local integer i = 0
  50.         local Data D
  51.         loop
  52.             exitwhen i > Data.index
  53.             set D = Data(i)
  54.             if D.target == target and D.hasbuff then
  55.                 set D.caster = caster
  56.                 return
  57.             endif
  58.             set i = i + 1
  59.         endloop
  60.     endmethod
  61. endstruct
  62.  
  63. private function Loop takes nothing returns nothing
  64.     local timer t = GetExpiredTimer()
  65.     local Data D = Data(GetTimerData(t))
  66.     local real Dam = Damage(GetUnitAbilityLevel(D.caster, SpellID))
  67.     if not D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  68.         set D.hasbuff = true
  69.     endif
  70.     if D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  71.         call UnitDamageTarget(D.caster, D.target, Dam * dt, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
  72.     endif
  73.     if D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) < 1 then
  74.         call D.destroy()
  75.         call ReleaseTimer(t)
  76.     endif
  77.     set t = null
  78. endfunction
  79.  
  80. private function Conditions takes nothing returns boolean
  81.     return GetSpellAbilityId() == SpellID
  82. endfunction
  83.  
  84. private function Actions takes nothing returns nothing
  85.     local Data D
  86.     local timer t
  87.     if not IsUnitInGroup(GetSpellTargetUnit(), Data.IsBitten) then
  88.         //if the target unit doesn't have the effect, then it will start a new effect...
  89.         set D = Data.create(GetTriggerUnit(), GetSpellTargetUnit())
  90.         set t = NewTimer()
  91.         call SetTimerData(t, integer(D))
  92.         call TimerStart(t, dt, true, function Loop)
  93.     else
  94.         // Otherwise, it will update the current effect with the new caster...
  95.         call Data.SetCaster(GetTriggerUnit(), GetSpellTargetUnit())
  96.     endif
  97.     set t = null
  98. endfunction
  99.  
  100. //===========================================================================
  101. function InitTrig_Rabid_Bite takes nothing returns nothing
  102.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  103.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  104.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  105. endfunction
  106.  
  107. endscope

Things to notice:
  • The Duration function and the counter parameter in the struct have been removed now that this spell is not stackable anymore.
  • We've added 2 static components: group IsBitten and integer index, as we said before, they behave as global variables, dependent of the Data struct. The purpose of the group variable is to store all the units affected by the spell and the integer stores the struct array size.
  • We have a new method: onInit. Every method created with this name is executed at map initialization. In this case we added it in order to set up the IsBitten group variable.
  • We added a custom method: SetCaster. This one will search through all the active structs and if it finds the respective target unit, then it will update the caster which deals the passive damage.
  • The Actions function has been changed so the spell can determine if it has to create or updated an existing struct.
this keyword??? integer as a function?? what's happening??
The keyword this is used ONLY in non static methods to make reference to the struct that it's applying it. This cannot be used in static methods because it has no sense in them, so don't try it!!

Notation: set this.caster = u set .caster = u

Both notations are perfectly equivalents.

the integer(Struct) function is used to return the index of the struct. It's possible to get this value directly but it's advisable to use this notation in order to ensure compatibility with later versions of JassHelper.

local integer i = integer(Struct) = local integer i = Struct

Very well, now our spell is stable and can work in the way we needed. Now let's see the second approach.



Approach N° 2: using one single timer for all the units casting the spell.

I personally love this approach, because it allows you to reduce (for not saying avoid) the usage of storage systems. This approach is based in the following precept: If the time is the same for all the units, then one timer should be able to review and control all the units affected by one spell and not one timer per spell casted as we did before.

So the first step has been defined: We can't start a new timer every time we cast the spell, instead, we need to start one timer at map init and putting it to run a code periodically so it can check units affected.

Let's do the modifications based on the non stackable version of Rabid Bite:

Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5.     private constant integer BuffID = 'B000' //Buff Rawcode
  6.     private constant real    dt      = 0.1 //timer period
  7. endglobals
  8.  
  9. private constant function Damage takes integer level returns real
  10.     return 10. + 7. * (level - 1)
  11. endfunction
  12.  
  13. private struct Data
  14.     static group IsBitten
  15.     static integer index = 0
  16.  
  17.     unit caster
  18.     unit target
  19.     boolean hasbuff = false
  20.    
  21.     private static method onInit takes nothing returns nothing
  22.         set Data.IsBitten = CreateGroup()
  23.     endmethod
  24.  
  25.     static method create takes unit c, unit t returns Data
  26.         local Data D = Data.allocate()
  27.         set D.caster = c
  28.         set D.target = t
  29.         call GroupAddUnit(Data.IsBitten, t)
  30.         if integer(D) > Data.index then
  31.             set Data.index = integer(D)
  32.         endif
  33.         return D
  34.     endmethod
  35.    
  36.     method onDestroy takes nothing returns nothing
  37.         call GroupRemoveUnit(Data.IsBitten, .target)
  38.         set .hasbuff = false
  39.         if integer(this) == Data.index then
  40.             set Data.index = Data.index - 1
  41.         endif
  42.     endmethod
  43.    
  44.     static method SetCaster takes unit caster, unit target returns nothing
  45.         local integer i = 0
  46.         local Data D
  47.         loop
  48.             exitwhen i > Data.index
  49.             set D = Data(i)
  50.             if D.target == target and D.hasbuff then
  51.                 set D.caster = caster
  52.                 return
  53.             endif
  54.             set i = i + 1
  55.         endloop
  56.     endmethod
  57. endstruct
  58.  
  59. private function Loop takes nothing returns nothing
  60.     local integer i = 0 //Used to make the loop through all the struct array
  61.     local Data D
  62.     local real Dam
  63.     loop // Looping through the struct array...
  64.         exitwhen i > Data.index
  65.         set D = Data(i)
  66.         if not D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  67.             set D.hasbuff = true
  68.         endif
  69.         if D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  70.             set Dam = Damage(GetUnitAbilityLevel(D.caster, SpellID))
  71.             call UnitDamageTarget(D.caster, D.target, Dam * dt, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
  72.         endif
  73.         if D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) < 1 then
  74.             call D.destroy()
  75.         endif
  76.         set i = i + 1
  77.     endloop
  78. endfunction
  79.  
  80. private function Conditions takes nothing returns boolean
  81.     return GetSpellAbilityId() == SpellID
  82. endfunction
  83.  
  84. private function Actions takes nothing returns nothing
  85.     // The Actions function just determines if it has to create or update an active struct...
  86.     if not IsUnitInGroup(GetSpellTargetUnit(), Data.IsBitten) then
  87.         call Data.create(GetTriggerUnit(), GetSpellTargetUnit())
  88.     else
  89.         call Data.SetCaster(GetTriggerUnit(), GetSpellTargetUnit())
  90.     endif
  91. endfunction
  92.  
  93. //===========================================================================
  94. function InitTrig_Rabid_Bite takes nothing returns nothing
  95.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  96.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  97.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  98.     call TimerStart(CreateTimer(), dt, true, function Loop) //Start the timer at map init...
  99. endfunction
  100.  
  101. endscope

As you can see, too few changes were required to get this improvement. In this case we removed the usage of CSSafety and HandleVars, which is in my opinion a big improvement. Other thing to notice is that the timer never stops, if there's no units, the loop won't run so this function practically won't put any stress in the game.


Stackacle spell using the second approach

Now... if we want to make it stackable, we just have to remove the SetCaster method, add the duration function, do the modifications in the conditionals in the Loop function and modify the Actions function to get the desired effect. Check the highlighted text and appreciate how the code changed:

Code: jass
  1. scope RabidBite
  2.  
  3. globals
  4.     private constant integer SpellID = 'A000' //Spell Rawcode.
  5.     private constant integer BuffID = 'B000' //Buff Rawcode
  6.     private constant real    dt      = 0.1 //timer period
  7. endglobals
  8.  
  9. private constant function Damage takes integer level returns real
  10.     return 10. + 7. * (level - 1)
  11. endfunction
  12.  
  13. private constant function Duration takes integer level returns real
  14.     return 20. + 4. * (level - 1) // Returns the duration of this spell...
  15. endfunction
  16.  
  17. private struct Data
  18.     static group IsBitten
  19.     static integer index = 0
  20.  
  21.     unit caster
  22.     unit target
  23.     boolean hasbuff = false
  24.     real counter
  25.    
  26.     private static method onInit takes nothing returns nothing
  27.         set Data.IsBitten = CreateGroup()
  28.     endmethod
  29.  
  30.     static method create takes unit c, unit t returns Data
  31.         local Data D = Data.allocate()
  32.         set D.caster = c
  33.         set D.target = t
  34.         set D.counter = 0. // The counter component is set to 0 so it can count the time it should be active...
  35.         call GroupAddUnit(Data.IsBitten, t)
  36.         if integer(D) > Data.index then
  37.             set Data.index = integer(D)
  38.         endif
  39.         return D
  40.     endmethod
  41.    
  42.     method onDestroy takes nothing returns nothing
  43.         call GroupRemoveUnit(Data.IsBitten, .target)
  44.         set .hasbuff = false
  45.         if integer(this) == Data.index then
  46.             set Data.index = Data.index - 1
  47.         endif
  48.     endmethod
  49.  
  50.     //The method SetCaster has been removed....
  51. endstruct
  52.  
  53. private function Loop takes nothing returns nothing
  54.     local integer i = 0
  55.     local Data D
  56.     local real Dam
  57.     local real Dur
  58.     loop
  59.         exitwhen i > Data.index
  60.         set D = Data(i)
  61.         if not D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  62.             set D.hasbuff = true
  63.         endif
  64.         if D.hasbuff and GetUnitAbilityLevel(D.target, BuffID) > 0 then
  65.             set Dam = Damage(GetUnitAbilityLevel(D.caster, SpellID))
  66.             call UnitDamageTarget(D.caster, D.target, Dam * dt, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
  67.             set D.counter = D.counter  + dt //Stores in the struct the elapsed time...
  68.         endif
  69.         set Dur = Duration(GetUnitAbilityLevel(D.caster, SpellID))
  70.         if D.hasbuff and (D.counter > Dur or GetUnitAbilityLevel(D.target, BuffID) < 1) then
  71.             call D.destroy()
  72.         endif
  73.         set i = i + 1
  74.     endloop
  75. endfunction
  76.  
  77. private function Conditions takes nothing returns boolean
  78.     return GetSpellAbilityId() == SpellID
  79. endfunction
  80.  
  81. private function Actions takes nothing returns nothing
  82.     call Data.create(GetTriggerUnit(), GetSpellTargetUnit()) // Just one line of code!!!
  83. endfunction
  84.  
  85. //===========================================================================
  86. function InitTrig_Rabid_Bite takes nothing returns nothing
  87.     set gg_trg_Rabid_Bite = CreateTrigger(  )
  88.     call TriggerAddCondition( gg_trg_Rabid_Bite, Condition( function Conditions ) )
  89.     call TriggerAddAction( gg_trg_Rabid_Bite, function Actions )
  90.     call TimerStart(CreateTimer(), dt, true, function Loop)
  91. endfunction
  92.  
  93. endscope



When is better one approach than other??

Unconsciously, this example has evolved from the approach 1 to the approach 2, but it's important to point out that it doesn't mean that one approach is worst than the other, actually it depends in how is used and with which frequency.

For example, if we have an AoS sytle map, which is generally hero based, the first approach is more convenient because you don't know if the hero with the custom spells will be summoned and therefore the chances that this spell can be casted are less. In the other hand, with spells that can be casted by several hundreds of units (like in custom melee games of footies) the second approach is more convenient, because instead of having several timers controlling a spell (one timer per unit, and imagine a footies game with full house and the footmen casting those custom spells), this will be a considerable memory eater. With one timer checking all the units, we can optimize it pretty fine.




Final Words.

Well, what we did in this tutorial was creating a spell, modify it according to the required circumstances and at the end we ended doing something very interesting: a template, a very nice template. That's something good, because it allows us develop several kind of spells with small modifications of one pattern. The template is basically in this way:

Code: jass
  1. Scope My spell
  2. // Customization section
  3. globals
  4.     //Constant variables...
  5. endglobals
  6.  
  7. < Constant functions... >
  8.  
  9. // End customization section
  10.  
  11. private struct Data
  12.      // Struct components
  13. endstruct
  14.  
  15. < Spell functions required for the looping function>
  16.  
  17. < Looping function >
  18.  
  19. < Triggers functions >
  20.  
  21. endscope

Well, I think this is all. I hope this tutorial helps you to improve your the spell development. Any questions, typos, mistakes or suggestions about how to make this tutorial better can be post here. Happy spell making :)

==========================================================================

Addition: Looping throught units.

As a part of development of spells controlled with a single timer, we need to iterate through an array of data. this can be done in several way, ones are less efficients than others. If we have units as a part of the data struct, we can use them to develop a very safe way of iteration using the ForGroup command. An example can help us right now.

Code: jass
  1. // ================================================================= \\
  2. // Custom Immolation modifier spell, so it targets destructables too \\
  3. // Request by Abriko, by moyack. 2008.                               \\
  4. // ================================================================= \\
  5. // Requires Table to work...                                         \\
  6. // ================================================================= \\
  7. scope Immolation2 initializer init
  8.  
  9. // Configuration Part...
  10. globals
  11.     private constant integer SpellID = 'AEi2' //Spell based on Immolation
  12.     private constant integer BuffID = 'BEim' //Immolation buff, please base it on the immolation buff.
  13.     private constant real dt = 1.
  14. endglobals
  15.  
  16. private constant function DamageRate takes integer level returns real
  17.     return 10. + 5. * (level - 1)
  18. endfunction
  19.  
  20. private constant function AOE takes integer level returns real
  21.     return 160.
  22. endfunction
  23. // End configuration Part...
  24.  
  25. private struct data
  26.     static HandleTable T
  27.     static group G
  28.     static rect R
  29.     static unit U
  30.  
  31.     unit c
  32.     boolean flag = false
  33.    
  34.     static method Start takes unit c returns nothing
  35.         local data D = data.allocate()
  36.         set D.c = c
  37.         call GroupAddUnit(data.G, c)
  38.         set data.T[c] = integer(D)
  39.     endmethod
  40.    
  41.     method onDestroy takes nothing returns nothing
  42.         call GroupRemoveUnit(data.G, .c)
  43.         call data.T.flush(.c)
  44.     endmethod
  45. endstruct
  46.  
  47. private function GetLivingDestructables takes nothing returns boolean
  48. endfunction
  49.  
  50. private function BurnDestructables takes nothing returns nothing
  51.     call UnitDamageTarget(data.U, d, DamageRate(GetUnitAbilityLevel(data.U, SpellID)) * dt, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
  52.     set d = null
  53. endfunction
  54.  
  55. private function CheckStatus takes nothing returns nothing
  56.     local unit u = GetEnumUnit()
  57.     local data D = data( data.T[u] )
  58.     if not D.flag and GetUnitAbilityLevel(u, BuffID) > 0 then
  59.         set D.flag = true
  60.     endif
  61.     if D.flag and GetUnitAbilityLevel(u, BuffID) > 0 then
  62.         call SetRect(data.R, GetUnitX(u) - AOE(GetUnitAbilityLevel(u, SpellID)), GetUnitY(u) - AOE(GetUnitAbilityLevel(u, SpellID)), GetUnitX(u) + AOE(GetUnitAbilityLevel(u, SpellID)), GetUnitY(u) + AOE(GetUnitAbilityLevel(u, SpellID)))
  63.         set data.U = u
  64.         call EnumDestructablesInRect(data.R, Condition(function GetLivingDestructables), function BurnDestructables)
  65.     endif
  66.     if D.flag and GetUnitAbilityLevel(u, BuffID) < 1 then
  67.         call D.destroy()
  68.     endif
  69.     set u = null
  70. endfunction
  71.  
  72. private function Loop takes nothing returns nothing
  73.     call ForGroup(data.G, function CheckStatus)
  74. endfunction
  75.  
  76. private function Conditions takes nothing returns boolean
  77.     return GetSpellAbilityId() == SpellID
  78. endfunction
  79.  
  80. private function Actions takes nothing returns nothing
  81.     if not IsUnitInGroup(GetTriggerUnit(), data.G) then
  82.         call data.Start(GetTriggerUnit())
  83.     endif
  84. endfunction
  85.  
  86. //===========================================================================
  87. private function init takes nothing returns nothing
  88.     local trigger t = CreateTrigger(  )
  89.     call TriggerAddCondition( t, Condition( function Conditions ) )
  90.     call TriggerAddAction( t, function Actions )
  91.     set t = null
  92.     set data.T = HandleTable.create()
  93.     set data.G = CreateGroup()
  94.     set data.R = Rect(0,0,1,1)
  95.     call TimerStart(CreateTimer(), dt, true, function Loop)
  96. endfunction
  97.  
  98. endscope

What I'm doing in this spell:

  • I use a group to store all the units that cast the spell. Those units will be my reference or index key.
  • I use a HandleTable to store the struct data ID related to the unit.
  • To iterate though all the active struct data, I call the ForGroup command and in the enumerating function I retrieve the data struct related to the unit with the command data.
The advantages of this iterating process:

  • You can have total control of the structs.
  • Therefore, you'll have control over all the struct in a safe way.
  • Data search is practically O(1) thanks to the usage of table (gamecache search property)
Disadvantage:

  • Only can be used with handles which support grouper handles (units > groups, player > force)
This procedure is very useful in spells because almost in all the cases (for not saying all the cases) there's a unit involved in the spell process, and that unit can serve us as a index for the spell itself.
« Last Edit: December 02, 2012, 12:10:16 PM by moyack »



Re: How to develop spells with effects over time
Reply #1 on: December 29, 2011, 10:24:35 PM

This tutorial looks pretty cool :)
Is it going to be updated to make use of the new Jass tags? ;P



Re: How to develop spells with effects over time
Reply #2 on: December 30, 2011, 10:00:46 AM

This tutorial looks pretty cool :)
Is it going to be updated to make use of the new Jass tags? ;P
Definitely, and not only that, I'll update to the new coding standards to make it up to date.


Re: How to develop spells with effects over time
Reply #3 on: December 30, 2011, 05:24:46 PM

Tutorial converted, now I have to update to the current coding style


Re: How to develop spells with effects over time
Reply #4 on: December 31, 2011, 06:57:15 AM

And to do that, you have to make the struct extend an array, use a library, use struct/module initializers (they look cooler :D)