 |
WC3Jass.com "The Jass Vault"
|
Affiliates
|
 |
|
 |
|
|
| Message |
Posted:
Tue May 30, 2006 10:49 am Post subject:
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:
| Code: |
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:
| Code: |
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:
| Code: |
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:
| Code: |
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.
| Code: |
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:
| Code: |
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:
| Code: |
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:
| Code: |
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:
| Code: |
set udg_AbilityCache = InitGameCache("abilitycache.w3v")
|
The InitCache JASS trigger should look like this now:
| Code: |
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:
| Code: |
Stomp
Events
Unit - A unit Starts the effect of an ability
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.
Unit - A unit Begins casting an ability: This 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 ability: This 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 ability: This 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:
| Code: |
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:
| Code: |
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:
| Code: |
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:
| Code: |
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.
| Code: |
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.
| Code: |
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.
| Code: |
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.
| Code: |
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.
| Code: |
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:
| Code: |
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:
| Code: |
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.
| Code: |
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.
| Code: |
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:
| Code: |
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.
| Code: |
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.
| Code: |
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:
SetUnitPosition: This 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/Y:SetUnitX 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.
| Code: |
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:
| Code: |
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:
| Code: |
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.
| Code: |
...
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:
| Code: |
...
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:
| Code: |
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:
| Code: |
...
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.
| Code: |
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:
| Code: |
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.
| Code: |
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:
| Code: |
// Stomp spell by Blade.dk
// Visit http://www.wc3campaigns.net
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 forums.
If you make a nice spell, please submit it to Wc3Campaigns' resource section. Please read the 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 _________________ #wc3dev
Last edited by Blade.dk on Fri Jun 02, 2006 5:36 pm; edited 1 time in total |
|
|
|
 |
|
 |
|
 |
 |
|
 |
|
|
| Message |
Posted:
Tue May 30, 2006 10:54 am Post subject:
|
|
|
Comments and suggestions are welcome.
There might be some minor formatting issues (because I originally wrote it for Wc3C, so it used other code tags), but I think I've fixed them all. _________________ #wc3dev |
|
|
|
 |
|
 |
|
 |
 |
|
 |
|
|
| Message |
Posted:
Wed May 31, 2006 12:22 pm Post subject:
|
|
|
For a tutorial named "Making a simple stomp spell", I find it quite long. Really really long actually. I mean you are picking up a lot of stuff like explaining the handle type, type-casting in Jass, game cache and memory leaks (including nullifying). There are a lot of detailed explanations of stuff that isn't really used in the spell, for example: "A unit Begins casting an ability" and "A unit Finishes casting an ability".
Okay, you have covered a lot of information, and those stalwart enough to read through it all will have learned a lot. But maybe in part two you should not try to cover everything at once.
Still nice work though, just try and keep an eye on the amount of information you are trying to cover in one take.
A couple of headings that appear in very small text:
More realistic movement
Adding dust
Making effects easily changeable |
|
|
|
 |
|
 |
|
 |
 |
|
 |
|
|
| Message |
Posted:
Fri Jun 02, 2006 5:33 pm Post subject:
|
|
|
I wanted it to cover almost everything, everything else would have been messy in my opinion. It could have been splitted into two parts, one about types, typecasting and gamecache and one about how to make the actual spell, but that would not be good, in my opinion. Here is all the theory and base knowledge that you should need to make the spell, and it also tells how to make the actual thing, and use the knowledge from the first part.
The spell is simple, yes. But the title is not supposed to say that making it is simple.
The next tutorials in this 'spell making course' will most likely not be as long as this one, simply because a lot of the basics are covered in here.
I'll fix the formatting errors. _________________ #wc3dev |
|
|
|
 |
|
 |
|
 |
 |
|
 |
|
|
| Message |
Posted:
Thu Aug 02, 2007 8:38 pm Post subject:
|
|
|
T_T i get an error that says
undeclared function I2G
at local group g= Stomp_CopyGroup(I2G(GetStoredInteger(gc , s , "group")))
and one
at call DestroyGroup(I2G(GetStoredInteger(gc , s , "group"))) |
|
|
|
 |
|
|
|
| Message |
Posted:
Thu Aug 02, 2007 10:54 pm Post subject:
|
|
|
| nvm didnt see the custom script thinggy |
|
|
|
 |
|
 |
|
 |
 |
|
 |
|
|
| Message |
Posted:
Sat Aug 09, 2008 1:42 am Post subject:
Why doesn't this work |
|
|
I have used GUI for close to 3 years now and I know how to use it quite well but recently I have decided to move onto jass, mostly for 2 reasons.
Its harder, and in many cases impossible to make some spells MUI in GUI, you cannot make dynamic triggers in GUI(and since when a unit takes damage only works for a specific unit and not player owned unit or anyunit I am at a huge disadvantage.
So I tried your tutorial on how to make this spell and I did learn quite a bit, and the trigger had no errors while loading into the game, but when I tried to use it, it didn't make any of the units slide, but instead it just did the damage.
I read through it a few times and I am to inexperience to find whatever is causing the spell to misfire so all.
Also, thank you for making this tutoral, along with Vex's 2 tutorials this has help me understand jass, and before reading this tutorial I had never used timers or GameCaches so this tutorial also taught me a little about those.
I did add the H2I, G2I, I2G, and I2H functions to the map custom script just as you did.
Here is my final code:
| Code: |
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(GetLastCreatedTimerBJ()))
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
|
|
|
|
|
 |
|
 |
|
 |
|
You can post new topics in this forum You can reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
|