wc3campaigns
WC3C Homepage - www.wc3c.netUser Control Panel (Requires Log-In)Engage in discussions with other users and join contests in the WC3C forums!Read one of our many tutorials, ranging in difficulty from beginner to advanced!Show off your artistic talents in the WC3C Gallery!Download quality models, textures, spells (vJASS/JASS), systems, and scripts!Download maps that have passed through our rigorous approval process!

Go Back   Wc3C.net > Tutorials > JASS/AI scripts tutorials
User Name
Password
Register Rules Get Hosted! Chat Pastebin FAQ and Rules Members List Calendar



Reply
 
Thread Tools Search this Thread
Old 05-18-2006, 03:59 PM   #1
Blade.dk
.
 
Blade.dk's Avatar


Respected User
 
Join Date: May 2005
Posts: 1,990

Submissions (15)

Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)

Approved Map: Azeroth's Arcane ArenaSpell session 01 winner

Send a message via MSN to Blade.dk
Default Spell Making Course: Part 1: Making a simple stomp spell.

Table of Contents
Introduction
Types and typecasting
Gamecache
Creating the spell itself
Making the looping function
Improving the spell
Making the spell follow the JESP Standard
Final notes


Introduction

The purpose of this tutorial is to teach people how to make a simple stomp spell in JASS.

That is, however, not the only thing you will learn. The stomp spell is mainly used as an example, the tutorial should teach you some general things about JASS and spell making, which is more important.

This tutorial is NOT an introduction or starting course to JASS. It is highly recommended (more accurately, required) that you know the basics of JASS before you start on this.
If you know a programming language already, then I suggest reading The JASS Manual. Else Vexorian's tutorials, Introduction to JASS and Triggers in JASS, should be a fine start.

This is the first part of a series of tutorials covering different areas in spell-making that I plan to make - A so-called spell making course.

All you need to follow this tutorial, is the WE and some JASS knowledge (take a look at the link listed above if you don't know anything about JASS).

I recommend you to use an editor like JassCraft, as it helps you look up function names and easily check the syntax of your code. Even though the code can be restored, I don't recommend coding spells in the WE, because a small error is enough to make the program crash on saving.


Types and typecasting

So, let's get started.

You probably already know some of this now, but I will go over it anyway, to prevent confusion.

The following types exists in JASS.

integer - Numbers without decimals. For a more detailed description, go here.

real - Numbers with decimals. Examples: 0.2, 0.54654675. The number does not have to have decimals unless it is a return value in a function, so in most cases you could use 1, 35465, and -340 as reals too.

boolean - A boolean can be either true or false.

string - Text between quotes. Examples "hi", "hello".

code - function pointers. Example: call TimerStart(myTimer, 0.05, true, function myFunction). The last argument, function myFunction, is of the type code.

handle - A handle is an object. All types that are not integer, real, boolean, string or code are derived from the type handle.

For example, the type timer is a child of the type handle. The type handle is the parent of the type timer.

The type widget is extends the handle type. It is a child of the handle type, but it is also the parent of several other types - unit, destructable and item.

Whenever a function takes an argument, you can pass a value of the type as the argument and or of any of it's child types to the function.

This means, that if a function takes a widget argument, you can give it both a widget, an item, a unit or a destructable, because the destructable, item and unit types all are childs of the widget type.

If you have a variable, you can store both objects of the type of the variable and of it's child types in it.

This means that you, for example, can save a both a timer and a unit in a variable of the type handle.

But what if you have a variable of the type handle that you want to use with a function that takes a timer argument, for example?

Even though you know that the handle is a timer, the game does not and will give you an error if you use it.

To 'convert' the handle to a timer that the game will recognize, we will have to use a function like this:

Collapse JASS:
function MyFunction takes handle h returns timer
    return h
endfunction

If h, however, is not actually a timer, the function that you use it will act like you gave it the value 'null' - no timer.

Blizzard already uses functions like that, an example would be this:

Collapse JASS:
function GetDyingDestructable takes nothing returns destructable
    return GetTriggerWidget()
endfunction

Ok, so now we know how the handle type works and how we can 'convert' it and it's sub-types.
You should also know how the basic Blizzard conversion function works, such as S2I (string to integer), S2R (string to real), I2S (integer to string), I2R (integer to real) R2I (real to integer), R2S (real to string) and R2SW (real to formatted string).

But what if we want to convert a handle type to an integer, for example? This is very often used in JASS together with gamecache to create multinstanceable code or 'databases'.

It is actually very simple. The method is called 'the return bug' because it takes advantage of a bug in the game and the editor.

The editor and the game only checks if the last value returned by a function is of the correct type (a function can only return one value, after that it exits. But it is possible to have multiple return lines in a function, which is often used for functions that uses if statements).

So to make a handle to integer, H2I, conversion possible, all we add is one more return statement. Like this:

Collapse JASS:
function H2I takes handle h returns integer
    return h
    return 0
endfunction

This function (which probably is the most well-known JASS function not written by Blizzard) takes a handle value, h, and returns an integer value.
As you can see, the first line in it returns h. The second returns 0.

The first return statement will return 'h' as an integer id that only that particular handle uses. The second line will never be executed, it is just there so the function will work instead of giving syntax errors.

You can, of course, also convert the opposite way, so something like this:

Collapse JASS:
function I2H takes integer i returns handle
    return i
    return null
endfunction

Is fine as well. Notice that the last return statement returns null instead of 0 as in the other function - Because null is the value for the handle type that means 0, nothing. Actually the return bug exploiting functions doesn't use this last value at all, so you could have placed GetTriggerUnit() there as well. The value null just makes more sense, and doesn't require you to call another function.

Now let's create a few return bug exploiters and place them in the custom script section (the thing at top of the triggers list with the map's name on it) of the map.

Collapse JASS:
function H2I takes handle h returns integer
    return h
    return 0
endfunction

function I2G takes integer i returns group
    return i
    return null
endfunction

This should be all we need for this spell. Notice the I2G (integer to group) function, instead of converting to handle, I converts it directly to group.


Gamecache

Even though the gamecache type originally was designed to transfer data between campaign missions, it can be used for a lot more. Mainly 2-dimensional arrays and databases.

Before we continue, I would like to state this clearly so there won't be any misunderstandings.

Gamecache does NOT work in multiplayer games if you want to save content to it that another map is supposed to load later, or if you want to restore content from a gamecache from a previous game.

Gamecache does, however, WORK perfectly fine in ALL games, including multiplayer, as long as the data you store and load there is only to be used in that single game.
This means that there are absolutely NO problems with using it as a 2 dimensional array or a database in a multiplayer game, it will work fine.

I also recommend you to read this post, which explains some bugs in the gamecache type.

Let's get started doing some actual work now. Create a gamecache variable in the variable editor in the trigger editor.
I'll simply call it "AbilityCache" ("udg_AbilityCache" in JASS), as we are going to use it to store data of custom abilities in.

Create a new trigger in the GUI with the name InitCache, and convert it to JASS.

After you convert it, it will look like this:

Collapse JASS:
function Trig_InitCache_Actions takes nothing returns nothing
endfunction

//===========================================================================
function InitTrig_InitCache takes nothing returns nothing
    set gg_trg_InitCache = CreateTrigger(  )
    call TriggerAddAction( gg_trg_InitCache, function Trig_InitCache_Actions )
endfunction

Most of that is unneeded, as we will only use it to initialize the gamecache. Remove everything outside and inside the InitTrig_InitCache function, except the function and endfunction lines.
It should look like this now:

Collapse JASS:
function InitTrig_InitCache takes nothing returns nothing
endfunction

Now we will add a few lines of code inside the InitTrig_InitCache function, to initialize the gamecache.
First we will add a line that initializes and instantly flushes the gamecache, in case the cache was saved during another game. By doing this, we clear all data in the cache, so data from old games won't clash with data from the current game and cause problems.

The line that initializes and flushes the cache should look like this:

Collapse JASS:
    call FlushGameCache(InitGameCache("abilitycache.w3v"))

I use "abilitycache.w3v" as the gamecache filename here.

Now to add another line, the line that actually initializes the game cache:

Collapse JASS:
    set udg_AbilityCache = InitGameCache("abilitycache.w3v")

The InitCache JASS trigger should look like this now:

Collapse JASS:
function InitTrig_InitCache takes nothing returns nothing
    call FlushGameCache(InitGameCache("abilitycache.w3v"))
    set udg_AbilityCache = InitGameCache("abilitycache.w3v")
endfunction

Everything else like using the cache will be explained later in this tutorial.


Creating the spell itself

Now to the hardest part: Creating the spell itself.

We could base it on the War Stomp spell. That would be pretty sensible, and save us some code, but for the sake of learning, we won't do that.
Base it on something else, for example Channel, or base it on War Stomp and set all targets, art, aoe, damage and effect fields to nothing.

Create a simple GUI trigger like this:

Trigger:
Stomp
Collapse Events
Unit - A unit Starts the effect of an ability
Collapse Conditions
(Ability being cast) Equal to Stomp
Actions

As event we use "A unit Starts the effect of an ability". Below will come a short explanation of the normal spell cast events (spell channel events not included), that should teach you the difference.

Table:
Unit - A unit Begins casting an abilityThis event makes the trigger fire just before the spell is cast. This means that it can be used for special condition checks (like if the spell is within a minimum range or so), and should only be used for that. If you use it to detect when a spell is actually cast, quick players will be able to cheat and make the spell trigger fire without starting the spell's cooldown and making the unit lose mana.
Unit - A unit Starts the effect of an abilityThis event makes the trigger fire when the spell is cast, cooldown starts and mana is taken. Therefore it is the ideal event in this case.
Unit - A unit Finishes casting an abilityThis event makes the trigger fire when the unit has finished casting the spell. This is useful if you for example want to remove a unit when it casts a certain spell, and you want to be sure that the spells effect will appear. For example, if you want to remove a unit casting Heal, you should use this event, else the target won't be healed correctly.

Convert the trigger to JASS. It should look like this now:

Collapse JASS:
function Trig_Stomp_Conditions takes nothing returns boolean
    if ( not ( GetSpellAbilityId() == 'A000' ) ) then
        return false
    endif
    return true
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
endfunction

//===========================================================================
function InitTrig_Stomp takes nothing returns nothing
    set gg_trg_Stomp = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Stomp, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( gg_trg_Stomp, Condition( function Trig_Stomp_Conditions ) )
    call TriggerAddAction( gg_trg_Stomp, function Trig_Stomp_Actions )
endfunction


The 'A000' might be something else if you are making this in a map where you already have custom spells. It is the rawcode of the spell, a code unique to each spell in the map.
The simplest way to find a spell's rawcode, is to select it in the object editor and press CTRL+D. The first four letters of the spell's name is the rawcode (case sensitive, must be put within single quotes) the next four letters (only appears on custom spells) is the rawcode of the spell it was based on. Lastly the name of the spell is, inside a parenthesis.

Most of this trigger is fine, and it took shorter time to set it up in the GUI than it did in JASS. There is, however, something that we will change:

Collapse JASS:
function Trig_Stomp_Conditions takes nothing returns boolean
    if ( not ( GetSpellAbilityId() == 'A000' ) ) then
        return false
    endif
    return true
endfunction

This part, the condition, is ridiculously coded. Replace it with this, much simpler and much smaller, verion:

Collapse JASS:
function Trig_Stomp_Conditions takes nothing returns boolean
    return GetSpellAbilityId() == 'A000'
endfunction

We only plan to use one special effect model for the spell for now: The normal War Stomp model.
The path of that model is: Abilities\Spells\Orc\WarStomp\WarStompCaster.mdl

When you want to use a path in JASS, you must first put it between quotes, as it is a string. You'll also have to replace every single backslash (\) with two backslashes instead (\\). It will only show up one, the first backslash is used for control.
Just one single backslash will cause crashes when saving. A single backslash is only to be used if you want to use the " symbol in JASS strings.

Example: "Bob \"Boogieman\" Johnson" will show up like this in the game:

Bob "Boogieman" Johnson

To prevent lag the first time our spell is cast, we will preload the effect model. We do that with the 'Preload' native.
So preload the effect in the InitTrig function, so it looks like this:

Collapse JASS:
function InitTrig_Stomp takes nothing returns nothing
    set gg_trg_Stomp = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Stomp, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( gg_trg_Stomp, Condition( function Trig_Stomp_Conditions ) )
    call TriggerAddAction( gg_trg_Stomp, function Trig_Stomp_Actions )
    call Preload("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl")
endfunction

So let's begin coding the spell itself now. Everything we do now will be done in the Trig_Stomp_Actions function.

You should know about local variables already, so I won't explain that again.

First we will store the caster, the ability level and the x and y positions of him/her/it in local variables.

Collapse JASS:
function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
...

Now let's add a new function, to be used in the filter that will be used to find the units that the spell can affect.

Collapse JASS:
function Stomp_Filter takes nothing returns boolean
    return IsPlayerEnemy(GetOwningPlayer(GetTriggerUnit()), GetOwningPlayer(GetFilterUnit())) and GetWidgetLife(GetFilterUnit()) > 0.405 and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction

A function must follow some rules to be used as a filter: It must not take any arguments, and it must return a boolean.

This filter accepts units that are enemies of the player, have more than 0.405 life and aren't flying.
The reason we check if the unit's life is greater than 0.405 and not just 0, is that units actually die when they have 0.405 life or below, instead of 0, as many people think.
The reason we check if the unit is not flying instead of checking if it is ground, is so the spell also will affect hovering units.
IsUnitType is bugged when used in boolexpr filters in JASS, you can read more about the bug here. The bug shouldn't be a problem in our filter.

Now add the function that we will use as filter to the script, above the Trig_Stomp_Actions function.
Now we use the Condition native to create a filter based on the function and save it in a boolexpr (boolean expression) variable.
We will also create a new unit group in the function.

Collapse JASS:
function Stomp_Filter takes nothing returns boolean
    return IsPlayerEnemy(GetOwningPlayer(GetTriggerUnit()), GetOwningPlayer(GetFilterUnit())) and GetWidgetLife(GetFilterUnit()) > 0.405 and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
...

The line where the boolexpr variable is declared also shows how to use function pointers - Notice that the function you refer to must be above the function where you refer to it in.
I use the Condition() native here, but the Filter() native could also be used, as it does the same (Note: They don't do exactly the same. The Condition() native returns a conditionfunc, and the Filter() native returns a filterfunc. But both conditionfunc and filterfunc are children of the boolexpr type, and all the GroupEnumUnits* natives takes a boolexpr argument. Therefore both are valid, so it does not matter which one we use).

Now we will have to create the special effect, pick the units that shall be damaged and deal the damage to them.
There are two ways that we can use to damage all units in a group: the ForGroup() native that uses another function and therefore will force us to repeat calling the same functions multiple times.
The other method is to create a copy of the group, and then loop through it in the main function, picking the first unit in the copied group, doing whatever we want to do with the units on the picked unit. Then we remove the picked unit from the copied group, so the loop won't run forever. Lastly we destroy the copied group, as we won't use it anymore and it would leak if not destroyed.

The last method is best in this case, so we will use that. To do that, we will need a function that copies the group for us.

Collapse JASS:
function CopyGroup takes group g returns group
    set bj_groupAddGroupDest = CreateGroup()
    call ForGroup(g, function GroupAddGroupEnum)
    return bj_groupAddGroupDest
endfunction

What this function does, is create a new group in the bj_groupAddGroupDest variable (from Blizzard.j) and then use the ForGroup native on the group, executing the GroupAddGroupEnum function for each unit in the group.
That function is also from Blizzard.j, and adds all units it is used with to the bj_groupAddGroupDest group.
So now we have a new group with the same units as the original, which we return.

Collapse JASS:
function Stomp_Filter takes nothing returns boolean
    return IsPlayerEnemy(GetOwningPlayer(GetTriggerUnit()), GetOwningPlayer(GetFilterUnit())) and GetWidgetLife(GetFilterUnit()) > 0.405 and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction

function Stomp_CopyGroup takes group g returns group
    set bj_groupAddGroupDest = CreateGroup()
    call ForGroup(g, function GroupAddGroupEnum)
    return bj_groupAddGroupDest
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
...

First of all, I create and destroy the special effect at the position of the caster. When you destroy an effect, the death animation (if any) is played.
If the model only has one animation (like this model), then it will continue playing that animation and disappear when it is finished.
Special effects needs to be destroyed to prevent leaks, and as it will just play the same animation anyways, I destroy this effect instantly.

Then I use the GroupEnumUnitsInRange() to add all units inside a range of 100+50*i (i is the level of the spell) to the group.

Then I copy the group to the group variable called 'n'.
Notice that I renamed the CopyGroup function to Stomp_CopyGroup, to avoid problems if you have it somewhere else in the script.
You could also just let the name of the function stay, and place it in the custom script section instead.

Now I use the unit variable 'f' and the FirstOfGroup() native to loop through the group.
What my script does, is set f to the first unit in the start of the group, and if f is no unit (null), then stop the loop.
Else damage the unit, and remove it from the copied group (so it won't be damaged multiple times and the loop won't last forever).

I use the UnitDamageTarget native to deal 25*i (level) damage, using the ATTACK_TYPE_NORMAL (called Spell in the GUI) attack type and the DAMAGE_TYPE_MAGIC damage type. I use null for the weapon type, as this spell would be weird with a weapon type (the weapon type is only used to play a sound when dealing damage, and that is not needed here).

Now it is time to start working with gamecache and a timer.

Gamecache natives always takes some of the same arguments: The cache itself, the 'category' string and the 'label' string. The two strings are what we use to use the gamecache as a two-dimensional array.

So let's add some more local variables to our function now:

Collapse JASS:
function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
...

The following variables have been added here:

gc - This is just a variable that I store the gamecache in, to save me time when typing.
t - This is a new timer that we create. It is the timer that will move the units.
s - This is the unique integer id of the timer that H2I gets us, converted to a string. This is what we will use for the 'category' string when using the gamecache. Because the integer id is unique to this timer, so is the string. That is why the spell will be multiinstanceable.

Now it is time to store the values we need in the looping function in the gamecache under the 's' label - This is often called 'attaching' the values to the handle, even though all we do is storing them in the gamecache under the handle's unique id-string.

So, let's do it:

Collapse JASS:
function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
    call StoreInteger(gc, s, "level", i)
    call StoreInteger(gc, s, "group", H2I(g))
    call StoreReal(gc, s, "x", x)
    call StoreReal(gc, s, "y", y)
...

So we have now added four lines.

The first line directly saves the level of the spell in the gamecache.
The next line saves the pointer, the unique id of the group in the gamecache by using H2I. We can use the id later to get the group.
The last two lines added saves respectively the x and y coordinate of the unit in the cache, so we know the starting point of the spell.

Now, let us start the timer. We add a new, empty functon called "Stomp_Move" just above the "Trig_Stomp_Actions" function, and a TimerStart call in our function.

Collapse JASS:
function Stomp_CopyGroup takes group g returns group
    set bj_groupAddGroupDest = CreateGroup()
    call ForGroup(g, function GroupAddGroupEnum)
    return bj_groupAddGroupDest
endfunction

function Stomp_Move takes nothing returns nothing
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
    call StoreInteger(gc, s, "level", i)
    call StoreInteger(gc, s, "group", H2I(g))
    call StoreReal(gc, s, "x", x)
    call StoreReal(gc, s, "y", y)
    call TimerStart(t, 0.05, true, function Stomp_Move)
...

As you can see, we added the Stomp_Move function about the Trig_Stomp_Actions function (it has to be above to be referred to) and below
the "Stomp_CopyGroup" function, as "Stomp_Move" will be using "Stomp_CopyGroup".

The timer now expires every 0.05 seconds. The most common timer expiration intervals for spells are 0.05 and 0.04.
0.04 looks better, but is also more likely to create lag, so we won't use this now, as the spell can move many units.
You can always make the value higher for a less laggy spell, but not as smooth movement, or lower for a more smooth movement, but more memory-consuming spell.

Now only one thing is left that we need to do in the main function: Clean up what we won't use anymore, to avoid leaks.

Collapse JASS:
function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
    call StoreInteger(gc, s, "level", i)
    call StoreInteger(gc, s, "group", H2I(g))
    call StoreReal(gc, s, "x", x)
    call StoreReal(gc, s, "y", y)
    call TimerStart(t, 0.05, true, function Stomp_Move)
    set c = null
    call DestroyBoolExpr(b)
    set b = null
    set g = null
    call DestroyGroup(n)
    set n = null
    set f = null
    set gc = null
    set t = null
endfunction

Notice that we don't destroy the group saved in the 'g' variable, as the looping function is using the group.

We set 't' to null, to avoid a small leak. In very rare cases, this can cause problems. If it happens, just remove the line that sets t to null. You can read more about that bug here.


Making the looping function

Now the main function that deals the damage and set up the timer has been made, and we are going to the next step: making the looping function.

We will start reading the values from the gamecache that we saved in the other function:

Collapse JASS:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
...

You should notice some things here.
First of all, we don't store the expired timer in a variable, we just use it directly as we (except for the single time the timer runs and will clean up itself) only will have to use it once each time the function runs.
We don't store the group either, for the same reasons. We take it directly from the cache and copies it to the variable 'g'.

A very common mistake in JASS spells like this is loading the original group from the cache and then modifying it. People thinks that next time the timer runs, it will still load the same group, with the same unit, even though they remove units and destroy the group. But it won't.
For example, if you kill a unit attached to a timer, the unit will still be dead next time the timer expires. It is the same with groups and all other objects.

The spell will push units back for a certain duration, so to keep track of how long the spell has run, we will add another variable.

Collapse JASS:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    if dur < 1+0.5*i then
    else
    endif
...

The real variable called 'dur' has been added. It loads the real saved as 'dur' on the timer and adds +0.05 (the expiration interval of the timer).

If you load a value from a place in a gamecache where nothing is saved, it will always return 0/0.0/"" or null, depending on the type.

This spell will push units away for 1+0.5*i ('i' is the level of the spell) seconds, so we add an if/then/else block.

So let us add the part that moves the units affected by the spell. We will need a few more variables for that: real ux, real uy, real a, unit f.

Collapse JASS:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+40*Cos(a), uy+40*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
    else
    endif
...

Like in the main function, we loop through the group here by using FirstOfGroup().
First we store the x and y coordinates of the unit.
Then we calculate the angle (in radians) between the center of the spell and the position of the unit by using Atan2.

I won't go into much detail here about how Atan2 works, but I will tell you how to use it.
Simply use Atan2(otherPointY-centerPointY, otherPointX-centerPointX) to get the angle (in radians) from centerPoint to otherPoint.

The spell has to move the unit. There are two different (good) ways of moving a unit:

Table:
SetUnitPositionThis native moves the unit to the X and Y coordinates. While being moved, the unit can't move, and channeling spells and the like are stopped. This native does not need any extra checks, it is completely safe.
SetUnitX/YSetUnitX and SetUnitY are natives that also changes the X or Y position of the unit. The unit will, however, not stop moving channeling spells and so while being moved. These natives are faster than SetUnitPosition, but if you use a coordinate outside the map bounds, the game will crash.

I use SetUnitPosition here to move the unit 40 units every time the timer expires, that means a speed of 40*100*0.05 = 800. It is simpler as it does not require extra checks, but the main reason is, that units can't move while the timer is pushing them back with this, and eventual spells they are channeling will stop. Therefore it fits better for this spell.

I also store the 'dur' variable that is increased each time the timer expires in the gamecache again, else the spell would last forever.

When the spell has lasted for a duration, it shall end. So let us add the code that cleans up the cache, destroys the group, and stops the timer.

Collapse JASS:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+40*Cos(a), uy+40*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
    else
        call DestroyGroup(I2G(GetStoredInteger(gc, s, "group")))
        call FlushStoredMission(gc, s)
        call DestroyTimer(GetExpiredTimer())
    endif
...

First we destroy the unit group, that we saved in the cache.

Then we flush everything in the 's' category in the gamecache. This clears all the data we 'attached' to the timer, so it won't use memory or conflict with other spells later.

Lastly we destroy the expired timer.

Now all that is left of this function, is leak cleanup. Let's add it:

Collapse JASS:
function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+40*Cos(a), uy+40*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
    else
        call DestroyGroup(I2G(GetStoredInteger(gc, s, "group")))
        call FlushStoredMission(gc, s)
        call DestroyTimer(GetExpiredTimer())
    endif
    set gc = null
    call DestroyGroup(g)
    set g = null
    set f = null
endfunction

That's it! Now you have made your own Stomp spell!

Here is the full code of the spell trigger:

Collapse JASS:
function Trig_Stomp_Conditions takes nothing returns boolean
    return GetSpellAbilityId() == 'A000'
endfunction

function Stomp_Filter takes nothing returns boolean
    return IsPlayerEnemy(GetOwningPlayer(GetTriggerUnit()), GetOwningPlayer(GetFilterUnit())) and GetWidgetLife(GetFilterUnit()) > 0.405 and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction

function Stomp_CopyGroup takes group g returns group
    set bj_groupAddGroupDest = CreateGroup()
    call ForGroup(g, function GroupAddGroupEnum)
    return bj_groupAddGroupDest
endfunction

function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+40*Cos(a), uy+40*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
    else
        call DestroyGroup(I2G(GetStoredInteger(gc, s, "group")))
        call FlushStoredMission(gc, s)
        call DestroyTimer(GetExpiredTimer())
    endif
    set gc = null
    call DestroyGroup(g)
    set g = null
    set f = null
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
    call StoreInteger(gc, s, "level", i)
    call StoreInteger(gc, s, "group", H2I(g))
    call StoreReal(gc, s, "x", x)
    call StoreReal(gc, s, "y", y)
    call TimerStart(t, 0.05, true, function Stomp_Move)
    set c = null
    call DestroyBoolExpr(b)
    set b = null
    set g = null
    call DestroyGroup(n)
    set n = null
    set f = null
    set gc = null
    set t = null
endfunction

//===========================================================================
function InitTrig_Stomp takes nothing returns nothing
    set gg_trg_Stomp = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Stomp, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( gg_trg_Stomp, Condition( function Trig_Stomp_Conditions ) )
    call TriggerAddAction( gg_trg_Stomp, function Trig_Stomp_Actions )
    call Preload("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl")
endfunction


Improving the spell

More realistic movement

Now the base spell is finished, so let us try to add some tasty extra effects.

Right now it does not look very realistic that units move at the same speed all the time, so we will make it so the pushback seems more powerful in the start and then slows down, as it would do if it was real.

So let's start by attaching the initial speed as a real variable to the timer.

Collapse JASS:
...
    call StoreInteger(gc, s, "level", i)
    call StoreInteger(gc, s, "group", H2I(g))
    call StoreReal(gc, s, "x", x)
    call StoreReal(gc, s, "y", y)
    call StoreReal(gc, s, "speed", 50)
    call TimerStart(t, 0.05, true, function Stomp_Move)
...

So the initial speed is now 50. Now let's go to the "Stomp_Move" function and change that:

Collapse JASS:
...
    local real ux
    local real uy
    local real a
    local unit f
    local real p = GetStoredReal(gc, s, "speed")-0.5/(1+0.5*i)
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+p*Cos(a), uy+p*Sin(a))
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
        call StoreReal(gc, s, "speed", p)
    else
...

We load the variable and subtract a bit (based on level, as the duration is based on level. If it was not based on the spell's level, then the spell would be extremely slow during the extra duration at the higher levels).

In the SetUnitPosition line, we now simply use 'p', the variable, instead of the speed we used directly before (40).
We also save the new, reduced speed in the gamecache.

Adding dust

NOTE: I will continue working on the code we first made WITH the changes for speed we added above.

To make the pushback look more realistic, we will add a dust effect.

I'm going to use the Impale Target Dust model (Objects\Spawnmodels\Undead\ImpaleTargetDust\ImpaleTargetDust.mdl) here, so let's start by adding a line that preloads this effect to the InitTrig function:

Collapse JASS:
function InitTrig_Stomp takes nothing returns nothing
    set gg_trg_Stomp = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Stomp, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( gg_trg_Stomp, Condition( function Trig_Stomp_Conditions ) )
    call TriggerAddAction( gg_trg_Stomp, function Trig_Stomp_Actions )
    call Preload("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl")
    call Preload("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl")
endfunction

Now let's go to the timer function. We won't create the effect on each unit everytime the timer expires, so we are going to use another real to keep track of when to create the effect:

Collapse JASS:
...
    local real ux
    local real uy
    local real a
    local unit f
    local real p = GetStoredReal(gc, s, "speed")-0.5/(1+0.5*i)
    local real fx = GetStoredReal(gc, s, "fx")+0.05
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+p*Cos(a), uy+p*Sin(a))
            if fx >= 1 then
                call DestroyEffect(AddSpecialEffectTarget("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl", f, "origin"))
            endif
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
        call StoreReal(gc, s, "speed", p)
        call StoreReal(gc, s, "fx", fx)
        if fx >= 1 then
            call StoreReal(gc, s, "fx", 0)
        endif
    else
...

First we add a new real variable, 'fx'. We load the real attached to the timer as "fx". We increase it a bit, and if it is greater than or equal to 1, we will create (and instantly destroy, as the effect only has one animation) on each unit in the group.
Then we save the increased value, and in case it is bigger than or equal to 1, we save the value 0, so the effects will repeat.

Making effects easily changeable

NOTE: Like before, we continue working on the code with the above changes implemented.

This part of the tutorial will show you how to use the GetAbilityEffectById native, which can extract strings from the effect fields of any ability in the object editor.

This is very useful when you want to make changing effects used by a custom spell easy, because clicking on the effect in the Object Editor is a lot easier than what you can do in a script.

Collapse JASS:
native GetAbilityEffectById         takes integer abilityId, effecttype t, integer index returns string

The native is simple, here's a short explanation of the arguments:

integer abilityId - This is the rawcode of the spell you want to extract the effect from.

effecttype t - The effect field in the object editor that the effect is extracted from.
Here's a list of the effecttypes:
  • EFFECT_TYPE_AREA_EFFECT
  • EFFECT_TYPE_CASTER
  • EFFECT_TYPE_EFFECT
  • EFFECT_TYPE_LIGHTNING
  • EFFECT_TYPE_MISSILE
  • EFFECT_TYPE_SPECIAL
  • EFFECT_TYPE_TARGET
It should be pretty self-explanatory which field each effecttype uses.

integer index - The number of the effect in the effecttype field that you want to load, starting from 0.

For this spell I will use the EFFECT_TYPE_MISSILE field. An instant cast channel-based ability, that our spell is, won't use that field, as it does not send out any missiles. And it is best to select an unused field, as the spell then won't create the effect(s) you use at weird places when cast.

So let's add the War Stomp model (Abilities\Spells\Orc\WarStomp\WarStompCaster.mdl) as the first missile effect on the spell, the dust effect (Objects\Spawnmodels\Undead\ImpaleTargetDust\ImpaleTargetDust.mdl) as the second and the attachment point of the dust model (origin) as the third effect.
Yes, that's right, the effect fields can be used to store all kinds of strings that you use in your JASS-enhanced spell.

So let's replace the strings in the trigger with the GetAbilityEffectById native:

Collapse JASS:
function Trig_Stomp_Conditions takes nothing returns boolean
    return GetSpellAbilityId() == 'A000'
endfunction

function Stomp_Filter takes nothing returns boolean
    return IsPlayerEnemy(GetOwningPlayer(GetTriggerUnit()), GetOwningPlayer(GetFilterUnit())) and GetWidgetLife(GetFilterUnit()) > 0.405 and not IsUnitType(GetFilterUnit(), UNIT_TYPE_FLYING)
endfunction

function Stomp_CopyGroup takes group g returns group
    set bj_groupAddGroupDest = CreateGroup()
    call ForGroup(g, function GroupAddGroupEnum)
    return bj_groupAddGroupDest
endfunction

function Stomp_Move takes nothing returns nothing
    local string s = I2S(H2I(GetExpiredTimer()))
    local gamecache gc = udg_AbilityCache
    local real x = GetStoredReal(gc, s, "x")
    local real y = GetStoredReal(gc, s, "y")
    local integer i = GetStoredInteger(gc, s, "level")
    local group g = Stomp_CopyGroup(I2G(GetStoredInteger(gc, s, "group")))
    local real dur = GetStoredReal(gc, s, "dur")+0.05
    local real ux
    local real uy
    local real a
    local unit f
    local real p = GetStoredReal(gc, s, "speed")-0.5/(1+0.5*i)
    local real fx = GetStoredReal(gc, s, "fx")+0.05
    if dur < 1+0.5*i then
        loop
            set f = FirstOfGroup(g)
            exitwhen f == null
            set ux = GetUnitX(f)
            set uy = GetUnitY(f)
            set a = Atan2(uy-y, ux-x)
            call SetUnitPosition(f, ux+p*Cos(a), uy+p*Sin(a))
            if fx >= 1 then
                call DestroyEffect(AddSpecialEffectTarget(GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 1), f, GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 2)))
            endif
            call GroupRemoveUnit(g, f)
        endloop
        call StoreReal(gc, s, "dur", dur)
        call StoreReal(gc, s, "speed", p)
        call StoreReal(gc, s, "fx", fx)
        if fx >= 1 then
            call StoreReal(gc, s, "fx", 0)
        endif
    else
        call DestroyGroup(I2G(GetStoredInteger(gc, s, "group")))
        call FlushStoredMission(gc, s)
        call DestroyTimer(GetExpiredTimer())
    endif
    set gc = null
    call DestroyGroup(g)
    set g = null
    set f = null
endfunction

function Trig_Stomp_Actions takes nothing returns nothing
    local unit c = GetTriggerUnit()
    local real x = GetUnitX(c)
    local real y = GetUnitY(c)
    local integer i = GetUnitAbilityLevel(c, 'A000')
    local boolexpr b = Condition(function Stomp_Filter)
    local group g = CreateGroup()
    local group n
    local unit f
    local gamecache gc = udg_AbilityCache
    local timer t = CreateTimer()
    local string s = I2S(H2I(t))
    call DestroyEffect(AddSpecialEffect(GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 0), x, y))
    call GroupEnumUnitsInRange(g, x, y, 100+50*i, b)
    set n = Stomp_CopyGroup(g)
    loop
        set f = FirstOfGroup(n)
        exitwhen f == null
        call UnitDamageTarget(c, f, 25*i, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
        call GroupRemoveUnit(n, f)
    endloop
    call StoreInteger(gc, s, "level", i)
    call StoreInteger(gc, s, "group", H2I(g))
    call StoreReal(gc, s, "x", x)
    call StoreReal(gc, s, "y", y)
    call TimerStart(t, 0.05, true, function Stomp_Move)
    set c = null
    call DestroyBoolExpr(b)
    set b = null
    set g = null
    call DestroyGroup(n)
    set n = null
    set f = null
    set gc = null
    set t = null
endfunction

//===========================================================================
function InitTrig_Stomp takes nothing returns nothing
    set gg_trg_Stomp = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Stomp, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( gg_trg_Stomp, Condition( function Trig_Stomp_Conditions ) )
    call TriggerAddAction( gg_trg_Stomp, function Trig_Stomp_Actions )
    call Preload(GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 0))
    call Preload(GetAbilityEffectById('A000', EFFECT_TYPE_MISSILE, 1))
endfunction


Making the spell follow the JESP Standard

The JESP Standard is a standard that makes spell sharing easier. It is in no way required that your spell follows the standard, but if you plan to release it, it is good if it does. A spell that follows the standard is usually a lot easier to import, configure and work with than a spell that does not.

You can find a read the standard here.

This part of the tutorial is a short guide about how to make a spell follow the standard. I will of course use the spell we just created here as example.

First we will have to change the function names, so all of them starts with the spell's code name (Stomp) + _ + the function name.

Almost all the functions I've added here follows this, except the "Trig_Stomp_Actions" and "Trig_Stomp_Conditions" functions that the WE generated and we later modified. Simply remove the "Trig_" in the function names.
Also change the lines in the "InitTrig_Stomp" function to remove the "Trig_" in front of the two function names there.

The JESP Standard requires the spell to have configuration functions, so let's add some.

When making configuration functions, they should always be constant functions when possible.
You can read more about constant functions here.

The first configuration function that we will add one that allows you to easily change the spell's rawcode.

Collapse JASS:
constant function Stomp_SpellId takes nothing returns integer
    return 'A000'
endfunction

Add a function like that to the top of the spell's code, and do a "Replace all" where you replace the id you used when coding the spell, 'A000', with Stomp_SpellId().

This is basically how it is done. A good idea would be to add configuration functions for damage, speed and duration - You can (and should) make these functions take a level parameter, to make it easier for the user to configure the spell if it has multiple levels.

To make the spell follow the JESP Standard, the script has to include the name of the author. Simply add that as a comment in the top, like this:

Collapse JASS:
// Stomp spell by Blade.dk
// Visit [url]http://www.wc3campaigns.net[/url]

constant function Stomp_SpellId takes nothing returns integer
    return 'A000'
endfunction

You can also include contact information and other things that you want there.

You might consider using a system like KaTTaNa's Local Handle Variables functions or Vexorian's CSCache engine (a part of the Caster System).

The advantage of using a more known and common system is, that people will have to deal with less different systems that does the same thing, and it will prevent trouble with different gamecache / return bug systems that can be caused by, for example, similiar function names.

The disadvantage is that direct gamecache usage actually is more efficient and therefore faster.

Remember that you will have to include the JESP Standard document in a disabled trigger in the map, so the knowledge of the standard can be spread - It is also a requirement for the spell to follow the standard.


Final notes

The spell we made here is multistanceable and free of memory leaks.

If you succeeded making it, congratulations.

There is a lot more that you can do with JASS. I plan to make several tutorials following up on this one, based on different spell themes and teaching you about other subject in JASS and spell making.

Thanks for reading this tutorial. Comments are very welcome. If you have a problem with your spell, don't forget to ask in the Triggers & Scripts forum.

If you make a nice spell, please submit it to our resource section. Please read our rules before doing so, and please do not submit the result of what you made following this tutorial. I will be happy to hear if it went good, but we don't really need 20 stomp with pushback spells there.

- Thanks,

Blade.dk
__________________
Spell Making Course: Part 1: Making a simple stomp spell.
I wonder if I'll ever finish part 2.
Blade.dk is offline   Reply With Quote
Sponsored Links - Login to hide this ad!
Old 05-18-2006, 06:15 PM   #2
Blade.dk
.
 
Blade.dk's Avatar


Respected User
 
Join Date: May 2005
Posts: 1,990

Submissions (15)

Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)

Approved Map: Azeroth's Arcane ArenaSpell session 01 winner

Send a message via MSN to Blade.dk
Default

I would like to specially thank Anitarf and Tim. for helping me find and fix several errors in this tutorial. If you have any questions and comments for this tutorial, please post them.
__________________
Spell Making Course: Part 1: Making a simple stomp spell.
I wonder if I'll ever finish part 2.
Blade.dk is offline   Reply With Quote
Old 05-21-2006, 12:00 AM   #3
PipeDream
Moderator
 
PipeDream's Avatar


Code Moderator
 
Join Date: Feb 2006
Posts: 1,405

Submissions (6)

PipeDream is a glorious beacon of light (463)PipeDream is a glorious beacon of light (463)PipeDream is a glorious beacon of light (463)PipeDream is a glorious beacon of light (463)

Default

Wow Blade, great job.

Why not use GroupAddGroup(source,target) from blizzard.j instead of rolling your own copygroup?
I think you should also fix the timer stuff explicitly by wrapping the main function. You could save people a lot of headaches by showing them how to avoid it right away.
Just my 2c.
__________________
PipeDream is offline   Reply With Quote
Old 05-21-2006, 01:35 AM   #4
emjlr3
Rehabbing
 
emjlr3's Avatar
 
Join Date: Jun 2005
Posts: 1,386

Submissions (14)

emjlr3 is a jewel in the rough (151)emjlr3 is a jewel in the rough (151)

Mapping Contest First Place

Send a message via AIM to emjlr3 Send a message via MSN to emjlr3
Default

wow, simply put

anyone looking to make better spells/even try and learn some JASS, this i very helpful
__________________
emjlr3 is offline   Reply With Quote
Old 05-23-2006, 03:01 PM   #5
Chuckle_Brother
Oh for the sake of fudge
 
Chuckle_Brother's Avatar


Respected User
 
Join Date: Dec 2005
Posts: 782

Submissions (2)

Chuckle_Brother will become famous soon enough (53)Chuckle_Brother will become famous soon enough (53)

Send a message via ICQ to Chuckle_Brother Send a message via AIM to Chuckle_Brother Send a message via MSN to Chuckle_Brother Send a message via Yahoo to Chuckle_Brother
Default

Great tutorial for beginners.

Gj, but no rep. 'tis too evil
__________________
"...you play a mean banjo"
Chuckle_Brother is offline   Reply With Quote
Old 05-23-2006, 11:49 PM   #6
Kahlar
User
 
Join Date: May 2006
Posts: 3

Kahlar has little to show at this moment (0)

Default

This was very insightful. I learned a lot great Tutorial.
Kahlar is offline   Reply With Quote
Old 05-24-2006, 10:43 PM   #7
Kingtofu
User
 
Join Date: May 2004
Posts: 43

Kingtofu has little to show at this moment (0)

Default

Very nice tutorial. However, I haven't managed to test it yet as I've encountered my fourth error. Frustrating. I even attempted to save the map before I hit 'test map,' and another error came up. Instead of saving the map, it saved a folder of the various components. Using default editor, not sure why this is happening. I haven't had any success in using the custom script section in any of the tutorials I've gone through yet. Whats more is in the last two attempts to test out the spell, I copied and pasted the finished code straight from your post. Any ideas?

Edit: I've been testing to figure out what's causing the errors. I've tested my map and it works fine, so I'm sure that it's the World Editor as a whole. When I started a new map, I went straight to the custom script section and pasted your code:

Collapse JASS:
function H2I takes handle h returns integer
    return h
    return 0
endfunction

function I2G takes integer i returns group
    return i
    return 0
endfunction

When I went to uncheck the "Enabled" box on the Map Initialization trigger, instantly an error popped up. I follow your link and download JassCraft.

So I'm messing around with JassCraft. I'm totally in the dark about how to use it properly.

Last edited by Kingtofu : 05-24-2006 at 11:21 PM.
Kingtofu is offline   Reply With Quote
Old 05-25-2006, 09:10 AM   #8
Anitarf
Procrastination Incarnate


Development Director
 
Join Date: Feb 2004
Posts: 8,075

Submissions (19)

Anitarf has a brilliant future (888)Anitarf has a brilliant future (888)Anitarf has a brilliant future (888)Anitarf has a brilliant future (888)Anitarf has a brilliant future (888)Anitarf has a brilliant future (888)Anitarf has a brilliant future (888)Anitarf has a brilliant future (888)

2008 Spell olympics - Fire - SilverApproved Map: Old School Alliance TacticsHero Contest #2 - 3rd PlaceSpell making session 2 winner

Default

Yeah, I2G should return null, not 0, silly Blade!

Also, when you are talking about making the spell follow the JESP standard, you forgot to mention that Game Cache keys also need the spell code prefix, same as function names; also, the spell not only should, but must use one of the mentioned game cache systems, because you are not allowed to have any code that's unique to your spell outside of the spell's "trigger" if you want it to be JESP, you are only allowed to use general external systems.

A good tutorial, otherwise.
__________________
Anitarf is offline   Reply With Quote
Old 05-25-2006, 01:39 PM   #9
Kingtofu
User
 
Join Date: May 2004
Posts: 43

Kingtofu has little to show at this moment (0)

Default

Ah, that fixed the custom scripts section problem Ani, thanks.

Now when I set up the code, set the rawcode tags, and cast the ability... nothing happens. Any chance I can get a hold of the finished product and cross-examine?

Last edited by Kingtofu : 05-25-2006 at 01:45 PM.
Kingtofu is offline   Reply With Quote
Old 05-25-2006, 02:21 PM   #10
blu_da_noob
Nonchalant
 
blu_da_noob's Avatar


Respected User
 
Join Date: Mar 2006
Posts: 1,933

Submissions (2)

blu_da_noob is just really nice (398)blu_da_noob is just really nice (398)blu_da_noob is just really nice (398)blu_da_noob is just really nice (398)blu_da_noob is just really nice (398)blu_da_noob is just really nice (398)

[Quicksilver #2] - 2nd Place[Quicksilver#1] 1st place

Send a message via MSN to blu_da_noob
Default

Quote:
Originally Posted by Anitarf
you forgot to mention that Game Cache keys also need the spell code prefix

Not always; only when attached to a public object. If your spell creates a new timer each time the spell is cast, anything attached to that timer does not need a unique key, and thus does not need the key to be prefixed by the spell name for changing.
__________________
blu_da_noob is offline   Reply With Quote
Old 05-26-2006, 10:28 PM   #11
MasterofSickness
User
 
MasterofSickness's Avatar
 
Join Date: Feb 2006
Posts: 212

MasterofSickness is on a distinguished road (23)

Exclamation

Please correct that in the toturial above, because it took me much time to figure out, that the problem is perhaps already solved in further posts under the tutorial... and yeah, Kingtofu had the same problem!
Please fix that:

Collapse JASS:
function I2G takes integer i returns group
    return i
    return null
endfunction
return null not 0

Thanks!
... and also thanks for this nice tut!
__________________
my nicknames:
20002006200720102012
Master of SicknessDesignatusKakarotEdwardElricSourceSeeker

Last edited by MasterofSickness : 05-26-2006 at 10:30 PM.
MasterofSickness is offline   Reply With Quote
Old 05-28-2006, 07:30 PM   #12
Blade.dk
.
 
Blade.dk's Avatar


Respected User
 
Join Date: May 2005
Posts: 1,990

Submissions (15)

Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)

Approved Map: Azeroth's Arcane ArenaSpell session 01 winner

Send a message via MSN to Blade.dk
Default

Yeah, CnP for the win.

Game cache keys shall not use the spells codename as prefix in this cache. That only applies to public values stored in the cache, and none of the values here are public, as they use a string unique to each timer - Nothing else is supposed to read it.

And actually, another system similiar to those could be made, and it would be valid to use it.
__________________
Spell Making Course: Part 1: Making a simple stomp spell.
I wonder if I'll ever finish part 2.
Blade.dk is offline   Reply With Quote
Old 05-30-2006, 10:55 AM   #13
Blade.dk
.
 
Blade.dk's Avatar


Respected User
 
Join Date: May 2005
Posts: 1,990

Submissions (15)

Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)

Approved Map: Azeroth's Arcane ArenaSpell session 01 winner

Send a message via MSN to Blade.dk
Default

* Updated *

Some minor errors got fixed, and I added a table of contents.
__________________
Spell Making Course: Part 1: Making a simple stomp spell.
I wonder if I'll ever finish part 2.
Blade.dk is offline   Reply With Quote
Old 06-14-2006, 03:45 AM   #14
CrashLemon
User
 
CrashLemon's Avatar
 
Join Date: Jul 2005
Posts: 105

Submissions (2)

CrashLemon has little to show at this moment (7)

Default

Dosen't work for me... I have an error at almost every single Line... Can someone help me? (66 Compiled error)
__________________
I'm working on Automne RPG (Fall RPG). Its a french RPG and you can take a look at the terrain by cliking here.

Last edited by CrashLemon : 06-14-2006 at 03:46 AM.
CrashLemon is offline   Reply With Quote
Old 06-14-2006, 06:52 AM   #15
Blade.dk
.
 
Blade.dk's Avatar


Respected User
 
Join Date: May 2005
Posts: 1,990

Submissions (15)

Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)Blade.dk is a glorious beacon of light (418)

Approved Map: Azeroth's Arcane ArenaSpell session 01 winner

Send a message via MSN to Blade.dk
Default

We can help you if you post your code, and only if you post your code.
__________________
Spell Making Course: Part 1: Making a simple stomp spell.
I wonder if I'll ever finish part 2.
Blade.dk is offline   Reply With Quote
Reply


Thread Tools Search this Thread
Search this Thread:

Advanced Search

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off


All times are GMT. The time now is 01:04 PM.


Donate

Affiliates
The Hubb http://bylur.com - Warcraft, StarCraft, Diablo and DotA Blog & Forums The JASS Vault Clan WEnW Campaign Creations Clan CBS GamesModding Flixreel Videos

Powered by vBulletin (Copyright ©2000 - 2014, Jelsoft Enterprises Ltd).
Hosted by www.OICcam.com
IT Support and Services provided by Executive IT Services