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.
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.
So all together now:
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...
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:
ABIL being our spell id constant, and OnCast being the function we want executed. So all together now:
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?
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:
All together:
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:
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:
to our InstanceData struct. All together now:
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.
WOAH! What the hell is that create unit line?
Still with me? Sweet. If you got a little lost, here it is, all together:
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.
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:
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:
WHY KEY TIMERS 2?
So, all together now:
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...
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:
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:
Looking good! All together:
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:
And we add to our struct this:
Now let's change that terrible model! Just change the constant.
So all together now, after our first round of testing:
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:
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:
So all together:
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:
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.
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...
Then deal damage... (Let's just handle this separately, ok? You won't be able to save until we define this function, though.)
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...
Yay! Now we just need to handle the damage stuff. Here's the shell of that function.
So all together now:
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:
Now we add a shell for a basic "for each unit in range" loop. So for our ParticleDamage function we now have...
WAIT, I DON'T GET IT!
Now we need to make sure we don't hit units more than once.
Then, if it is an enemy, let's DAMAGE it! That sounds FUN. 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:
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:
So altogether now:
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.
We will need the x/y of the ball, after it is destroyed. This means adding it to the InstanceData...
And the OnCast function (and we get to neaten our d.balldummy initialising)...
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...
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.
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:
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:
So all together now:
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=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
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.
JASS:
All together:
JASS:
scope MySpell initializer Init
globals
private constant integer ABIL=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
JASS:
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
JASS:
scope MySpell initializer Init
globals
private constant integer ABIL=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
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.
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...
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.
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.
JASS:
scope MySpell initializer Init
globals
private constant integer ABIL=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
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:
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.
JASS:
scope MySpell initializer Init
globals
private constant integer ABIL=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
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=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
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=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
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=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
JASS:
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=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
JASS:
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:
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:
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:
So all together now:
JASS:
scope MySpell initializer Init
globals
private constant integer ABIL=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
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.
Then, you have a loop that takes units out of the group until the group is empty.
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.
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.
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. 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:
call UnitDamageTarget(whichUnit,target,damage,true,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,WEAPON_TYPE_WHOKNOWS)
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=039;A000039;
private constant integer DUMMYUNIT=039;u000039;
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
JASS:
We will need the x/y of the ball, after it is destroyed. This means adding it to the InstanceData...
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.