• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Deathball v 1.0b

Moin moin =)

So here we go: This is my first spell in vJass. Actually I never create create any good spell in GUI or Jass, so this is my first, longer spell at all^^
I have to say, it's nothing special, but for me it's something special, because I never did something like that before and that's why I want upload my spell here, because here I can get good critic, tips and help how to improve this spell and how I can work on my next spells, so here we go!


The Spell: Deathball
The caster creates a Deathball with dark energy. This ball will move forward for 10 seconds. Nearly every second, the ball release some of his dark power, damage nearby enemies and drains mana from them ( if they have )

Level 1: 50 damage, 25 mana - 250 AOE
Level 2: 100 damage, 50 mana - 275 AOE
Level 3: 150 damage, 75 mana - 300 AOE
Level 4: 200 damage, 100 mana - 325 AOE
1) I make damage, drained mana and AOE with arrays for better configure.
2) You can choose, if the ball should destroy trees and the range of destroying trees.
3) If the ball is out of the map, the spell ends immediately ( no game crush then )

Special thanks to:
watermelon_1234: He was and still is my vJass teacher, I learned so many things from him and I'm really happy, that he took the time for helping me!
Inferior: He also taught me a lot of vJass things and was there, when I need help
Bribe: He also helped me with the start in vJass!

And special thanks to Vexorian, for vJass and TimerUtils!

JASS:
////////////////////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
                                      // My first own spell in vJass \\
                                            // by: Dr. Boom \\
                                         // Special thanks to: \\
         // watermelon_1234: Without him I never would use vJass. He teach me nearly all of it!!! \\
                       // Inferior : He teach me a lot of things with vJass too!! \\
                           // Bribe: He helped me to get into vJass - Thanks! \\
                           
                         // Vexorian: Thanks for vJass ofc^^ and for TimerUtils \\
                         
                // How to use this spell:
                // 1) Copy this trigger
                // 2) Open your map, create a new trigger with the name "Deathball" and paste the trigger into it
                // 3) Go back to this map and copy%past the Ability "Deathball" and the unit "D_WaveBall"
                // 4) Make sure you also c%p the trigger "TimerUtils" which is needed for this spell!!
////////////////////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

library Deathball initializer Init requires TimerUtils

    globals
        private constant integer    SPELL_ID           = 'A003' // The RawCode of the casted spell
        private constant integer    DUMMY_BALL_ID      = 'dum1' // The RawCode of the dummy (the ball)
        private constant string     AOE_SFX            = "Objects\\Spawnmodels\\Undead\\UndeadDissipate\\UndeadDissipate.mdl" // The Area of Effect SFX
        private constant string     TARGET_SFX         = "Abilities\\Spells\\Undead\\AnimateDead\\AnimateDeadTarget.mdl"// The SFX for the targets, that get damage
        private constant attacktype A_TYPE             = ATTACK_TYPE_NORMAL // The attacktype ( is used to deal damage to the targets )
        private constant damagetype D_TYPE             = DAMAGE_TYPE_NORMAL // The damagetype ( is used to deal damage to the targets )
        private constant weapontype W_TYPE             = WEAPON_TYPE_WHOKNOWS // The weapontype ( is used to deal damaghe to the targets)

        private constant real       WAVE_RELEASE       = 30. // How often the AOE_SFX is spawned and the targets get damage
        private constant boolean    DESTROY_TREE       = true // If true, trees in range of the ball will be destroyed
        private constant real       DESTROY_TREE_RANGE = 100. // If DESTROY_TREE, this is the range
        
        private constant real       AOE_SFX_NUMBER     = 10. // Change this value, to chance the number of AOE_SFX,that are spawned
        private constant real       BALL_SPEED         = 17. // The speed of the ball ( the ball moves 17 every 0.04 )
        private constant real       SPELL_DURATION     = 10. // The duration of the full spell
    endglobals
    
    globals // Don't change something here
        private real array DAMAGE_LIFE // This is for the normal damage
        private real array DAMAGE_MANA // This is for the drained mana
        private real array DAMAGE_AREA // The damage area
    endglobals
    
    // Here you can set the damage, the drained mana and the area of effect
    // The current spell has 4 levels. 
    // If you do this spell with only two levels, you can delete any array variable above ([3] + [4]
    // If you do this spell with ten levels, you must add the arrays here ( [5]to[10] )
    // Make sure you use the correct variable names, without any typo.
    private function Setup takes nothing returns nothing // In the Init trigger must be: call Setup()
        set DAMAGE_LIFE[1] =  50.
        set DAMAGE_LIFE[2] = 100.
        set DAMAGE_LIFE[3] = 150.
        set DAMAGE_LIFE[4] = 200.
        
        set DAMAGE_MANA[1] =  25.
        set DAMAGE_MANA[2] =  50.
        set DAMAGE_MANA[3] =  75.
        set DAMAGE_MANA[4] = 100.
        
        set DAMAGE_AREA[1] = 250.
        set DAMAGE_AREA[2] = 275.
        set DAMAGE_AREA[3] = 300.
        set DAMAGE_AREA[4] = 325.
    endfunction
 
/////////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 
    // The configuration ends here. Don't change something under this line \\
////////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
    
    globals
        private constant real    LOOP_TIME     = 0.04 // The loop time (Like GUI: Every 0.04 seconds)
        private constant group   CONTAINER     = CreateGroup() // The group where we filter the targets
        private constant integer HARVEST_ORDER = 852018 // The order number of harvest ( to remove the trees )
        private          integer TEMP_INSTANCE
        private          unit    TREE_CHECKER
        private          rect    ENUM_RECT
        private          real    MIN_X
        private          real    MAX_X
        private          real    MIN_Y
        private          real    MAX_Y  
    endglobals
    
    private struct Deathball
        unit    caster   = null   // the caster
        unit    ball     = null   // the ball
        real    time     = 0.     // HOW LONG the spell should be
        real    x1       = 0.     // x for the creation point of the ball and where the ball should move
        real    y1       = 0.     // y for the creation point of the ball and where the ball should move 
        real    x2       = 0.     // x for the AOE_SFX around the ball
        real    y2       = 0.     // y for the AOE_SFX around the ball
        real    face     = 0.     // facing of the caster, so the ball can move one line
        real    counter  = 0.     // The counter ( in the loop the counter increased by 1. If it match WAVE_RELEASE, resett to 0 ( for the damage and effect )

        private static method areafilter takes nothing returns boolean
            local real life
            local real mana
            local unit enum = GetFilterUnit()
            local Deathball d = TEMP_INSTANCE
            
            if GetWidgetLife(enum) > 0.405 and IsUnitEnemy(enum,GetOwningPlayer(d.caster)) and not IsUnitType(enum,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(enum,UNIT_TYPE_STRUCTURE) then
                set life = DAMAGE_LIFE[GetUnitAbilityLevel(d.caster,SPELL_ID)]
                set mana = DAMAGE_MANA[GetUnitAbilityLevel(d.caster,SPELL_ID)]
                
                call UnitDamageTarget(d.caster,enum,life,true,false,A_TYPE,D_TYPE,W_TYPE)
                call SetUnitState(enum,UNIT_STATE_MANA,(GetUnitState(enum,UNIT_STATE_MANA) - mana ))
                call DestroyEffect(AddSpecialEffect(TARGET_SFX,GetUnitX(enum),GetUnitY(enum)))
            endif
            return false
        endmethod
        
        private static method IsDestructableTree takes destructable d returns boolean
            local boolean b
            if GetWidgetLife(d)>0.405 then
                call PauseUnit(TREE_CHECKER,false)
                set b = IssueTargetOrderById(TREE_CHECKER,HARVEST_ORDER,d)
                call PauseUnit(TREE_CHECKER,true)
                return b
            endif
            return false
        endmethod

        private static method TreeFilter takes nothing returns boolean
            if IsDestructableTree(GetFilterDestructable()) then
                call KillDestructable(GetFilterDestructable())
            endif
            return false
        endmethod
        
        private static method Loop takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local Deathball d = GetTimerData(t)
            local integer i = 0
            local real effectdistance = DAMAGE_AREA[GetUnitAbilityLevel(d.caster,SPELL_ID)]
            local real cosinus = Cos(d.face)
            local real sinus = Sin(d.face)
            local real cosinus2
            local real sinus2
            
            set TEMP_INSTANCE = d
            set d.counter = d.counter + 1.
            set d.x1 = d.x1 + BALL_SPEED * cosinus 
            set d.y1 = d.y1 + BALL_SPEED * sinus
            
            call SetUnitX(d.ball,d.x1)
            call SetUnitY(d.ball,d.y1)
            if d.counter == WAVE_RELEASE then
                set d.counter = 0.
                call GroupEnumUnitsInRange(CONTAINER,GetUnitX(d.ball),GetUnitY(d.ball),DAMAGE_AREA[GetUnitAbilityLevel(d.caster,SPELL_ID)],Condition(function Deathball.areafilter))
                loop
                    exitwhen i == AOE_SFX_NUMBER
                    set cosinus2 = Cos(2 * bj_PI / AOE_SFX_NUMBER * i)
                    set sinus2 = Sin(2 * bj_PI / AOE_SFX_NUMBER * i)
                    set d.x2 = d.x1 + (DAMAGE_AREA[GetUnitAbilityLevel(d.caster,SPELL_ID)] - 25.) * cosinus2
                    set d.y2 = d.y1 + (DAMAGE_AREA[GetUnitAbilityLevel(d.caster,SPELL_ID)] -25.) * sinus2

                    call DestroyEffect(AddSpecialEffect(AOE_SFX,d.x2,d.y2))
                    set i = i + 1
                endloop
            endif
            
            static if DESTROY_TREE then
                call MoveRectTo(ENUM_RECT,d.x1,d.y1)
                call EnumDestructablesInRect(ENUM_RECT,Condition(function Deathball.TreeFilter),null)
            endif

            set d.time = d.time - LOOP_TIME
            if d.time <= 0. or d.x1 < MIN_X or d.x1 > MAX_X or d.y1 < MIN_Y or d.y1 > MAX_Y then
                call ReleaseTimer(t)
                call d.destroy()
                call RemoveUnit(d.ball)
                set d.ball = null
            endif
        endmethod
 
        static method create takes unit caster, real x, real y returns Deathball
            local thistype d = thistype.allocate()
            local timer t = NewTimer()
            set d.caster  = caster
            set d.time    = SPELL_DURATION
            call SetTimerData(t,d)
            call TimerStart(t,LOOP_TIME,true,function Deathball.Loop)

            set d.x1 = GetUnitX(d.caster)
            set d.y1 = GetUnitY(d.caster)
            set d.face = Atan2(y-d.y1,x-d.x1)
            set d.ball = CreateUnit(GetOwningPlayer(d.caster),DUMMY_BALL_ID,d.x1,d.y1,d.face)
            return d
        endmethod
    endstruct


    private function DeathballCast takes nothing returns boolean
        if GetSpellAbilityId() == SPELL_ID then
            call Deathball.create(GetTriggerUnit(),GetSpellTargetX(),GetSpellTargetY())
        endif
        return false
    endfunction

    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
                
        loop
            exitwhen i == bj_MAX_PLAYER_SLOTS
            call TriggerRegisterPlayerUnitEvent(t,Player(i),EVENT_PLAYER_UNIT_SPELL_EFFECT,null)
            set i = i + 1
        endloop
        call TriggerAddCondition(t,Condition(function DeathballCast))
        call Setup()

        call Preload(AOE_SFX)
        call Preload(TARGET_SFX)
        
        set TREE_CHECKER = CreateUnit(Player(15),'hpea',0.,0.,0.)
        set ENUM_RECT = Rect(-DESTROY_TREE_RANGE,-DESTROY_TREE_RANGE,DESTROY_TREE_RANGE,DESTROY_TREE_RANGE)
    
        call ShowUnit(TREE_CHECKER,false)

        set MIN_X = GetRectMinX(bj_mapInitialPlayableArea)
        set MAX_X = GetRectMaxX(bj_mapInitialPlayableArea)
        set MIN_Y = GetRectMinY(bj_mapInitialPlayableArea)
        set MAX_Y = GetRectMaxY(bj_mapInitialPlayableArea)
        set t = null
    endfunction
endlibrary


v 1.0:
Release

v 1.0a:
1) Make number of aoe sfx, spell duration and ball speed configure able
2) Add a note, when the spell has not 4 levels, like in this example.
3) Little code fixes.
4) Set dummy food cost = 0 ^^

v 1.0b:
1) Some improvements for the code.
2) Make the struct private.
3) No variable for timer anymore, it's local now.

Last words: Again this is my first spell. Any spell improvements, any vJass tips, any feedback, rating and more are welcome!!, because I need to and want to learn more things about vJass.
I hope you enjoy the spell - thanks you!

Keywords:
Death, ball, Deathball, vJass
Contents

Moonglade (Map)

Reviews
14:04, 4th Mar 2011 Bribe: The LOOP_TIME could be put to better use with a struct-timer loop or Timer32, which would eliminate the need to require TimerUtils (old, outdated library). Keep that in mind if this is updated in the future. Status...

Moderator

M

Moderator

14:04, 4th Mar 2011
Bribe:

The LOOP_TIME could be put to better use with a struct-timer loop or Timer32, which would eliminate the need to require TimerUtils (old, outdated library). Keep that in mind if this is updated in the future.

Status: Approved
 
Level 16
Joined
May 1, 2008
Messages
1,605
The spell should support more than four levels. Come up with equations, like Damage_Life = 50 * Abilitylevel, Damage_Mana = 25 * Abilitylevel and Damage_Area = 250 + 25 * (abilitylevel - 1).

Yes I thought the same but I didn't make something like 50 * AbilityLevel, because ( I saw many spells ) everyone defined his own damage, so it can be 100 > 130 > 160 > 190 or something, that's why I didn't do this and use arrays.

But I can add a comment, that if the spell has more then 4 levels, they can add it inside the Setup function.

Greetings and Peace
Dr. Boom
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
Hey! ^_^
Don't worry, I wouldn't be mad with you for uploading your own spell. XD

Anyway, on to the code review...
(Side note: I haven't tested the spell yet. I'm checking the code posted on the site. If I do, I'll edit this post.)

Code Review:

  • There are some things that should be easier to configure.
    Example: The duration of the spell, the speed of the ball, how many special effects would be created in a ring.
    • If you do make the number of special effects configurable, you should set cosinus2 and sinus2 something like this:
      JASS:
      set cosinus2 = Cos(2*bj_Pi/ NUMBER_SPECIAL_EFFECT * i)
      set sinus2 = Sin(2*bj_Pi/ NUMBER_SPECIAL_EFFECT * i)
      Note that I'm using radians which saves me the need of having to multiply the angle by bj_DEGTORAD.
  • I wouldn't rely on using the facing angle of the caster for the direction of the ball since the caster won't always be facing the right way. Instead, you should set d.face like this:
    JASS:
    set d.face = Atan2(y-d.y1,x-d.x1)
    Note that that would result in the d.face being in radians. Because of this, you don't need to convert d.face to radians in cosinus and sinus.
  • Whenever you do an if-statement with DESTROY_TREE, you should use a static if.
  • You don't really need the struct member "release." Just use WAVE_RELEASE directly.
  • You would want to make your struct private.
  • You could have a static member inside the struct that's pretty does what TEMP_INSTANCE does except without doing a conversion.
    Ex:
    JASS:
    static thistype temp // Member of the struct.
    ...
    // In the areafilter method:
    call UnitDamageTarget(temp.caster,enum,life,true,false,A_TYPE,D_TYPE,W_TYPE)
    ...
    // In the loop method:
    set temp = d
External Libraries you could have used:

  • IsDestructableTree for checking if a destructable is a tree.
  • BoundSentinel to prevent the ball from going outside map boundaries.
    Just saw your thread in the T&S forum. :p
 
Level 16
Joined
May 1, 2008
Messages
1,605
Moin moin =)

There are some things that should be easier to configure.
Yes you right, I added it into the changeable globals. New:
- Number of AOE_SFX
- Duration of the spell
- The speed of the ball

Cos(2*bj_Pi/ NUMBER_SPECIAL_EFFECT * i)
With that I have a question, if I do bj_Pi he shows me an error. If I replace the bj_Pi with a number, I get an error too! So I created for now: local real pi = 3.1415926535897932 and use the local real for now, because I get an error, if I type bj_Pi.

set d.face = Atan2(y-d.y1,x-d.x1)
Sorry, but it doesn't work when I test it. If I cast the spell the first time, the spell note the current facing of the unit and everything works. But if I change the facing of the caster and cast the spell, the ball will fly to the facing angle, of the first cast and this always. Anyway I leave facing of caster, because I can't see any complication with it.

Whenever you do an if-statement with DESTROY_TREE, you should use a static if.
You don't really need the struct member "release." Just use WAVE_RELEASE directly.

Changed that and you're right =)

You would want to make your struct private.
Sorry, but how should I understand this and why I would want this?

You could have a static member inside the struct that's pretty does what TEMP_INSTANCE does except without doing a conversion.
Sounds like a bigger change, but I don't think, if there will be any noticeable improvement, if I do like you said or how I did it, so I will leave it. If I'm wrong, please tell me and I will make some changes ^^

External Libraries you could have used:
For real: I hate external things^^. Because, if I want create an own spell, I want create everything my own way. Ok I use TimerUtils now, but for me this is the only necessary external library. As long I can code it for my own, I will not use external libraries, if I can code it my own!


Ok here we go. I made some changes, thanks that you took some of your time, to comment my spell.
@ ap0calypse: I failed with this example -_-, but I think, like you, everyone now, what I want to say ^^

Greetings - Thanks - Peace
Dr. Boom
 
Level 19
Joined
Feb 4, 2009
Messages
1,313
you don't have to hide and show your tree checker
but you should pause it else it might start to chop down trees

you got the link to it already but so you don't have to scroll
http://www.wc3c.net/showthread.php?t=103927

and you should check the life of the destructable because it will be much much faster (so much faster that it actually makes a difference)
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
With that I have a question, if I do bj_Pi he shows me an error. If I replace the bj_Pi with a number, I get an error too! So I created for now: local real pi = 3.1415926535897932 and use the local real for now, because I get an error, if I type bj_Pi.
Blame myself for typing the code freehand and the Hive Workshop for highlighting that as correct. XD

Sorry, but it doesn't work when I test it. If I cast the spell the first time, the spell note the current facing of the unit and everything works. But if I change the facing of the caster and cast the spell, the ball will fly to the facing angle, of the first cast and this always. Anyway I leave facing of caster, because I can't see any complication with it.
You see nothing wrong because the Paladin's Cast Backswing is low. Try doing the spell with the Archmage; cast the spell in the opposite direction the Archmage is facing and you'll see that the ball won't be moving in the right direction.

The way I suggested works without those complications.
However, note that using my way results in d.face being in radians.
So you need to set cosinus and sinus like this:
JASS:
            local real cosinus = Cos(d.face)
            local real sinus = Sin(d.face)
Sorry, but how should I understand this and why I would want this?
Even though it is unlikely, someone might make a struct called Deathball and doesn't want it private. Making your own struct private prevents conflicting issues. =D

It's pretty easy to make the struct private; just add private when you declare the struct.
JASS:
private struct Deathball
Sounds like a bigger change, but I don't think, if there will be any noticeable improvement, if I do like you said or how I did it, so I will leave it. If I'm wrong, please tell me and I will make some changes ^^
No super improvement, just thought to let you know. :p

For real: I hate external things^^. Because, if I want create an own spell, I want create everything my own way. Ok I use TimerUtils now, but for me this is the only necessary external library. As long I can code it for my own, I will not use external libraries, if I can code it my own!
Hm, if you say so.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Preloading should be automatic, no reason to have that as a configurable thing. All Blizzard's campaign maps preload automatically.

I recommend implementing the changes these guys recommended first, and when that is done I will examine the code more closely.

Something I know no one else has mentioned, the onDestroy method is not needed here. You only call "d.destroy()" once, so you should instead release the timer right near that line.

JASS:
call ReleaseTimer(d.timer)
call d.destroy()

Also, it's not needed to use an array (struct instance) to store the timers. It's literally a waste of array space. Just do local timer t = NewTimer() and local timer t = GetExpiredTimer().
 
Level 30
Joined
Jan 31, 2010
Messages
3,551
I can help you out with better description.
Learn Tooltip:
Enveloping a dark pact from the Netherworld, hero conjures a missile of pure savage, filled with darkest energy. The missile will slowly move forward for a set amount of time, releasing a dark blast around it every once in a while. Each dark blast deals damage and drains mana from affected enemy units.
Spell Tooltip (Level 1 example):
Enveloping a dark pact from the Netherworld, hero conjures a missile of pure savage, filled with darkest energy. The missile will slowly move forward for up to 10 seconds, releasing a dark blast around it each second. Each dark blast affects AoE of 250, and it deals 50 damage and drains 25 mana from affected enemy units.
I like the spell idea :)
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
JASS:
    private function Setup takes nothing returns nothing // In the Init trigger must be: call Setup()
        set DAMAGE_LIFE[1] =  50.
        set DAMAGE_LIFE[2] = 100.
        set DAMAGE_LIFE[3] = 150.
        set DAMAGE_LIFE[4] = 200.
        
        set DAMAGE_MANA[1] =  25.
        set DAMAGE_MANA[2] =  50.
        set DAMAGE_MANA[3] =  75.
        set DAMAGE_MANA[4] = 100.
        
        set DAMAGE_AREA[1] = 250.
        set DAMAGE_AREA[2] = 275.
        set DAMAGE_AREA[3] = 300.
        set DAMAGE_AREA[4] = 325.
    endfunction

Three wasted global arrays and requiring the user to manually modify any spells greater than 4 levels is far less viable than a simple level * 25 + 225. At worse, these should be combined into one single array and have default values for anything greater. But that's still weird.
 
Top