• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[vJASS] Spells, structs, MUI, lots of stuff.

Status
Not open for further replies.
Level 2
Joined
Sep 21, 2013
Messages
22
So, recently I have started a thread regarding an issue with a spell I was making. Since then, I have essentially broken it beyond repair and decided to try to re-write it. So far, it's looking promising (I still have a LOT of optimization to do, but I like to get it to work before sweating the small stuff, something I didn't do before as I had no set path in mind for it).

The idea of the spell is simple. I lob a javelin which goes across the map and speeds up, increasing damage as it travels until it contacts a unit.

It's a little messy right now, but I will be improving it over time and hopefully get enough experience from this adventure to be able to create some truly interesting things and submit them to this website and hopefully get more involved in the community.

Here is the spell:
JASS:
scope MagicJavelin2 initializer Init
    
    private struct Data
        integer Ticks = 0
        unit Caster
        unit Dummy
        real Angle
        location CasterLoc
        
        static method create takes nothing returns thistype
            local thistype this = Data.allocate()
            local location CastLoc = GetSpellTargetLoc()
            
            set this.Caster = GetSpellAbilityUnit()
            set this.CasterLoc = GetUnitLoc(this.Caster)
            
            set this.Angle = AngleBetweenPoints(this.CasterLoc, CastLoc)
            
            call RemoveLocation(CastLoc)
            return this
        endmethod
        
        method destroy takes nothing returns nothing
            set .Caster = null
            set .Dummy = null
            call RemoveLocation(.CasterLoc)
        endmethod
    endstruct
    
    globals
        private Data array D
        private integer Total = 0
        private timer ticker = CreateTimer()
        private integer i
        private integer min_index = 0
    endglobals
    
    private function Finish takes nothing returns nothing
        if(i == min_index) then
            set min_index = min_index + 1
        endif
        call D[i].destroy()
        call PauseTimer(ticker)
    endfunction
    
    private function GroupActions takes nothing returns nothing
        call UnitDamageTargetBJ( D[i].Caster, GetEnumUnit(), ( ( 50.00 + ( 75.00 * ( I2R(GetUnitAbilityLevel(D[i].Caster, 'A002')) - 1 ) ) ) + ( ( ( 100.00 + ( 150.00 * ( I2R(GetUnitAbilityLevel(D[i].Caster, 'A002')) - 1 ) ) ) * 0.03 ) * D[i].Ticks ) ), ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL )
        call RemoveUnit( D[i].Dummy )
        call Finish()
    endfunction
    
    private function GroupConds takes nothing returns boolean
        return (IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) == false) and (IsUnitAliveBJ(GetFilterUnit()) == true) and (GetFilterUnit() != D[i].Dummy)
    endfunction
    
    private function Tick takes nothing returns nothing
        local location DummyLoc
        local location Offset
        local group g
        
        set i = 0
        loop
            set DummyLoc = GetUnitLoc(D[i].Dummy)
            set Offset = PolarProjectionBJ(DummyLoc, ( 20.00 + ( 0.10 * D[i].Ticks ) ), D[i].Angle)
            call SetUnitPositionLoc( D[i].Dummy, Offset )
            set g = GetUnitsInRangeOfLocMatching(60.00, Offset, Condition(function GroupConds))
            call RemoveLocation(DummyLoc)
            call RemoveLocation(Offset)
            call ForGroupBJ( g, function GroupActions )
            call DestroyGroup(g)
            set D[i].Ticks = D[i].Ticks + 1
            exitwhen i >= Total
            set i = i + 1
        endloop
    endfunction
    
    private function Actions takes nothing returns boolean
        local location Offset
        if(GetSpellAbilityId() == 'A002') then
            set D[Total] = Data.create()
            
            set Offset = PolarProjectionBJ(D[Total].CasterLoc, 100.00, D[Total].Angle)
            call CreateNUnitsAtLoc( 1, 'h001', GetOwningPlayer(D[Total].Caster), Offset, D[Total].Angle )
            set D[Total].Dummy = GetLastCreatedUnit()
            
            call RemoveLocation(Offset)
            
            if(Total == 0) then
                call TimerStart(ticker, 0.03, true, function Tick)
            endif
            return true
        endif
        return false
    endfunction

    //===========================================================================
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger(  )
        call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_SPELL_EFFECT )
        call TriggerAddCondition( t, Condition( function Actions ) )
        call TriggerAddAction( t, function Actions )
    endfunction

endscope

So there is the spell in all of its glory. Written in about 10-20 minutes as I had a path set in my mind. My current issues are these:

Functionality error: 2 dummies are created. One is on the caster, the other travels perfectly fine.
-So the spell works, but for some reason it creates a second dummy. I am 100% certain that it's not another trigger, so the problem lies here. From what I have seen it's because the function "Actions" is also the same function I use for its condition.

I believe I can fix this by simply moving the if/then statement away from all the actions and just storing a return there, but I'm not sure if that would actually work or just result in the spell not working at all. What is the standard for that? Removing the "TriggerAddAction" so that the trigger runs through the condition, or by doing what I said in the former.

Efficiency problem: Indexes are not recycled, but are destroyed.
-This problem is simply due to the fact that I don't know the best way to recycle indexes. Normally, this would be easy if a spell had a fixed amount of time it ran, but with a spell like this, if you cast it once, then one second later, the first one is not guaranteed to finish first, thus creating an issue. As such, it runs through ALL of the indexes ever used each time it is cast, even though previous ones had been destroyed.

I have started working around this issue by adding a minimum index (although I haven't fully implemented that yet), just so that it only runs through the index range I know is currently in-use. The problem I have then is just recycling the old indexes without breaking anything, seeing how it would be detrimental of the function of the ability if it was in the process of being cast and I reduced "Total" to re-use those older indexes. I am unsure on how to proceed with that.

Efficiency problem: Locations, polar projections, X & Ys.
-I have heard in the past that locations are inefficient. I would like to work around them if that is the case with X & Y locations. This is less of an issue as I have a general idea of how this would go, but I can't ever seem to get it right. This is something I feel I can research by myself, however, but any advice that may not be so obvious would be appreciated if any exist.

-------------

There are also lots of small issues (I pause the timer after one of the javelins finish, rather than after all of them. A problem I feel requires knowledge on recycling the indexes.) but I think I had highlighted the major ones I have ran into that I can't seem to find anything that elaborates on why it works. I hate copying if I can't understand it. I don't learn anything that way.

I would also like to apologize if I am a nuisance beforehand. 80% of the reason for this thread is to get some help and to understand these things better, and the other 20% is to understand my mistakes by trying to figure out what I may have done wrong.

Thanks ahead of time for any help.
 
Never use location unless you are trying to get something z value. coordinates are much faster and more efficient. Also easier to use. You should really work on getting rid of BJs and never using most of them again.

This does not work all the time
JASS:
(IsUnitAliveBJ(GetFilterUnit()) == true)
Also you should learn how to inline.

Get rid of this and don't use this
JASS:
call TriggerAddAction( t, function Actions )
or TriggerAddAction. (since you have TriggerAddCondition and have them both running the same function that function is running twice and can be messing up a few things.

You never increase Total so you are overwriting your data every time you are adding an instance. Check out my spells they use an indexed array method. That is what you should use. I also use polar projection without locations so you can see how to do the angles with X and Y values.
 
Level 2
Joined
Sep 21, 2013
Messages
22
Alright, thanks for the help :) As far as the BJs are concerned, like I said I haven't done any optimization on this yet and just want to get it working right now.

Anyway, you've answered my major questions fairly well, and I definitely will look at your spells. I would prefer not using any external indexing system just so I can learn how to do workarounds where need be (I don't have internet all the time, so sometimes I need to use workarounds).

As far as not increasing total, is it due to the fact that I have it initialized at 0? or is it similar to a local that it only will work for that instance specifically and just create separate instances per cast? (if the latter were the case then I imagine every spell written in vJass would be MUI)

Later on I will post my progress and request opinions on it on that part of development, or.. should I just submit it as a spell to the section on this website? The purpose of it is to better myself after all, rather than create something truly interesting.
 
As far as not increasing total, is it due to the fact that I have it initialized at 0? or is it similar to a local that it only will work for that instance specifically and just create separate instances per cast? (if the latter were the case then I imagine every spell written in vJass would be MUI)

Each index represents a separate instance of a spell. That is why you increment "Total". Once you get a new spell cast, it should take up a different slot of the array--otherwise you just end up overwriting 0 over and over again. When you need to do the actions, you simply loop through the array and then boom, you're done. Once a spell is finished, you simply destroy it, reduce Total by 1, and clean-up (including reassigning the last instance to the destroyed instance's place, so that it will loop properly).

Before you go blindly implementing it, you should understand what you're actually trying to accomplish. I recommend looking through this:
http://www.hiveworkshop.com/forums/...orials-279/visualize-dynamic-indexing-241896/
Even though it is for GUI, it is designed to help you understand: (1) how dynamic indexing works (2) spell "instances" and what they actually mean. After that, you can check out this tutorial to implement it in vJASS:
http://www.hiveworkshop.com/forums/...s-280/vjass-dynamic-indexing-tutorial-245956/
 
Level 2
Joined
Sep 21, 2013
Messages
22
oooh, thanks for those links! Those will come strongly in handy. At the moment, I've been experimenting with writing it using a library instead of a scope and just putting everything inside a struct, so I can index it differently that way. I've ran into the issue where it says "this is not a type that allows . syntax", which I'm unfamiliar with dealing with methods inside a struct for the line

" set DummyLoc = GetUnitLoc(this.Dummy)"

in the method "Tick." Here's what I have so far.

JASS:
library MagicJavelin2
  
    globals
        private integer Total = 0
        private timer ticker = CreateTimer()
        private integer i
        private integer min_index = 0
    endglobals
    
    private struct Data
        private integer Ticks = 0
        private unit Caster
        private unit Dummy
        private real Angle
        private location CasterLoc
        private real x
        private real y
        
        private method destroy takes nothing returns nothing
            set .Caster = null
            set .Dummy = null
            call RemoveLocation(.CasterLoc)
        endmethod
    
        private method Finish takes nothing returns nothing
            if(i == min_index) then
                set min_index = min_index + 1
            endif
            call this.destroy()
            call PauseTimer(ticker)
        endmethod
        
        private static method Tick takes nothing returns nothing
            local location DummyLoc
            local location Offset
            local group g
            local unit u
            
            set i = 0
            loop
                set DummyLoc = GetUnitLoc(this.Dummy)
                set Offset = PolarProjectionBJ(DummyLoc, ( 20.00 + ( 0.10 * this.Ticks ) ), this.Angle)
                call SetUnitPositionLoc( this.Dummy, Offset )
                set g = GroupEnumUnitsInRange(g, this.x, this.y, 60, null)
                loop
                    set u = FirstOfGroup(g)
                    exitwhen u == null
                    if(u != this.Dummy and u != UNIT_TYPE_STRUCTURE and not IsUnitType(u, UNIT_TYPE_DEAD) and u != this.Caster) then
                        call UnitDamageTargetBJ( this.Caster, u, ( ( 50.00 + ( 75.00 * ( I2R(GetUnitAbilityLevel(this.Caster, 'A002')) - 1 ) ) ) + ( ( ( 100.00 + ( 150.00 * ( I2R(GetUnitAbilityLevel(this.Caster, 'A002')) - 1 ) ) ) * 0.03 ) * this.Ticks ) ), ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL )
                        call RemoveUnit( this.Dummy )
                        call Finish()
                    endif
                    call GroupRemoveUnit(g, u)
                endloop
                call RemoveLocation(DummyLoc)
                call RemoveLocation(Offset)
                call DestroyGroup(g)
                set this.Ticks = this.Ticks + 1
                exitwhen i >= Total
                set i = i + 1
            endloop
        endmethod
        
        private static method create takes nothing returns thistype
            local thistype this = Data.allocate()
            local location CastLoc = GetSpellTargetLoc()
            
            set this.Caster = GetSpellAbilityUnit()
            set this.CasterLoc = GetUnitLoc(Caster)
            set this.x = GetUnitX(this.Caster)
            set this.y = GetUnitY(this.Caster)
            
            set this.Angle = AngleBetweenPoints(this.CasterLoc, CastLoc)
            
            call RemoveLocation(CastLoc)
            return this
        endmethod
        
        private static method Actions takes nothing returns boolean
            local thistype data
            local location Offset
            if(GetSpellAbilityId() == 'A002') then
                set this = thistype.create()
                
                set Offset = PolarProjectionBJ(this.CasterLoc, 100.00, this.Angle)
                call CreateNUnitsAtLoc( 1, 'h001', GetOwningPlayer(this.Caster), Offset, this.Angle )
                set this.Dummy = GetLastCreatedUnit()
                
                call RemoveLocation(Offset)
                
                if(Total == 0) then
                    call TimerStart(ticker, 0.03, true, function thistype.Tick)
                endif
            endif
            return false
        endmethod

        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger(  )
            call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_SPELL_EFFECT )
            call TriggerAddCondition( t, Condition( function thistype.Actions ) )
            set t = null
        endmethod
    endstruct
endlibrary

As far as "blindly" going into it, everything I have written is written for a purpose with my complete idea in mind (even though incorrect) on why/how it works, I'm just getting used to how everything processes the functions. I suppose it is pretty blind seeing how I am unsure on where it will end up. I've only been working with jass/vjass for roughly 3-4 days.
 
About the "blindly" comment–I didn't mean any offense by it, I just recommend reading those tutorials to get the general picture of things. :)

As for your issue, the problem is that "this" does not exist with static methods. static is a method that is not associated with an instance of the struct. For example:
JASS:
struct A
    static method staticMethod takes nothing returns nothing
    endmethod
    method regularMethod takes nothing returns nothing 
    endmethod
endstruct

// ... 
local A instance = A.create()

call A.staticMethod()    // correct
call A.regularMethod()    // incorrect

call instance.staticMethod()    // incorrect
call instance.regularMethod()    // correct

In this way, you can imagine static method and regular methods like so:
JASS:
// static method
function staticMethod takes nothing returns nothing 
endfunction

// regular method
function regularMethod takes A this returns nothing
endmethod

That is why you can use the this keyword in regular methods. However, for timers, groups, etc. the natives specify that the function cannot take any arguments. This is the whole reason behind the problem of data attachment, etc. It is the reason why we need to use stacks, lists, indexing, hashtables, etc. Blizzard didn't give us a way to pass data to a timer function.

As such, you'll have to revert to your old code (or write in new code), or you can use a system such as TimerUtils to ease the process.
 
Level 2
Joined
Sep 21, 2013
Messages
22
Ah, I see. Thanks a bunch! Yeah, I didn't take too much offense to the blindly comment, it was more for the fact that a very fair number of tutorials I have read have merely said "do this" without explaining why it worked like it did, so it made it more difficult for me to comprehend exactly where I should end-up and why I end up there.

With this information, I believe I can progress further. I may post back later if I run into another issue. It's just so much fun to see how people work around the problems in jass and to understand how everything works. :)

UPDATE: I have just noticed, for some reason I run into the issue "cannot convert nothing to group" with this function

JASS:
set g = GroupEnumUnitsInRange(g, this.x, this.y, 60, null)

it confuses me a bit, as I have seen a similar line in one of deathismyfriend's spells and it seems to work there. I assume the "null" is causing this issue. I have the group in a local variable, and even tried initializing it with CreateGroup() to see if that would fix the issue, and no luck. Is this potentially related to my version of JNGP?
 
The problem is pretty simple actually.
JASS:
GroupEnumUnitsInRange takes group whichGroup, real x, real y, real radius, boolexpr filter returns nothing
What the above means is that it returns nothing so you can't do set g =
Instead you need to do this.
JASS:
set g = CreateGroup() // Create the group
call GroupEnumUnitsInRange(g, this.x, this.y, 60, null) // Add units to the group in range.

When using JNGP you have the function list. It is a very useful list that shows you almost all of the available functions. Also if you hold ctrl and click the function name it brings up the function list with the clicked function.
 
Level 2
Joined
Sep 21, 2013
Messages
22
Can do! It might take me a bit to figure it out, but I will post when I get more of it down.

UPDATE: Alright, so it's too early to bump, so I will just edit this post.

So I have a fairly good understanding on how dynamic indexing works, and I was working with attempting to attach everything within the struct. Here is what I have thus far.

Fair warning: There is a lot of garbage code left in here from experimenting (Haven't yet removed all the BJs and switched out the locations with X/Ys.)

JASS:
library MagicJavelin2
  
    globals
        private integer Total = -1
        private timer ticker = CreateTimer()
    endglobals
    
    private struct Data
        private integer Ticks = 0
        private unit Caster
        private unit Dummy
        private real Angle
        private location CasterLoc
        private real x
        private real y
        static thistype array D
        
        private method destroy takes nothing returns nothing
            set .Caster = null
            set .Dummy = null
            call RemoveLocation(.CasterLoc)
            if(Total == -1) then
                call PauseTimer(ticker)
            endif
            call this.deallocate()
        endmethod
        
        private static method Tick takes nothing returns nothing
            local thistype this
            local location DummyLoc
            local location Offset
            local group g = CreateGroup()
            local unit u
            local integer i
            
            set i = 0
            loop
                exitwhen i > Total
                set this = D[i]
                set DummyLoc = GetUnitLoc(this.Dummy)
                set Offset = PolarProjectionBJ(DummyLoc, ( 20.00 + ( 0.10 * this.Ticks ) ), this.Angle)
                call SetUnitPositionLoc( this.Dummy, Offset )
                call GroupEnumUnitsInRange(g, this.x, this.y, 60, null)
                loop
                    set u = FirstOfGroup(g)
                    exitwhen u == null
                    call GroupRemoveUnit(g, u)
                    if(u != this.Dummy and u != UNIT_TYPE_STRUCTURE and not IsUnitType(u, UNIT_TYPE_DEAD) and u != this.Caster) then
                        call UnitDamageTargetBJ( this.Caster, u, ( ( 50.00 + ( 75.00 * ( I2R(GetUnitAbilityLevel(this.Caster, 'A002')) - 1 ) ) ) + ( ( ( 100.00 + ( 150.00 * ( I2R(GetUnitAbilityLevel(this.Caster, 'A002')) - 1 ) ) ) * 0.03 ) * this.Ticks ) ), ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL )
                        call RemoveUnit( this.Dummy )
                        set D[i] = D[Total]
                        set i = i - 1
                        set Total = Total - 1
                        call this.destroy()
                        call ForGroup(g, function GroupRemoveGroupEnum)
                    endif
                endloop
                call RemoveLocation(DummyLoc)
                call RemoveLocation(Offset)
                call DestroyGroup(g)
                set this.Ticks = this.Ticks + 1
                set i = i + 1
            endloop
        endmethod
        
        private static method create takes nothing returns thistype
            local thistype this = Data.allocate()
            local location CastLoc = GetSpellTargetLoc()
                
            set this.Caster = GetSpellAbilityUnit()
            set this.CasterLoc = GetUnitLoc(Caster)
            set this.x = GetUnitX(this.Caster)
            set this.y = GetUnitY(this.Caster)
                
            set this.Angle = AngleBetweenPoints(this.CasterLoc, CastLoc)
                
            call RemoveLocation(CastLoc)
            return this
        endmethod
            
        private static method Actions takes nothing returns boolean
            local thistype this
            local thistype data
            local location Offset
                
            if(GetSpellAbilityId() == 'A002') then
                set this = thistype.create()
                    
                set Offset = PolarProjectionBJ(this.CasterLoc, 100.00, this.Angle)
                call CreateUnit( GetOwningPlayer(this.Caster), 'h001', this.x, this.y, this.Angle )
                set this.Dummy = GetLastCreatedUnit()
                
                call RemoveLocation(Offset)
                set Total = Total + 1
                    
                if(Total == 0) then
                    call BJDebugMsg("Timer started")
                    call TimerStart(ticker, 0.03, true, function thistype.Tick)
                endif
                
                set D[Total] = this
                return true
            endif
            return false
        endmethod
        
        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger(  )
            call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_SPELL_EFFECT )
            call TriggerAddCondition( t, Condition( function thistype.Actions ) )
            set t = null
        endmethod
    endstruct
endlibrary

My main issue that I'm having is that the timer runs and the action ticks, plus I know it goes through the loop successfully, but the dummy does not move. I feel it's something to deal with the data not transferring to the Tick method properly, however, I'm not sure what I'm doing wrong here.

I believe this is something I can get working without placing everything inside of the struct itself, but I am experimenting a bit and would like to get an understanding on how to get it to work like this (It's been seemingly more convenient, but it's proving not to be at the moment until I can get this to work properly)
 
Last edited:
Use the BJDebugMsg to display messages to tell you what is and is not running. Also it can tell you what positions your unit is supposed to be in. ( You should remove the locations and use X and Y value before trying to debug the movement as you may mess it up when first learning x and y values instead of the locations)

This may be your problem. Remove this.
JASS:
call ForGroup(g, function GroupRemoveGroupEnum)
 
Level 2
Joined
Sep 21, 2013
Messages
22
I used BJDebugMsg to figure out exactly what's running, I suppose I should've mentioned that's how I found out that the loop was working in the first place, but I will try to project the locations that are being sent through that.

As for the ForGroup, that's unrelated to the movement of the dummy, so I wouldn't think of that as the reason as the timer still runs. I will post back later with what I come up with. I will also try to convert the locations to X + Ys.

Thanks for the help thus far, I will continue to try to keep in touch.
 
Remove the BJs. Use JassCraft or google (to eventually get to some jass pedia) to see how "those red-underlined" functions look inside and decide to perform actions by yourself or not. Why even giving chance to those BJs? Because some of them are just fine, thats why one should look at given function before actually changing the code.

E.g:
JASS:
function PolarProjectionBJ takes location source, real dist, real angle returns location
    local real x = GetLocationX(source) + dist * Cos(angle * bj_DEGTORAD)
    local real y = GetLocationY(source) + dist * Sin(angle * bj_DEGTORAD)
    return Location(x, y)
endfunction
Clearly, we should perform actions manually with real usage instead of working with this one. You still work a lot with locations while multiple ppl already pointed out that reals is what you should head for.

Do not use uber should names for scalars/arrays (the D array in this case) unless their meaning is clear. Change your workflow:
JASS:
// not accebtable, you would get fired from any team immidiately
// and adding /* */ is not an option here
call UnitDamageTargetBJ( this.Caster, u, ( ( 50.00 + ( 75.00 * ( I2R(GetUnitAbilityLevel(this.Caster, 'A002')) - 1 ) ) ) + ( ( ( 100.00 + ( 150.00 * ( I2R(GetUnitAbilityLevel(this.Caster, 'A002')) - 1 ) ) ) * 0.03 ) * this.Ticks ) ), ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL )

// correct (few options available, but you get the point
local real val = ( I2R(GetUnitAbilityLevel(this.Caster, 'A002')) - 1 )
local real r1 = 75*val
local real result = ( (50.00 + r1) + ( ( (100.00 + 2*r1) * 0.03 ) * this.Ticks ) )
call UnitDamageTarget( this.Caster, u, result, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS )
Don't mix . and this. syntax - you either use just . or this. in your method.

There are multiple tutorials in Tutorial section that might help you.
 
Level 2
Joined
Sep 21, 2013
Messages
22
--To Spinnaker--

I have already acknowledged the use of BJs and to not use them, they're just placeholders. I would never, ever in my life officially release a spell like this without changing the things you mentioned. The use of locations, however, is proving to be a problem with the functionality of the spell, so I changed that with some positive results.

BJ and workflow wise, what you are seeing are just remnants of when I've written the spell in GUI. Later I will set the damage formula and everything I want configurable as constants, so no need to worry about it. I'm just concerned about functionality.

Do not use uber should names for scalars/arrays (the D array in this case) unless their meaning is clear.

I'm afraid I don't quite follow this statement 100%. I assume it's to deal with the naming convention that just naming the array D is improper? I will fix that if that is the case. I will also stick with the this. syntax instead of the . syntax, thanks for letting me know.

I know it's asking a lot and it's very easy to redirect me to general tutorials on how to do these things, but general tutorials only will tell you so much. I don't learn very much from general tutorials and learn more from trying something and having someone tell me "this is wrong, that is wrong." If there were a major flaw in my code (say if I didn't understand leaks), it would be more useful to link me to a tutorial that explained leaks. (PurgeandFire's post for instance is a perfect example.) It's just a little difficult to pinpoint major issues in my code if I don't know what I should look for, that's why I'm here.

Besides, I've followed a lot of tutorials and that's how I'm even at this point in the first place :) There just aren't a lot of tutorials out there that cater to the way I learn things.

--Back to the spell--

I'm very, very close to getting this working. Changing out the locations with X + Ys definitely helped a lot, but I've ran into the issue where the dummy moves, but the model of the dummy does not. Everything else works fine. Perhaps there is something I missed in the dummy itself? Or maybe I have to update it's animation through a function every tick? It also spawns facing the wrong direction, even though the angle it travels is correct (which seems strange seeing how the angle it travels is equal to the angle it spawns facing, at least the values are.)

JASS:
library MagicJavelin2
  
    globals
        private integer Total = -1
        private timer ticker = CreateTimer()
    endglobals
    
    private struct Data
        private integer Ticks = 0
        private unit Caster
        private unit Dummy
        private real Angle
        private real x
        private real y
        static thistype array castData
        
        private static method PolarProjectionX takes real x, real dist, real angle returns real
            return x + dist * Cos(angle)
        endmethod

        private static method PolarProjectionY takes real y, real dist, real angle returns real
            return y + dist * Sin(angle)
        endmethod
        
        private static method AngleBetweenXY takes real x1, real y1, real x2, real y2 returns real
            return Atan2( y2 - y1, x2 - x1)
        endmethod 
    
        private method destroy takes nothing returns nothing
            set this.Caster = null
            set this.Dummy = null
            if(Total == -1) then
                call PauseTimer(ticker)
            endif
            call this.deallocate()
        endmethod
        
        private static method Tick takes nothing returns nothing
            local thistype this
            local real dX
            local real dY
            local group g = CreateGroup()
            local unit u
            local integer i
            
            set i = 0
            loop
                exitwhen i > Total
                set this = castData[i]
                
                if(this.Dummy == null and this.Ticks < 3) then
                    call BJDebugMsg("Dummy is null")
                endif
                
                set dX = this.PolarProjectionX(GetUnitX(this.Dummy), 20+(0.10*this.Ticks), this.Angle)
                set dY = this.PolarProjectionY(GetUnitY(this.Dummy), 20+(0.10*this.Ticks), this.Angle)
                call SetUnitX(this.Dummy, dX)
                call SetUnitY(this.Dummy, dY)
                
                call GroupEnumUnitsInRange(g, dX, dY, 60, null)
                loop
                    set u = FirstOfGroup(g)
                    exitwhen u == null
                    call GroupRemoveUnit(g, u)
                    if(u != this.Dummy and u != UNIT_TYPE_STRUCTURE and not IsUnitType(u, UNIT_TYPE_DEAD) and u != this.Caster) then
                        call UnitDamageTargetBJ( this.Caster, u, ( ( 50.00 + ( 75.00 * ( I2R(GetUnitAbilityLevel(this.Caster, 'A002')) - 1 ) ) ) + ( ( ( 100.00 + ( 150.00 * ( I2R(GetUnitAbilityLevel(this.Caster, 'A002')) - 1 ) ) ) * 0.03 ) * this.Ticks ) ), ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL )
                        call RemoveUnit( this.Dummy )
                        set castData[i] = castData[Total]
                        set i = i - 1
                        set Total = Total - 1
                        call this.destroy()
                        call ForGroup(g, function GroupRemoveGroupEnum)
                    endif
                endloop
                call DestroyGroup(g)
                set this.Ticks = this.Ticks + 1
                set i = i + 1
            endloop
        endmethod
        
        private static method create takes nothing returns thistype
            local thistype this = Data.allocate()
            local real CastLocX = GetSpellTargetX()
            local real CastLocY = GetSpellTargetY()
                
            set this.Caster = GetSpellAbilityUnit()
            set this.x = GetUnitX(this.Caster)
            set this.y = GetUnitY(this.Caster)
                
            set this.Angle = this.AngleBetweenXY(this.x, this.y, CastLocX, CastLocY)
            
            set this.x = this.PolarProjectionX(this.x, 100, this.Angle)
            set this.y = this.PolarProjectionY(this.y, 100, this.Angle)

            return this
        endmethod
            
        private static method Actions takes nothing returns boolean
            local thistype this
            local thistype data
            local location Offset
                
            if(GetSpellAbilityId() == 'A002') then
                set this = thistype.create()
                    
                set this.Dummy = CreateUnit( GetOwningPlayer(this.Caster), 'h001', this.x, this.y, this.Angle )
                
                if(this.Dummy == null) then
                    call BJDebugMsg("Dummy not set!")
                else
                    call BJDebugMsg("Dummy set.")
                endif
                
                set Total = Total + 1
                    
                if(Total == 0) then
                    call BJDebugMsg("Timer started")
                    call TimerStart(ticker, 0.03, true, function thistype.Tick)
                endif
                
                set castData[Total] = this
                return true
            endif
            return false
        endmethod
        
        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger(  )
            call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_SPELL_EFFECT )
            call TriggerAddCondition( t, Condition( function thistype.Actions ) )
            set t = null
        endmethod
    endstruct
endlibrary

Aside from the model staying behind, the shadow (and thus the dummy, although not the complete dummy) travels and hits targets how it's supposed to.
 
In order to move its model, you'll have to set the movement speed in the object editor to a value greater than 0. (That way, SetUnitX and SetUnitY will move the model as well). Alternatively, you can use SetUnitPosition(). However, that native is a bit slower because it performs pathing checks and issues a stop order to the dummy.

For dummy effects, you'll generally have the movement speed > 0. For dummy casters, you'll have the movement speed = 0 (so that it can cast on the target without having to change facing or w/e).
 
Level 2
Joined
Sep 21, 2013
Messages
22
This is removing all units in the group so only the first unit is ever picked.

I am aware, I want it to only hit 1 unit. The only issue is that it hits the first unit found in the group if there is more than 1 unit in the group rather than picks the closest one to the caster if a unit is found. Something I could fairly easily fix and probably will later on.

----------

For now, the spell functions almost perfectly, my only issue is with the initial spear spawn facing. I have to grab the facing from the caster rather than using the angle data I have set. For some reason, it won't read that, but I know it's set. Perhaps I could set it in a local variable before transferring it to the unit? It seems less efficient to do it that way for around the same result, so I don't find it that big of a deal.

Currently, everything works, it's MUI (to my knowledge, I haven't truly tested yet, but it looks to be MUI on paper.) It's very configurable, down to every numerical value. Anything that is was a raw number is in a constant. Complex math used is stored in a function (only speed and damage were used) to be easily modified (just in case I want initial damage to be the final damage instead of have it increase over time).

I've renamed the methods and variables to better fit standard convention, or to at least be more consistent with eachother. Changed any BJs I found unnecessary to their native function (Only the event and the ForGroup contain a BJ). Basically, I prettied it up a bit. I believe there's some more decomposition I can do to make it more readable and to reduce the sheer size of the functions, but at the moment it works fine.

Thanks to everyone for your help, you have all been a massive help. If there's anything that sticks out in particular, please do not hesitate to say. At the moment, I am planning on fixing that ForGroup to go for closest unit, but I'm not sure which direction I should go (if any) at the moment.

JASS:
library MagicJavelin2
    
    globals
    //Object IDs
        private constant integer    ABIL_ID         = 'A002'
        private constant integer    DUMMY_ID        = 'h001'
        
    //Time inbetween ticks. Set to 0.03125 for accuracy.
        private constant real       INTERVAL        = 0.03125
        
    //Initial dummyspawn distance from caster.
        private constant real       INITIAL_DIST    = 100
        
    //Initial travel speed per tick
        private constant real       INITIAL_SPEED   = 20.000
        
    //Additional travel speed per tick
        private constant real       SPEED_PER_TICK  = 0.0500
        
    //Damage at cast/minimum damage/damage before additional damage over time (at level 1).
        private constant real       INITIAL_DAMAGE  = 50
        
    //Damage added to total damage, per second.
        private constant real       INITIAL_DPS     = 25
        
    //Initial damage added, per level.
        private constant real       DAMAGE_PER_LVL  = 75
        
    //Additional damage per second, per level
        private constant real       DPS_PER_LVL     = 35
        
    //Hit radius/sensitivity of dummy.
        private constant real       HIT_RADIUS      = 60
        
    //Damage/Attack type information.
        private constant attacktype ATTACK_TYPE     = ATTACK_TYPE_PIERCE
        private constant damagetype DAMAGE_TYPE     = DAMAGE_TYPE_NORMAL
        
    //Minimum + Maximum X + Y before the dummy is removed from the map.
        private constant real       MIN_X           = -3200
        private constant real       MIN_Y           = -3200
        private constant real       MAX_X           = 3000
        private constant real       MAX_Y           = 3000
        
    //Indexing variable & Timer. Do not touch.
        private          integer    Total           = -1
        private          timer      ticker          = CreateTimer()
    endglobals
    
    /*************************************************************************************************/    
    private function CalculateSpeed takes integer numberOfTicks returns real                       /**/
        return INITIAL_SPEED + (SPEED_PER_TICK * numberOfTicks)                                    /**/
    endfunction                                                                                    /**/
    /********************************Configurable Functions*******************************************/
    private function CalculateDamage takes integer abilityLevel, integer numberOfTicks returns real/**/
        local real initDamage = INITIAL_DAMAGE + (DAMAGE_PER_LVL * abilityLevel)                   /**/
        local real damagePerTick = (INITIAL_DPS + (DPS_PER_LVL * abilityLevel)) * INTERVAL         /**/
                                                                                                   /**/
        return initDamage + (damagePerTick * numberOfTicks)                                        /**/
    endfunction                                                                                    /**/
    /*************************************************************************************************/

/*********************************************************************************************************/
/**/                                    /*Spell Data*/                                                 /**/
/*********************************************************************************************************/
    private struct Data
        private integer ticks = 0
        private unit caster
        private unit dummy
        private real angle
        private real x
        private real y
        static thistype array castData
        
        private static method polarProjectionX takes real x, real dist, real angle returns real
            return x + dist * Cos(angle)
        endmethod

        private static method polarProjectionY takes real y, real dist, real angle returns real
            return y + dist * Sin(angle)
        endmethod
        
        private static method angleBetweenXY takes real x1, real y1, real x2, real y2 returns real
            return Atan2( y2 - y1, x2 - x1)
        endmethod 
    
        private method destroy takes nothing returns nothing
            call RemoveUnit(this.dummy)
            set this.caster = null
            set this.dummy = null
            if(Total == -1) then
                call PauseTimer(ticker)
            endif
            call this.deallocate()
        endmethod
        
        private static method tick takes nothing returns nothing
            local thistype this
            local real dX
            local real dY
            local group g = CreateGroup()
            local unit u
            local integer i
            local integer lvl
            
            set i = 0
            loop
                exitwhen i > Total
                set this = castData[i]
                
                set lvl = GetUnitAbilityLevel(this.caster, ABIL_ID) - 1
                
                set dX = this.polarProjectionX(GetUnitX(this.dummy), CalculateSpeed(this.ticks), this.angle)
                set dY = this.polarProjectionY(GetUnitY(this.dummy), CalculateSpeed(this.ticks), this.angle)
                call SetUnitX(this.dummy, dX)
                call SetUnitY(this.dummy, dY)
                
                call GroupEnumUnitsInRange(g, dX, dY, 60, null)
                loop
                    set u = FirstOfGroup(g)
                    exitwhen u == null
                    call GroupRemoveUnit(g, u)
                    if(u != this.dummy and u != UNIT_TYPE_STRUCTURE and not IsUnitType(u, UNIT_TYPE_DEAD) and u != this.caster) then
                        call UnitDamageTarget(this.caster, u, CalculateDamage(lvl, this.ticks), true, true, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE_WHOKNOWS)
                        set castData[i] = castData[Total]
                        set i = i - 1
                        set Total = Total - 1
                        call this.destroy()
                        call ForGroup(g, function GroupRemoveGroupEnum)
                    endif
                endloop
                if((dX < MIN_X) or (dX > MAX_X) or (dY < MIN_Y) or (dY > MAX_Y)) then
                    set castData[i] = castData[Total]
                    set i = i - 1
                    set Total = Total - 1
                    call this.destroy()
                endif
                set this.ticks = this.ticks + 1
                set i = i + 1
            endloop
            call DestroyGroup(g)
        endmethod
        
        private static method create takes nothing returns thistype
            local thistype this = Data.allocate()
            local real CastLocX = GetSpellTargetX()
            local real CastLocY = GetSpellTargetY()
                
            set this.caster = GetSpellAbilityUnit()
            set this.x = GetUnitX(this.caster)
            set this.y = GetUnitY(this.caster)
                
            set this.angle = this.angleBetweenXY(this.x, this.y, CastLocX, CastLocY)
            
            set this.x = this.polarProjectionX(this.x, INITIAL_DIST, this.angle)
            set this.y = this.polarProjectionY(this.y, INITIAL_DIST, this.angle)

            return this
        endmethod
            
        private static method actions takes nothing returns boolean
            local thistype this
            local thistype data
                
            if(GetSpellAbilityId() == ABIL_ID) then
                set this = thistype.create()
                    
                set this.dummy = CreateUnit( GetOwningPlayer(this.caster), DUMMY_ID, this.x, this.y, GetUnitFacing(this.caster) )
                
                set Total = Total + 1
                    
                if(Total == 0) then
                    call TimerStart(ticker, INTERVAL, true, function thistype.tick)
                endif
                
                set castData[Total] = this
            endif
            return false
        endmethod
        
        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger(  )
            call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_SPELL_EFFECT )
            call TriggerAddCondition( t, Condition( function thistype.actions ) )
            set t = null
        endmethod
    endstruct
endlibrary

I have learned so much from everyone and hope to continue learning :) I feel learning how to make this ability truly will allow me to make just about anything.
 
About the angle issue: Atan2 returns an angle in radians. As such, when you input it into CreateUnit, you'll end up facing the wrong way because that function expects degrees. :)

To fix it, simply convert the angle to degrees when you put it into CreateUnit():
JASS:
set this.dummy = CreateUnit( GetOwningPlayer(this.caster), DUMMY_ID, this.x, this.y, this.angle * bj_RADTODEG )

Multiplying an angle in radians by bj_RADTODEG will convert it to degrees. Multiplying an angle in degrees by bj_DEGTORAD will convert it to radians (rad2deg is just 180/pi, and deg2rad is pi/180).
 
Status
Not open for further replies.
Top