Tutorial Expert spellcrafting with KT2 and GT

Jesus4Lyf

Good Idea™
Reaction score
397
I couldn't actually post the tutorial, sadly, as it was way too long, and the maximum post length is 50,000. So here's as much as I can post, and the full post is attached. If you want to see the end result spell, simply download the attachment, open the StartHere map, go to the MySpell trigger, and copy in the "FINAL RESULT" part of the txt in the zip, which is at the very end. I'm not sure how useful this tutorial will be due to it's massive length, but I thought I may as well post it anyway and let others decide if it's useful or not.

Requires Key Timers 2, GTrigger and JASS NewGen.

This tutorial is aimed at teaching intermediate to experienced JASSers how to write very nice spells very quickly and very efficiently, using the full speed and efficiency of the research done on Key Timers 2 and GTrigger. Please note that I'm not here to teach you maths. Sorry.

Let's write a channelling spell that gathers a ball of energy together and then shoots it in a direction with crazy awesome effects, including particle emittors. Particle emittors can be added anywhere in nearly any spell, just for eye candy, as long as they don't spam particles -too- hard.

So firstly, you need Key Timers 2, GTrigger, a dummy model to attach effects to, and a channelling dummy spell to attach the code to. Acquiring these things is out of the scope of the tutorial, instead I'm simply attaching a StartHere map so we can get to what this tutorial is about. Coding.

Please remember that this tutorial is for intermediate to experienced JASSers. Here we go.

Open up the StartHere.w3x map in JASS NewGen. Open the object editor, note that there is a custom ability 'A000' which is MySpell, a custom unit which is 'u000', which uses a custom model "Dummy.mdx" found in the import manager. This has a single attachment point, "origin". Whatever dummy model you use should be fine, as long as it has "origin" (if you prefer a different one).

Open up the trigger editor, note that in Systems is KT and GT, which are Key Timers 2 and GTrigger respectively. You should check the threads for these systems to make sure you're using the latest versions, naturally. Moving on, open the Spells folder and click on MySpell, which to your horror, you will find completely blank. This tutorial will teach you how to change that.

Firstly, we need a basic scope and such which will contain our spell. It will have an intializer which is run when the map is loaded. This is the basic structure of any spell.
JASS:
scope MySpell initializer Init
    
private function Init takes nothing returns nothing
endfunction

endscope

Cool. Something that does nothing. Now let's remember that constants are good. Really good. Let's specify them appropriately. These should be private so they can't be accessed from outside the spell, and should be constant because they won't be changed. One for the dummy unit, and one for the spell id.
JASS:
globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
endglobals

So all together now:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
endglobals

private function Init takes nothing returns nothing
endfunction

endscope
Alright, cool. But it still does nothing. Let's make an OnCast function to be run when the spell gets cast. Now for some explaining. TriggerActions are frowned upon for various reasons. It is possible to put a spells actions in it's conditions instead. This is more efficient. All you need to do to do this is make the function return a boolean instead of nothing, and write "return false" at the end of the OnCast function. So we have our blank actions...
JASS:
private function OnCast takes nothing returns boolean
    // Do things
    return false
endfunction

Now to utilise GTrigger to make this real neat. (Actually, GTrigger is really nice here, as it will inline. Twice.) To make this function get called when the spell is cast, we simply add one line to the Init function. As per the GTrigger documentation, this line is:
JASS:
call GT_AddStartsEffectAction(function OnCast, ABIL)

ABIL being our spell id constant, and OnCast being the function we want executed. So all together now:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
endglobals

private function OnCast takes nothing returns boolean
    // Do things
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
And that will be the end of our GTrigger use for this spell. Note that if you destroy a temporary trigger with a GTrigger event, you must call the appropriate Unregister just before you destroy it. See the GTrigger documentation for more details.

WHY GTRIGGER?
GTrigger is faster to code with, takes less lines of code, doesn't spam events, and runs more efficiently. The traditional method of writing spells is to trigger off any spell being cast, then terminate the trigger if it isn't the relevant spell. This means whenever any spell is cast, you will fire 50 triggers if you have 50 spells, and then 49 will close shortly after when they fail the condition. Why do this, when you can have GTrigger which will only fire the necessary trigger? GTrigger, according to the most recent tests up to when this tutorial was written, is faster to fire even if you have as little as 2 spells in the map using it. Therefore, all spells should use GTrigger.
Now, we need a spell instance! Let's make it so the spell instance is centered around a ball which will spawn directly in front of the caster by 64 units. So we need a struct to represent an instance. This struct needs to know the caster, the order id of the spell (in case it is interrupted) and the ball dummy unit itself. So we have:
JASS:
private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
endstruct

All together:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
endglobals

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
endstruct

private function OnCast takes nothing returns boolean
    // Do things
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
We want to create little particles around the ball which fade into existance and then get sucked into the ball, making it grow. So we need to choose models for the ball and the particles getting sucked in, as well as the scaling of the particles getting sucked in. So we add more constants. So we add:
JASS:
private constant string BALLMODEL="Abilities\\Weapons\\SerpentWardMissile\\SerpentWardMissile.mdl"
private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
private constant real PARTICLESCALE=0.4

Feel free to choose alternatives. I just thought at a glance, "Hey, these look pretty cool".

Because the ball is growing, we also need our instance to know about how large the ball is, so we can increment it further. So we add:
JASS:
real ballscale
to our InstanceData struct. All together now:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\SerpentWardMissile\\SerpentWardMissile.mdl"
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
endstruct

private function OnCast takes nothing returns boolean
    // Do things
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
Now that we've finished the intial ground work for the spell, it's time to initialise our instance. I am not applying proper object oriented princibles here. If you were, you'd do these things on creation of the struct. Instead, so that everyone can follow and understand, I am doing this in the OnCast function. First, we declare a local variable of our InstanceData, and create a fresh one. Then, for that instance, we set our caster to the casting unit, the orderid to its current order, the balldummy to a new dummy unit, the balleffect to an effect attached to this dummy unit, and the ballscale to 0.01, and then set the scale of the dummy unit to the ballscale. Sounds like a lot? Well, not really. Here's the code.
JASS:
private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    set d.caster=GetSpellAbilityUnit()
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitY(d.caster)+Sin(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitFacing(d.caster))
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

WOAH! What the hell is that create unit line?
Don't worry, I won't be that mean. But I did say I'm not here to teach you maths. Let's take a good look at it.
JASS:
set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitY(d.caster)+Sin(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitFacing(d.caster))

So the ball dummy is created for Neutral Passive, the unit type is the dummy unit type, and the facing is the same direction as the caster. The rest? Well...
JASS:
X --> GetUnitX(d.caster)+Cos(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0
Y --> GetUnitY(d.caster)+Sin(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0

This is the code for a polar offset. It means 64.0 units in the direction the caster is facing, from where the caster is standing. In other words, create this ball in front of the caster. All you have to change to make it further or close, is the 64.0. This can go very nicely in a constant. In fact, I recommend you put it in a constant.
Still with me? Sweet. If you got a little lost, here it is, all together:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\SerpentWardMissile\\SerpentWardMissile.mdl"
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
endstruct

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    set d.caster=GetSpellAbilityUnit()
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitY(d.caster)+Sin(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitFacing(d.caster))
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
Now let's make that ball grow! It's a Good Idea (tm) to use one consistent period when you write spells. A common preference (and my preference these days) is 0.03125. Code that runs on a period of 0.03125 will be executed exactly 32 times a second. That's a pretty nice number. It won't be framey or jumpy, and it won't run so often that it wastes processing power. So let's use that. Here's where KT2 steps in. KT2 is going to be our best friend for things like what we're going to undertake in this tutorial. To add to KT2 you call KT_Add(function, data, period). This means we need something to attach (our instance data), and something to run (a functon). Let's write the frame for that function now, and call it ExpandBall. For KT2, the function must take nothing and return a boolean.
JASS:
private function ExpandBall takes nothing returns boolean

This function must return false while you want to keep executing it each period, and then return true once it's done with. So we always want the last line to be "return false", and where we say "return true" it's the equivalent of destroying a timer (except for the nastiness of actually doing that). KT2 also will give us back the struct we attach to it in our "Add" call which I will tell you about soon. We get this by declaring a local variable of our struct type, and setting it to KT_GetData(). In fact, KT2 requires that you use KT_GetData() somewhere in attached code exactly once, or else. If you don't actually attach or use the data, you can simply include "call KT_GetData()" to satisfy this. But for this tutorial, and nearly every time you use this system, you will want to attach data. So our function looks like this:
JASS:
private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    // Expand ball.
    return false
endfunction

Now, we want this code to be run 32 times a second from the moment the spell is cast, so we add to our OnCast function this line:
JASS:
call KT_Add(function ExpandBall, d, 0.03125)

WHY KEY TIMERS 2?
Key Timers 2, at the time of writing this tutorial, is easily the most efficient timer system for what we are undertaking. Yes, that means faster than TU Red, and Timer Ticker, and all the other alternatives. However, this is not the only reason to use it. It has a brilliantly neat interface, saving us lines of code and time, and it's also very stable. More stable than TU Red. KT2 is your friend for writing spells.
So, all together now:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\SerpentWardMissile\\SerpentWardMissile.mdl"
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
endstruct

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    // Expand ball.
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    set d.caster=GetSpellAbilityUnit()
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitY(d.caster)+Sin(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitFacing(d.caster))
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
Let's fill in that ExpandBall code! All we need for starters is two simple lines. Add to the ballscale by an amount, and then call SetUnitScale on the dummy to make the ball scale appropriately. So that's...
JASS:
set d.ballscale=d.ballscale+0.04
call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)

Why 0.04? Well, we know that we want the ball to get a little bigger than the default size of the specified normal ball model, and this spell is going to have a channel time of 1.5 seconds. The code executes every 0.03125 seconds, so if we used 0.03125, we would end up being exactly 1.5 times the size of the ball model. So for a little bigger, 0.04.
In reality, we don't want this hard coded into the spell. Let's let the user change this if they like. Let's make it a constant named "BALLSCALEINCREMENT". So all together now:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\SerpentWardMissile\\SerpentWardMissile.mdl"
    private constant real BALLSCALEINCREMENT=0.04
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
endstruct

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    set d.ballscale=d.ballscale+BALLSCALEINCREMENT
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    set d.caster=GetSpellAbilityUnit()
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitY(d.caster)+Sin(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitFacing(d.caster))
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
Now, we also want to stop growing when the spell finishes, or is cancelled. For the sake of this tutorial, let's simply say that's when the caster's current order changes. So all we have to add is the following:
JASS:
if GetUnitCurrentOrder(d.caster)!=d.orderid then
    // Do whatever should be done after the stops channeling.
    return true
endif

Looking good! All together:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\SerpentWardMissile\\SerpentWardMissile.mdl"
    private constant real BALLSCALEINCREMENT=0.04
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
endstruct

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    if GetUnitCurrentOrder(d.caster)!=d.orderid then
        // Do whatever should be done after the spell stops channeling.
        return true
    endif
    set d.ballscale=d.ballscale+BALLSCALEINCREMENT
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    set d.caster=GetSpellAbilityUnit()
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitY(d.caster)+Sin(GetUnitFacing(d.caster)*bj_DEGTORAD)*64.0, GetUnitFacing(d.caster))
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
Let's hit test.
Ew! What a terrible model! But before we change it, notice that the direction it faces isn't always correct? This is because of the deadlord's turn speed. So we need to stop using GetUnitFacing, and do some more trigonometry. To get the angle, in radians, to the target point of a spell, we find the target location, do some trig, then remove and null the target location. Note that now we have the direction in radians! We need to change our Create Dummy line also, because we no longer need to use *bj_DEGTORAD and we need to add *bj_RADTORED for the facing. Excuse the lack of explanation for this - once again you either know the maths or you don't. Now, I'm actually going to add direction into our struct because we'll need it in the next step anyway. So our on cast functon now looks like this:
JASS:
private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    local location targetpoint=GetSpellTargetLoc()
    set d.caster=GetSpellAbilityUnit()
    // Get spell direction. (In radians.)
    set d.direction=Atan2(GetLocationY(targetpoint)-GetUnitY(d.caster),GetLocationX(targetpoint)-GetUnitX(d.caster))
    call RemoveLocation(targetpoint)
    set targetpoint=null
    // End get spell direction.
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(d.direction)*64.0, GetUnitY(d.caster)+Sin(d.direction)*64.0, d.direction*bj_RADTODEG)
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

And we add to our struct this:
JASS:
real direction

Now let's change that terrible model! Just change the constant.
JASS:
private constant string BALLMODEL="Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"

So all together now, after our first round of testing:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"
    private constant real BALLSCALEINCREMENT=0.04
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
    real direction
endstruct

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    if GetUnitCurrentOrder(d.caster)!=d.orderid then
        // Do whatever should be done after the spell stops channeling.
        return true
    endif
    set d.ballscale=d.ballscale+BALLSCALEINCREMENT
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    local location targetpoint=GetSpellTargetLoc()
    set d.caster=GetSpellAbilityUnit()
    // Get spell direction. (In radians.)
    set d.direction=Atan2(GetLocationY(targetpoint)-GetUnitY(d.caster),GetLocationX(targetpoint)-GetUnitX(d.caster))
    call RemoveLocation(targetpoint)
    set targetpoint=null
    // End get spell direction.
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(d.direction)*64.0, GetUnitY(d.caster)+Sin(d.direction)*64.0, d.direction*bj_RADTODEG)
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
Hit test. Much nicer! Notice how smoothly the ball grows? And hitting stop makes the ball stop growing immediately. Let's make it so when the spell stops channeling, the ball blows up and shoots out a heap of tiny damaging projectiles depending on how big it grew. So firstly, the traditional frame for a KT2 periodic function:
JASS:
private function LaunchParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    // Launch a particle
    return false
endfunction

Now, just before the "return true" in ExpandBall, we want to dispose of the ball. So destroy the attached effect, null it, then kill the dummy unit and null that too. Why null a struct's members? Because we're writing a spell. This may have 30 instances going at once at one point, and never reach that again (consider WTF mode in DotA). If that's the case, these values that aren't nulled will indeed leak a handle. After we dispose of this, we need to call KT_Add, with the function LaunchParticles, the same InstanceData and the same period. So we switch the code that is firing. So we insert:
JASS:
call DestroyEffect(d.balleffect)
set d.balleffect=null
call KillUnit(d.balldummy)
set d.balldummy=null
call KT_Add(function LaunchParticles, d, 0.03125)

So all together:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"
    private constant real BALLSCALEINCREMENT=0.04
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
    real direction
endstruct

private function LaunchParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    // Launch a particle
    return false
endfunction

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    if GetUnitCurrentOrder(d.caster)!=d.orderid then
        call DestroyEffect(d.balleffect)
        set d.balleffect=null
        call KillUnit(d.balldummy)
        set d.balldummy=null
        call KT_Add(function LaunchParticles, d, 0.03125)
        return true
    endif
    set d.ballscale=d.ballscale+BALLSCALEINCREMENT
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    local location targetpoint=GetSpellTargetLoc()
    set d.caster=GetSpellAbilityUnit()
    // Get spell direction. (In radians.)
    set d.direction=Atan2(GetLocationY(targetpoint)-GetUnitY(d.caster),GetLocationX(targetpoint)-GetUnitX(d.caster))
    call RemoveLocation(targetpoint)
    set targetpoint=null
    // End get spell direction.
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(d.direction)*64.0, GetUnitY(d.caster)+Sin(d.direction)*64.0, d.direction*bj_RADTODEG)
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
Now, we need to design a particle. We want a stream of particles to shoot out from our ball, damaging units they pass through. So, let's make a BeamParticle struct. It needs to know its velocity in units per second as a vector, and how much damage it should deal to units it meets along the way. It also needs to know what units it has already hit, so that one particle does not hit a unit more than once. It also needs to know how far it has to travel. This means it needs to know how fast it is travelling to it can subtract this from how far it has left on each tick. How fast it is travelling is the square root of (vector.x*vector.x + vector.y*vector.y). We could calculate this each time we move the particle, but it is more efficient not to. Remember, we could easily have 20 particles or more going at once. We also, of course, need a dummy unit and attached effect. In addition, we only want enemy units to be hit, and want the damage to come from the caster. So we need to know the unit that cast the spell. Because in our code we'll probably refer to the x and y position of the particle, we should also store these in the struct. So our struct looks like this:
JASS:
private struct BeamParticle
    unit particledummy
    effect particleeffect
    unit caster
    group hitunits
    real damage
    real x
    real y
    real xvel
    real yvel
    real distanceleft
    real speed
endstruct

Consider that our handling of a beam particle is irrelevant to the rest of the spell so far. Think of it in it's own right, what a single particle needs to do once it is launched. Our spell will simply launch a number of these, and then leave these to do exactly what they do. So let's write another KT2 function for handling a single particle, without worrying about how it will start. It's pretty complicated on it's own. Let's start with the KT2 shell.
JASS:
private function BeamParticleMotion takes nothing returns boolean
    local BeamParticle d=KT_GetData()
    // Do things.
    return false
endfunction

Notice that the local data type is a BeamParticle, not an InstanceData? Good.

Now... Most obviously we need to move the particle by it's xvel and yvel...
JASS:
set d.x=d.x+d.xvel
set d.y=d.y+d.yvel
set d.distanceleft=d.distanceleft-d.speed
call SetUnitX(d.particledummy,d.x)
call SetUnitY(d.particledummy,d.y)

Then deal damage... (Let's just handle this separately, ok? You won't be able to save until we define this function, though.)
JASS:
call ParticleDamage(d.caster, d.x, d.y, d.damage, d.hitunits)

And then check to see if the particle is finished with. If it is, dispose of it by destroying/nulling the dummy unit, the effect and the group, null the casting unit, and then destroying the particle struct and stopping it's KT2 instance. So...
JASS:
if d.distanceleft<=0.0 then
    call DestroyEffect(d.particleeffect)
    set d.particleeffect=null
    call KillUnit(d.particledummy)
    set d.particledummy=null
    call DestroyGroup(d.hitunits)
    set d.hitunits=null
    set d.caster=null
    call d.destroy()
    return true
endif

Yay! Now we just need to handle the damage stuff. Here's the shell of that function.
JASS:
private function ParticleDamage takes unit c, real x, real y, real damage, group hit returns nothing
endfunction

So all together now:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"
    private constant real BALLSCALEINCREMENT=0.04
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct BeamParticle
    unit particledummy
    effect particleeffect
    unit caster
    group hitunits
    real damage
    real x
    real y
    real xvel
    real yvel
    real distanceleft
    real speed
endstruct

private function ParticleDamage takes unit c, real x, real y, real damage, group hit returns nothing
endfunction

private function BeamParticleMotion takes nothing returns boolean
    local BeamParticle d=KT_GetData()
    set d.x=d.x+d.xvel
    set d.y=d.y+d.yvel
    set d.distanceleft=d.distanceleft-d.speed
    call SetUnitX(d.particledummy,d.x)
    call SetUnitY(d.particledummy,d.y)
    call ParticleDamage(d.caster, d.x, d.y, d.damage, d.hitunits)
    if d.distanceleft<=0.0 then
        call DestroyEffect(d.particleeffect)
        set d.particleeffect=null
        call KillUnit(d.particledummy)
        set d.particledummy=null
        call DestroyGroup(d.hitunits)
        set d.hitunits=null
        call d.destroy()
        return true
    endif
    return false
endfunction

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
    real direction
endstruct

private function LaunchParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    // Launch a particle
    return false
endfunction

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    if GetUnitCurrentOrder(d.caster)!=d.orderid then
        call DestroyEffect(d.balleffect)
        set d.balleffect=null
        call KillUnit(d.balldummy)
        set d.balldummy=null
        call KT_Add(function LaunchParticles, d, 0.03125)
        return true
    endif
    set d.ballscale=d.ballscale+BALLSCALEINCREMENT
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    local location targetpoint=GetSpellTargetLoc()
    set d.caster=GetSpellAbilityUnit()
    // Get spell direction. (In radians.)
    set d.direction=Atan2(GetLocationY(targetpoint)-GetUnitY(d.caster),GetLocationX(targetpoint)-GetUnitX(d.caster))
    call RemoveLocation(targetpoint)
    set targetpoint=null
    // End get spell direction.
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(d.direction)*64.0, GetUnitY(d.caster)+Sin(d.direction)*64.0, d.direction*bj_RADTODEG)
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
At this point it should once again save without syntax errors. We need a temp group for holding all the units in range of the particle. Instead of creating/destroying this group each time, let's make it a global that gets reused. Remember, this could be executing something like 20x32 (that's 640) times a second (or more), and probably will be. That's why must use KT2, and that's why we must be mad about efficiency. So above this shell, add:
JASS:
globals
    private constant group DamageGroup=CreateGroup()
endglobals

Now we add a shell for a basic "for each unit in range" loop. So for our ParticleDamage function we now have...
JASS:
globals
    private constant group DamageGroup=CreateGroup()
endglobals
private function ParticleDamage takes unit c, real x, real y, real damage, group hit returns nothing
    local unit u
    call GroupEnumUnitsInRange(DamageGroup, x, y, 96.0, null)
    loop
        set u=FirstOfGroup(DamageGroup)
        exitwhen u==null
        call GroupRemoveUnit(DamageGroup,u)
        // Do things involving u, which is now enum unit.
    endloop
endfunction

WAIT, I DON'T GET IT!
Calm down. The loop isn't that complicated. First, you fill the empty DamageGroup variable with units within 96.0 of the particle.
JASS:
call GroupEnumUnitsInRange(DamageGroup, x, y, 96.0, null)

Then, you have a loop that takes units out of the group until the group is empty.
JASS:
set u=FirstOfGroup(DamageGroup)
exitwhen u==null
call GroupRemoveUnit(DamageGroup,u)

Then you do things to each unit, which is u. The end. This doesn't yet filter out units that have been hit, or allied units.
Now we need to make sure we don't hit units more than once.
JASS:
if not IsUnitInGroup(u,hit) then
    // Do things involving u, which is now enum unit which hasn't been hit yet.
endif

Then, if it is an enemy, let's DAMAGE it! That sounds FUN. :D MUHUHAHAHAHAHAHA. Imagine all the little particles ripping through the poor unit. NYAHAHAHAAAAA! This is done with a traditional call to the following which will deal spell damage to a target, from a source:
JASS:

And this needs to only happen if the unit is an enemy of the owner of the caster. And then we can add the unit to the "hit" group regardless of whether it actually took damage, as a little efficiency trick. (Getting into efficiency, yet?) So our ParticleDamage is now:
JASS:
globals
    private constant group DamageGroup=CreateGroup()
endglobals
private function ParticleDamage takes unit c, real x, real y, real damage, group hit returns nothing
    local unit u
    call GroupEnumUnitsInRange(DamageGroup, x, y, 96.0, null)
    loop
        set u=FirstOfGroup(DamageGroup)
        exitwhen u==null
        call GroupRemoveUnit(DamageGroup,u)
        if not IsUnitInGroup(u,hit) then
            if IsUnitEnemy(u,GetOwningPlayer(c)) then
                call UnitDamageTarget(c,u,damage,true,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,WEAPON_TYPE_WHOKNOWS)
            endif
            call GroupAddUnit(hit,u)
        endif
    endloop
endfunction

So altogether now:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"
    private constant real BALLSCALEINCREMENT=0.04
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct BeamParticle
    unit particledummy
    effect particleeffect
    unit caster
    group hitunits
    real damage
    real x
    real y
    real xvel
    real yvel
    real distanceleft
    real speed
endstruct

globals
    private constant group DamageGroup=CreateGroup()
endglobals
private function ParticleDamage takes unit c, real x, real y, real damage, group hit returns nothing
    local unit u
    call GroupEnumUnitsInRange(DamageGroup, x, y, 96.0, null)
    loop
        set u=FirstOfGroup(DamageGroup)
        exitwhen u==null
        call GroupRemoveUnit(DamageGroup,u)
        if not IsUnitInGroup(u,hit) then
            if IsUnitEnemy(u,GetOwningPlayer(c)) then
                call UnitDamageTarget(c,u,damage,true,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,WEAPON_TYPE_WHOKNOWS)
            endif
            call GroupAddUnit(hit,u)
        endif
    endloop
endfunction

private function BeamParticleMotion takes nothing returns boolean
    local BeamParticle d=KT_GetData()
    set d.x=d.x+d.xvel
    set d.y=d.y+d.yvel
    set d.distanceleft=d.distanceleft-d.speed
    call SetUnitX(d.particledummy,d.x)
    call SetUnitY(d.particledummy,d.y)
    call ParticleDamage(d.caster, d.x, d.y, d.damage, d.hitunits)
    if d.distanceleft<=0.0 then
        call DestroyEffect(d.particleeffect)
        set d.particleeffect=null
        call KillUnit(d.particledummy)
        set d.particledummy=null
        call DestroyGroup(d.hitunits)
        set d.hitunits=null
        set d.caster=null
        call d.destroy()
        return true
    endif
    return false
endfunction

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
    real direction
endstruct

private function LaunchParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    // Launch a particle
    return false
endfunction

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    if GetUnitCurrentOrder(d.caster)!=d.orderid then
        call DestroyEffect(d.balleffect)
        set d.balleffect=null
        call KillUnit(d.balldummy)
        set d.balldummy=null
        call KT_Add(function LaunchParticles, d, 0.03125)
        return true
    endif
    set d.ballscale=d.ballscale+BALLSCALEINCREMENT
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    local location targetpoint=GetSpellTargetLoc()
    set d.caster=GetSpellAbilityUnit()
    // Get spell direction. (In radians.)
    set d.direction=Atan2(GetLocationY(targetpoint)-GetUnitY(d.caster),GetLocationX(targetpoint)-GetUnitX(d.caster))
    call RemoveLocation(targetpoint)
    set targetpoint=null
    // End get spell direction.
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, GetUnitX(d.caster)+Cos(d.direction)*64.0, GetUnitY(d.caster)+Sin(d.direction)*64.0, d.direction*bj_RADTODEG)
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
We have how written the means for processing a particle. Now we just need to launch them. Back to our little LaunchParticles function we already wrote the shell for.
JASS:
private function LaunchParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    // Launch a particle
    return false
endfunction

We will need the x/y of the ball, after it is destroyed. This means adding it to the InstanceData...
JASS:

And the OnCast function (and we get to neaten our d.balldummy initialising)...
JASS:
set d.x=GetUnitX(d.caster)+Cos(d.direction)*64.0
set d.y=GetUnitY(d.caster)+Sin(d.direction)*64.0
set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, d.x, d.y, d.direction*bj_RADTODEG)

Back to our LaunchParticles function...
We need to declare a local BeamParticle variable, and initialise it with a new instance. We need a dummy unit (and some flying height would be great on it) with an attached effect, we need to set the caster, set the hitunits to a new group, set the damage per particle, set the xvel and yvel and speed, and intialise the x and y position and the distance left. Then we want to attach it to KT2 with the BeamParticleMotion code to fire. So that's...
JASS:
private function LaunchParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    local BeamParticle p=BeamParticle.create()
    local real direction=d.direction
    set p.x=d.x
    set p.y=d.y
    set p.particledummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, p.x, p.y, d.direction*bj_RADTODEG)
    call SetUnitFlyHeight(p.particledummy, 70.0, 0)
    set p.particleeffect=AddSpecialEffectTarget(PARTICLEMODEL, p.particledummy, "origin")
    set p.caster=d.caster
    set p.hitunits=CreateGroup()
    set p.damage=5.0 // Let's say 5, for now.
    set p.speed=10.0 // Just to see what it's like.
    set p.xvel=p.speed*Cos(direction)
    set p.yvel=p.speed*Sin(direction)
    set p.distanceleft=600.0 // Just to see what it's like.
    call KT_Add(function BeamParticleMotion, p, 0.03125)
    return false
endfunction

We'd also like it to stop some time. Let's use the ballscale variable, and decrement that by the amount it increased until it runs out. This means it will launch particles for as long as it took to create the ball. When it runs out, we need to finish the InstanceData. So that means destroying and nulling the balleffect and balldummy, and nulling the caster.

Continued here.
 

Attachments

  • Expert Spellcrafting with KT2 and GT.zip
    88.9 KB · Views: 236

XeNiM666

I lurk for pizza
Reaction score
138
I haven't read the whole tutorial yet, just read the parts where you put the codes and I have questions.

1. The tutorial is long and ( for me ) is complicated.
2. Why does onCast function return boolean?

FYI, great tut!
 

Trollvottel

never aging title
Reaction score
262
I haven't read the whole tutorial yet, just read the parts where you put the codes and I have questions.

1. The tutorial is long and ( for me ) is complicated.
2. Why does onCast function return boolean?

FYI, great tut!

1. Its Expert Spellcrafting :p
2. He doesnt use TriggerActions but TriggerConditions as Action. So he has to return a boolean and if he returns false the trigger is never ran.

I like this tutorial because its a good reference for not so experienced jassers to become better.
 

Kenny

Back for now.
Reaction score
202
So far a pretty interesting tutorial. I've read through everything in your post, just not the attached file.

Maybe continue the tutorial in your next post.

GTrigger makes initializing basic spells so easy :D haha.

And the spell is looking quite interesting.

2. Why does onCast function return boolean?

It returns a boolean so that we can use it as a condition (which needs to return boolean values). Conditions are considered to be safer to use than actions, thats why we go through the trouble of making our action functions use that condition "shell" as Jesus4Kyf would call it.

Nice tut!

EDIT:

Just read through the rest of the tut (i wish there was a syntax highlighter or whatever for textedit). It seemed pretty in depth; describing most things quite well.

I dont have wc3 on my laptop, but from reading the scripts its pretty clear what the spell does, and it seems nice. Well done!

You added particle absorption to your final product at the end of the tut :D
 

wraithseeker

Tired.
Reaction score
122
Looks awesome judging from the amount of time you have contributed to this tutorial.

This spell seems alittle too complicated though.

Please attach the spell that you are making here to the attachment, so people can take a good look at the code altogether or see how it works.
 

Romek

Super Moderator
Reaction score
963
> teaching intermediate to experienced JASSers how to write very nice spells very quickly and very efficiently
Experienced Jassers already know this.
And it appears that this tutorial is too complex for the beginners (Or people who claim to be intermediate, but are far from it).

So it seems this tutorial doesn't have a clear audience.

I haven't actually read the tutorial yet. But those are judging from the comments.

Also, it seems you have a few spelling mistakes. A spell check wouldn't hurt. :)
 

Viikuna

No Marlo no game.
Reaction score
265
I quicky checked it and there was some things you could improve, like recycling that hitunits group and stuff like that.

Anywas, will probably read this more carefully later and post more comments.
 

Jesus4Lyf

Good Idea™
Reaction score
397
>Please attach the spell that you are making here to the attachment, so people can take a good look at the code altogether or see how it works.

You can see the final spell. It's at attached complete at the end of the text document. Simply copy the contents of it into the StartHere map in the MySpell trigger, and it should run straight off... If someone else wants to post the finished product as a map file here, I'm fine with that.

And Romek's thoughts about an obscure audience were my initial thoughts exactly.

PS. Any thoughts on the final spell this teaches you to make? :D
 

Sim

Forum Administrator
Staff member
Reaction score
534
Just post the rest of the tutorial. I will merge it with the original post.
 

Jesus4Lyf

Good Idea™
Reaction score
397
Doesn't that mean I won't ever be able to edit the post? o_O

-- The rest --
JASS:
set d.ballscale=d.ballscale-BALLSCALEINCREMENT
if d.ballscale<0.0 then
    call DestroyEffect(d.balleffect)
    set d.balleffect=null
    call KillUnit(d.balldummy)
    set d.balldummy=null
    set d.caster=null
    return true
endif

All together now:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"
    private constant real BALLSCALEINCREMENT=0.04
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
endglobals

private struct BeamParticle
    unit particledummy
    effect particleeffect
    unit caster
    group hitunits
    real damage
    real x
    real y
    real xvel
    real yvel
    real distanceleft
    real speed
endstruct

globals
    private constant group DamageGroup=CreateGroup()
endglobals
private function ParticleDamage takes unit c, real x, real y, real damage, group hit returns nothing
    local unit u
    call GroupEnumUnitsInRange(DamageGroup, x, y, 96.0, null)
    loop
        set u=FirstOfGroup(DamageGroup)
        exitwhen u==null
        call GroupRemoveUnit(DamageGroup,u)
        if not IsUnitInGroup(u,hit) then
            if IsUnitEnemy(u,GetOwningPlayer(c)) then
                call UnitDamageTarget(c,u,damage,true,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,WEAPON_TYPE_WHOKNOWS)
            endif
            call GroupAddUnit(hit,u)
        endif
    endloop
endfunction

private function BeamParticleMotion takes nothing returns boolean
    local BeamParticle d=KT_GetData()
    set d.x=d.x+d.xvel
    set d.y=d.y+d.yvel
    set d.distanceleft=d.distanceleft-d.speed
    call SetUnitX(d.particledummy,d.x)
    call SetUnitY(d.particledummy,d.y)
    call ParticleDamage(d.caster, d.x, d.y, d.damage, d.hitunits)
    if d.distanceleft<=0.0 then
        call DestroyEffect(d.particleeffect)
        set d.particleeffect=null
        call KillUnit(d.particledummy)
        set d.particledummy=null
        call DestroyGroup(d.hitunits)
        set d.hitunits=null
        set d.caster=null
        call d.destroy()
        return true
    endif
    return false
endfunction

private struct InstanceData
    unit caster
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
    real direction
    real x
    real y
endstruct

private function LaunchParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    local BeamParticle p=BeamParticle.create()
    local real direction=d.direction
    set p.x=d.x
    set p.y=d.y
    set p.particledummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, p.x, p.y, d.direction*bj_RADTODEG)
    call SetUnitFlyHeight(p.particledummy, 70.0, 0)
    set p.particleeffect=AddSpecialEffectTarget(PARTICLEMODEL, p.particledummy, "origin")
    set p.caster=d.caster
    set p.hitunits=CreateGroup()
    set p.damage=5.0 // Let's say 5, for now.
    set p.speed=10.0 // Just to see what it's like.
    set p.xvel=p.speed*Cos(direction)
    set p.yvel=p.speed*Sin(direction)
    set p.distanceleft=600.0 // Just to see what it's like.
    call KT_Add(function BeamParticleMotion, p, 0.03125)
    set d.ballscale=d.ballscale-BALLSCALEINCREMENT
    if d.ballscale<0.0 then
        call DestroyEffect(d.balleffect)
        set d.balleffect=null
        call KillUnit(d.balldummy)
        set d.balldummy=null
        set d.caster=null
        return true
    endif
    return false
endfunction

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    if GetUnitCurrentOrder(d.caster)!=d.orderid then
        call DestroyEffect(d.balleffect)
        set d.balleffect=null
        call KillUnit(d.balldummy)
        set d.balldummy=null
        call KT_Add(function LaunchParticles, d, 0.03125)
        return true
    endif
    set d.ballscale=d.ballscale+BALLSCALEINCREMENT
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    local location targetpoint=GetSpellTargetLoc()
    set d.caster=GetSpellAbilityUnit()
    // Get spell direction. (In radians.)
    set d.direction=Atan2(GetLocationY(targetpoint)-GetUnitY(d.caster),GetLocationX(targetpoint)-GetUnitX(d.caster))
    call RemoveLocation(targetpoint)
    set targetpoint=null
    // End get spell direction.
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.x=GetUnitX(d.caster)+Cos(d.direction)*64.0
    set d.y=GetUnitY(d.caster)+Sin(d.direction)*64.0
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, d.x, d.y, d.direction*bj_RADTODEG)
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
Let's hit test.

Hm! Kinda laggy. Nothing too impressive yet, either, as all the particles just form into a white line. Way too many.

Let's multiple the particle emission period by 4, and make it subtract 4 times the ball size. So in ExpandBall...
JASS:
call KT_Add(function LaunchParticles, d, 0.125)

And in LaunchParticles...
JASS:
set d.ballscale=d.ballscale-BALLSCALEINCREMENT*4.0

And change the damage to something more satisfying...
JASS:
set p.damage=25.0 // Let's say 25, for now.

And let's hit test again.

Much better! Kind of cool. And yes I'm aware that since it has no cooldown you can channel it infinitely.

Let's make the spell instance remember what level of the spell you cast... So to the InstanceData add...
JASS:
integer level

And to OnCast add...
JASS:
set d.level=GetUnitAbilityLevel(d.caster,ABIL)

Let's take that damage and distance out and make it relative to your level, and let's make speed a constant. So in launch particles...
JASS:
set p.damage=DamagePerParticle(d.level)
set p.speed=PROJECTILESPEED*0.03125
set p.xvel=p.speed*Cos(direction)
set p.yvel=p.speed*Sin(direction)
set p.distanceleft=ParticleMaxDistance(d.level)

And at the top...
JASS:
private constant real PROJECTILESPEED=500.0

And...
JASS:
private constant function DamagePerParticle takes integer level returns real
    return 5.0+(level*20.0)
endfunction
private constant function MaxDistance takes integer level returns real
    return 600.0+(level*200.0)
endfunction

WAIT! CONSTANT FUNCTIONS??
Yes. Constant functions. All this means is these functions are not supposed to change any data when they're called, only return a value. In other words, it has no "side effects". An example of a side effect would be if it killed a unit, or changed the value of a global variable. Constant functions cannot call non-constant functions for this reason.
Let's also add...
JASS:
private constant function Spread takes integer level returns real
    return 10.0*bj_DEGTORAD
endfunction

And then in LaunchParticles, change "local real direction..." to...
JASS:
local real direction=d.direction+GetRandomReal(-Spread(d.level),Spread(d.level))

This gives it a random spread, which can be based on its level if you like. Also, just below, change "set p.distanceleft=..." to...
JASS:
set p.distanceleft=GetRandomReal(ParticleMaxDistance(d.level)/2.0, ParticleMaxDistance(d.level))

Hit test again. Suddenly our spell is quite impressive!

I should let you know now if you get a fatal error from casting it, it's because the particles are leaving the map boundary. I won't be going into how to fix that in this tutorial, as that is out of the scope. But I'll let you know that Vexorian has written a system specifically to deal with this issue.

You'll find this spell multi-instances very nicely and smoothly, without lag considering that each instance calls the particle move function hundreds of times a second. If you try hard enough, I'm sure you can make it lag. I did!

We really should make that 64.0 distance a constant, if you haven't already. Do so. If you don't know how by now, we have serious problems. Also, change that constant to 128.0.

So all together now:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant string BALLMODEL="Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"
    private constant real BALLSCALEINCREMENT=0.04
    private constant real DISTANCEFROMCASTER=128.0
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
    private constant real PROJECTILESPEED=500.0
endglobals
private constant function DamagePerParticle takes integer level returns real
    return 5.0+(level*20.0)
endfunction
private constant function ParticleMaxDistance takes integer level returns real
    return 600.0+(level*200.0)
endfunction
private constant function Spread takes integer level returns real
    return 10.0*bj_DEGTORAD
endfunction

private struct BeamParticle
    unit particledummy
    effect particleeffect
    unit caster
    group hitunits
    real damage
    real x
    real y
    real xvel
    real yvel
    real distanceleft
    real speed
endstruct

globals
    private constant group DamageGroup=CreateGroup()
endglobals
private function ParticleDamage takes unit c, real x, real y, real damage, group hit returns nothing
    local unit u
    call GroupEnumUnitsInRange(DamageGroup, x, y, 96.0, null)
    loop
        set u=FirstOfGroup(DamageGroup)
        exitwhen u==null
        call GroupRemoveUnit(DamageGroup,u)
        if not IsUnitInGroup(u,hit) then
            if IsUnitEnemy(u,GetOwningPlayer(c)) then
                call UnitDamageTarget(c,u,damage,true,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,WEAPON_TYPE_WHOKNOWS)
            endif
            call GroupAddUnit(hit,u)
        endif
    endloop
endfunction

private function BeamParticleMotion takes nothing returns boolean
    local BeamParticle d=KT_GetData()
    set d.x=d.x+d.xvel
    set d.y=d.y+d.yvel
    set d.distanceleft=d.distanceleft-d.speed
    call SetUnitX(d.particledummy,d.x)
    call SetUnitY(d.particledummy,d.y)
    call ParticleDamage(d.caster, d.x, d.y, d.damage, d.hitunits)
    if d.distanceleft<=0.0 then
        call DestroyEffect(d.particleeffect)
        set d.particleeffect=null
        call KillUnit(d.particledummy)
        set d.particledummy=null
        call DestroyGroup(d.hitunits)
        set d.hitunits=null
        set d.caster=null
        call d.destroy()
        return true
    endif
    return false
endfunction

private struct InstanceData
    unit caster
    integer level
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
    real direction
    real x
    real y
endstruct

private function LaunchParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    local BeamParticle p=BeamParticle.create()
    local real direction=d.direction+GetRandomReal(-Spread(d.level),Spread(d.level))
    set p.x=d.x
    set p.y=d.y
    set p.particledummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, p.x, p.y, d.direction*bj_RADTODEG)
    call SetUnitFlyHeight(p.particledummy, 70.0, 0)
    set p.particleeffect=AddSpecialEffectTarget(PARTICLEMODEL, p.particledummy, "origin")
    set p.caster=d.caster
    set p.hitunits=CreateGroup()
    set p.damage=DamagePerParticle(d.level)
    set p.speed=PROJECTILESPEED*0.03125
    set p.xvel=p.speed*Cos(direction)
    set p.yvel=p.speed*Sin(direction)
    set p.distanceleft=GetRandomReal(ParticleMaxDistance(d.level)/2.0, ParticleMaxDistance(d.level))
    call KT_Add(function BeamParticleMotion, p, 0.03125)
    set d.ballscale=d.ballscale-BALLSCALEINCREMENT*4.0
    if d.ballscale<0.0 then
        call DestroyEffect(d.balleffect)
        set d.balleffect=null
        call KillUnit(d.balldummy)
        set d.balldummy=null
        set d.caster=null
        return true
    endif
    return false
endfunction

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    if GetUnitCurrentOrder(d.caster)!=d.orderid then
        call DestroyEffect(d.balleffect)
        set d.balleffect=null
        call KillUnit(d.balldummy)
        set d.balldummy=null
        call KT_Add(function LaunchParticles, d, 0.125)
        return true
    endif
    set d.ballscale=d.ballscale+BALLSCALEINCREMENT
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    local location targetpoint=GetSpellTargetLoc()
    set d.caster=GetSpellAbilityUnit()
    // Get spell direction. (In radians.)
    set d.direction=Atan2(GetLocationY(targetpoint)-GetUnitY(d.caster),GetLocationX(targetpoint)-GetUnitX(d.caster))
    call RemoveLocation(targetpoint)
    set targetpoint=null
    // End get spell direction.
    set d.level=GetUnitAbilityLevel(d.caster,ABIL)
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.x=GetUnitX(d.caster)+Cos(d.direction)*DISTANCEFROMCASTER
    set d.y=GetUnitY(d.caster)+Sin(d.direction)*DISTANCEFROMCASTER
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, d.x, d.y, d.direction*bj_RADTODEG)
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
Now that you're an expert on using KT2, let's see if you can figure out what I've added...
And always remember, the difference between a good spell and a great spell is eye candy, and tweaking.
Try scrolling the mouse wheel! ;)

FINAL PRODUCT:
JASS:
scope MySpell initializer Init

globals
    private constant integer ABIL='A000'
    private constant integer DUMMYUNIT='u000'
    private constant real HEIGHT=35.0
    private constant string BALLMODEL="Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"
    private constant real BALLSCALEINCREMENT=0.04
    private constant real DISTANCEFROMCASTER=128.0
    private constant string PARTICLEMODEL="Abilities\\Weapons\\FaerieDragonMissile\\FaerieDragonMissile.mdl"
    private constant real PARTICLESCALE=0.4
    private constant real PROJECTILESPEED=512.0
    private constant string ABSORBMODEL="Abilities\\Spells\\Items\\OrbCorruption\\OrbCorruptionMissile.mdl"
    private constant real ABSORBSCALE=0.8
    private constant real ABSORBMAXOFFSET=256.0
endglobals
private constant function DamagePerParticle takes integer level returns real
    return 5.0+(level*20.0)
endfunction
private constant function ParticleMaxDistance takes integer level returns real
    return 600.0+(level*200.0)
endfunction
private constant function Spread takes integer level returns real
    return 10.0*bj_DEGTORAD
endfunction

private struct BeamParticle
    unit particledummy
    effect particleeffect
    unit caster
    group hitunits
    real damage
    real x
    real y
    real xvel
    real yvel
    real distanceleft
    real speed
endstruct

globals
    private constant group DamageGroup=CreateGroup()
endglobals
private function ParticleDamage takes unit c, real x, real y, real damage, group hit returns nothing
    local unit u
    call GroupEnumUnitsInRange(DamageGroup, x, y, 96.0, null)
    loop
        set u=FirstOfGroup(DamageGroup)
        exitwhen u==null
        call GroupRemoveUnit(DamageGroup,u)
        if not IsUnitInGroup(u,hit) then
            if IsUnitEnemy(u,GetOwningPlayer(c)) then
                call UnitDamageTarget(c,u,damage,true,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,WEAPON_TYPE_WHOKNOWS)
            endif
            call GroupAddUnit(hit,u)
        endif
    endloop
endfunction

private function BeamParticleMotion takes nothing returns boolean
    local BeamParticle d=KT_GetData()
    set d.x=d.x+d.xvel
    set d.y=d.y+d.yvel
    set d.distanceleft=d.distanceleft-d.speed
    call SetUnitX(d.particledummy,d.x)
    call SetUnitY(d.particledummy,d.y)
    call ParticleDamage(d.caster, d.x, d.y, d.damage, d.hitunits)
    if d.distanceleft<=0.0 then
        call DestroyEffect(d.particleeffect)
        set d.particleeffect=null
        call KillUnit(d.particledummy)
        set d.particledummy=null
        call DestroyGroup(d.hitunits)
        set d.hitunits=null
        set d.caster=null
        call d.destroy()
        return true
    endif
    return false
endfunction

private struct AbsorbParticle
    unit absorbdummy
    effect absorbeffect
    real x
    real y
    real z
    real speed
    real distleft
    real xvel
    real yvel
    real zvel
    integer fade
endstruct

private function AbsorbParticleMotion takes nothing returns boolean
    local AbsorbParticle d=KT_GetData()
    set d.x=d.x+d.xvel
    set d.y=d.y+d.yvel
    set d.z=d.z+d.zvel
    call SetUnitX(d.absorbdummy,d.x)
    call SetUnitY(d.absorbdummy,d.y)
    call SetUnitFlyHeight(d.absorbdummy,d.z,0)
    set d.distleft=d.distleft-d.speed
    if d.distleft<=0.0 then
        call DestroyEffect(d.absorbeffect)
        set d.absorbeffect=null
        call KillUnit(d.absorbdummy)
        set d.absorbdummy=null
        call d.destroy()
        return true
    endif
    return false
endfunction

private function AbsorbParticleFadeIn takes nothing returns boolean
    local AbsorbParticle d=KT_GetData()
    set d.fade=d.fade+32
    if d.fade>255 then
        call SetUnitVertexColor(d.absorbdummy,255,255,255,255)
        call KT_Add(function AbsorbParticleMotion,d,0.03125)
        return true
    endif
    call SetUnitVertexColor(d.absorbdummy,255,255,255,d.fade)
    return false
endfunction

private struct InstanceData
    unit caster
    integer level
    integer orderid
    unit balldummy
    effect balleffect
    real ballscale
    real direction
    real x
    real y
endstruct

private function LaunchAbsorbParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    local AbsorbParticle p=AbsorbParticle.create()
    set p.x=d.x+GetRandomReal(-ABSORBMAXOFFSET,ABSORBMAXOFFSET)
    set p.y=d.y+GetRandomReal(-ABSORBMAXOFFSET,ABSORBMAXOFFSET)
    set p.z=HEIGHT+GetRandomReal(-ABSORBMAXOFFSET,ABSORBMAXOFFSET)
    set p.distleft=SquareRoot((d.x-p.x)*(d.x-p.x)+(d.y-p.y)*(d.y-p.y)+(HEIGHT-p.z)*(HEIGHT-p.z))
    set p.speed=p.distleft*2.0*0.03125 // So time is 0.5 sec.
    set p.xvel=(d.x-p.x)/p.distleft*p.speed
    set p.yvel=(d.y-p.y)/p.distleft*p.speed
    set p.zvel=(HEIGHT-p.z)/p.distleft*p.speed
    set p.absorbdummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, p.x, p.y, Atan2(p.yvel,p.xvel)*bj_RADTODEG)
    call SetUnitFlyHeight(p.absorbdummy,p.z,0)
    call SetUnitScale(p.absorbdummy,ABSORBSCALE,ABSORBSCALE,ABSORBSCALE)
    set p.absorbeffect=AddSpecialEffectTarget(ABSORBMODEL,p.absorbdummy,"origin")
    call KT_Add(function AbsorbParticleFadeIn,p,0.03125)
    return GetUnitCurrentOrder(d.caster)!=d.orderid
endfunction

private function LaunchParticles takes nothing returns boolean
    local InstanceData d=KT_GetData()
    local BeamParticle p=BeamParticle.create()
    local real direction=d.direction+GetRandomReal(-Spread(d.level),Spread(d.level))
    set p.x=d.x
    set p.y=d.y
    set p.particledummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, p.x, p.y, d.direction*bj_RADTODEG)
    call SetUnitFlyHeight(p.particledummy, HEIGHT, 0)
    set p.particleeffect=AddSpecialEffectTarget(PARTICLEMODEL, p.particledummy, "origin")
    set p.caster=d.caster
    set p.hitunits=CreateGroup()
    set p.damage=DamagePerParticle(d.level)
    set p.speed=PROJECTILESPEED*0.03125
    set p.xvel=p.speed*Cos(direction)
    set p.yvel=p.speed*Sin(direction)
    set p.distanceleft=GetRandomReal(ParticleMaxDistance(d.level)/2.0, ParticleMaxDistance(d.level))
    call KT_Add(function BeamParticleMotion, p, 0.03125)
    set d.ballscale=d.ballscale-BALLSCALEINCREMENT*4.0
    if d.ballscale<0.0 then
        call DestroyEffect(d.balleffect)
        set d.balleffect=null
        call KillUnit(d.balldummy)
        set d.balldummy=null
        set d.caster=null
        return true
    endif
    return false
endfunction

private function ExpandBall takes nothing returns boolean
    local InstanceData d=KT_GetData()
    if GetUnitCurrentOrder(d.caster)!=d.orderid then
        call DestroyEffect(d.balleffect)
        set d.balleffect=null
        call KillUnit(d.balldummy)
        set d.balldummy=null
        call KT_Add(function LaunchParticles, d, 0.125)
        return true
    endif
    set d.ballscale=d.ballscale+BALLSCALEINCREMENT
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    return false
endfunction

private function OnCast takes nothing returns boolean
    local InstanceData d = InstanceData.create()
    local location targetpoint=GetSpellTargetLoc()
    set d.caster=GetSpellAbilityUnit()
    // Get spell direction. (In radians.)
    set d.direction=Atan2(GetLocationY(targetpoint)-GetUnitY(d.caster),GetLocationX(targetpoint)-GetUnitX(d.caster))
    call RemoveLocation(targetpoint)
    set targetpoint=null
    // End get spell direction.
    set d.level=GetUnitAbilityLevel(d.caster,ABIL)
    set d.orderid=GetUnitCurrentOrder(d.caster)
    set d.x=GetUnitX(d.caster)+Cos(d.direction)*DISTANCEFROMCASTER
    set d.y=GetUnitY(d.caster)+Sin(d.direction)*DISTANCEFROMCASTER
    set d.balldummy=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMYUNIT, d.x, d.y, d.direction*bj_RADTODEG)
    call SetUnitFlyHeight(d.balldummy,HEIGHT,0)
    set d.balleffect=AddSpecialEffectTarget(BALLMODEL, d.balldummy, "origin")
    set d.ballscale=0.01
    call SetUnitScale(d.balldummy, d.ballscale, d.ballscale, d.ballscale)
    call KT_Add(function ExpandBall, d, 0.03125)
    call KT_Add(function LaunchAbsorbParticles, d, 0.0625)
    return false
endfunction

private function Init takes nothing returns nothing
    call GT_AddStartsEffectAction(function OnCast, ABIL)
endfunction

endscope
You are now equipped to craft all your dream spells, using Key Timers 2 and GTrigger to make them an efficient and stable reality.
 

Sim

Forum Administrator
Staff member
Reaction score
534
Wow, I can't even merge it.

Your JASS codes are just too huge. :p
 

Romek

Super Moderator
Reaction score
963
You could've posted right after the first post. Would've been neater. Anyway, I stand by what I said before: There is no clear audience to this.

I suggest you dumb it down and explain more.
Also, a "Continued here" link in the first post would be nice. :)
 

GoGo-Boy

You can change this now in User CP
Reaction score
40
A pretty nice tutorial. Learned quite a bit further from it.
Why has this been graveyarded? In my opinion it is well written and really helps people that wanna work with GT and KT.
 

FlameSoul

New Member
Reaction score
0
Very good tutorial. Even touhgh I've never written anything in JASS I've been able to understand everything (I think knowing assembler C and C++ might have helped a lot, but still the tuto is really clear).
I think I will try doing this now onwards, because it is clearer to me and more flexible than the GUI spells I put so much effort in making a way around.
I will also make a question about this: how would you make a spell that is like: adding an animation to caster hand and when animation is fully grown throw "it" (with "it" preserving it's size). I think it is nearly the same to what is written in the example, but I've already made the dumb question anyways.
 
General chit-chat
Help Users
  • No one is chatting at the moment.

      The Helper Discord

      Staff online

      Members online

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top