|
|
#1 | ||||||||||
|
.
Respected User
|
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: 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: 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: 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: 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. 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: 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: 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: 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: JASS:set udg_AbilityCache = InitGameCache("abilitycache.w3v") The InitCache JASS trigger should look like this now: 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: 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:
Convert the trigger to JASS. It should look like this now: 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: 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: 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: 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. 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. 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. 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. 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. 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: 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: 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. 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. 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: 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. 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. 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:
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. 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: 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: 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. 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: 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: 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: 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. 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:
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: 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. 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: 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 |
||||||||||
|
|
|
| Sponsored Links - Login to hide this ad! |
|
|
|
|
#2 |
|
.
Respected User
|
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.
__________________ |
|
|
|
|
|
#3 |
|
Moderator
Code Moderator
Join Date: Feb 2006
Posts: 1,405
![]() ![]() ![]()
|
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. |
|
|
|
|
|
#4 |
|
Rehabbing
|
wow, simply put
__________________anyone looking to make better spells/even try and learn some JASS, this i very helpful |
|
|
|
|
|
#5 |
|
Oh for the sake of fudge
Respected User
|
Great tutorial for beginners.
__________________Gj, but no rep. 'tis too evil |
|
|
|
|
|
#6 |
|
User
Join Date: May 2006
Posts: 3
|
This was very insightful. I learned a lot great Tutorial.
|
|
|
|
|
|
#7 |
|
User
Join Date: May 2004
Posts: 43
|
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: 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. |
|
|
|
|
|
#8 |
|
Procrastination Incarnate
Development Director
|
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. |
|
|
|
|
|
#9 |
|
User
Join Date: May 2004
Posts: 43
|
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. |
|
|
|
|
|
#10 | |
|
Nonchalant
Respected User
|
Quote:
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. |
|
|
|
|
|
|
#11 |
|
User
Join Date: Feb 2006
Posts: 188
|
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: JASS:function I2G takes integer i returns group return i return null endfunction Thanks! ... and also thanks for this nice tut! Last edited by MasterofSickness : 05-26-2006 at 10:30 PM. |
|
|
|
|
|
#12 |
|
.
Respected User
|
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. |
|
|
|
|
|
#13 |
|
.
Respected User
|
* Updated *
__________________Some minor errors got fixed, and I added a table of contents. |
|
|
|
|
|
#14 |
|
User
Join Date: Jul 2005
Posts: 105
|
Dosen't work for me... I have an error at almost every single Line... Can someone help me? (66 Compiled error)
__________________Last edited by CrashLemon : 06-14-2006 at 03:46 AM. |
|
|
|
|
|
#15 |
|
.
Respected User
|
We can help you if you post your code, and only if you post your code.
__________________ |
|
|
|
![]() |
| Thread Tools | Search this Thread |
|
|
|
Donate |