• 🏆 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!

Water Ball v1.13

[HIGHLIGHT]Spell Description[/code]
The Elemental condenses a series of water balls and throw it to target area. Each water ball deals minor damage to target area, and has a chance to explode upon hitting the ground, dealing additional damage to target area around it. Lasts for a period.

How to import
==================================

1. Copy the ability "Water Ball" <'A000'> from Object Editor - Abilities to your map

###If already has dummy unit model, skip to Step 3.####
2. Import the dummy unit model <dummy.mdx> from Import Manager to your map

###If already has dummy unit, skip to Step 4.####
3. Copy the dummy unit "Dummy" <'h000'> from Object Editor - Units to your map

4. Copy all required libraries, already included in Libraries folder
a checklist for required libraries:
- SimError
- TimerUtils
- GroupUtils
- xebasic
- xepreload

5. Copy the spell trigger "WaterBall" to your map

6. Configure the spell,
mainly change the ABIL_ID to match the rawcode of the ability on your map
and to change the DUMMY_ID, you can change it at xebasic library.

7. Spell is ready to use

JASS:
//                                  Spell : Water Ball
//                                     by : Dark_Axl
//                       For the Spell Contest #1 at BlizzVault.com
//                            Library Required:       SimError, xebasic, xepreload, TimerUtils, GroupUtils
//                            Optional Library:       SpellEffectEvent
//                            Model Required  :       dummy.mdx
//                            Credits Goes to :       Vexorian, for the SimError, xe, TimerUtils and dummy.mdx
//                                                    Rising_Dusk, for the GroupUtils
//
// Spell Description:
//      Target an area, then launches a series of missile into random point in target area, each missile deals damage to an area, and
//      has a chance to explode, dealing more damage. The spell ends when the caster dies or the duration has ended.

scope WaterBall initializer init

    globals
        private constant integer ABIL_ID       = 'A000' 
        // Define the spell's rawcode ID.
        
        private constant integer DUMMY_ID      = XE_DUMMY_UNITID 
        // Define the dummy unit's rawcode ID.
        
        private constant integer FLY_ID        = XE_HEIGHT_ENABLER 
        // Define the Crow Form's rawcode ID.
                
        private constant string  ORDER         = "channel" 
        // Define the orderstring of the spell.
        
        private constant real    PERIOD_T      = 0.03125   
        // Define interval period of timer. Higher value will make missile look sloppy when move, Lower value might cause lag.
        
        private constant boolean CHANNEL       = true   
        // Define if its channeling or not, if set to true, the caster has to channel the ability for the spell to take effect.
        // if set to false, it will spawn the missile from the startpoint of cast, not follow.
                                                        
        private constant attacktype ATT_TYPE   = ATTACK_TYPE_NORMAL     
        // Define the attacktype of the damage dealt
        
        private constant damagetype DMG_TYPE   = DAMAGE_TYPE_NORMAL     
        // Define the damagetype of the damage dealt

        private constant integer MAX_MISSILE   = 200
        // Define the amount of maximum missile per caster per cast at one time, needed for struct member's array size
        
        private constant string  MISSILE_SFX   = "Abilities\\Weapons\\SeaElementalMissile\\SeaElementalMissile.mdl"
        // Define the SFX attached to the missile, put "" if not use.
        
        private constant string  MISSILE_ATT   = "origin"
        // Define the attachment point of the MISSILE_SFX

        private constant string  SPAWN_SFX     = "Abilities\\Spells\\Other\\CrushingWave\\CrushingWaveDamage.mdl"
        // Define the SFX played when the missile spawns, put "" if not use.

        private constant string  HIT_SFX       = "Abilities\\Spells\\Other\\CrushingWave\\CrushingWaveDamage.mdl"
        // Define the SFX played if the missile not explode, put "" if not use.

        private constant string  EXPLODE_SFX   = "Objects\\Spawnmodels\\Naga\\NagaDeath\\NagaDeath.mdl"
        // Define the SFX played if the missile explode, put "" if not use.

        private constant boolean ON_PATH       = true
        // Define if the caster must stand on certain pathable to be able to cast.
        
        private constant pathingtype PATH_TYPE = PATHING_TYPE_FLOATABILITY 
        // Define the pathing type checked if ON_PATH = true.
        // other pathingtype:
        //PATHING_TYPE_WALKABILITY
        //PATHING_TYPE_ANY
        //PATHING_TYPE_AMPHIBIOUSPATHING
        //PATHING_TYPE_BLIGHTPATHING
        //PATHING_TYPE_BUILDABILITY
        //PATHING_TYPE_FLYABILITY
        //PATHING_TYPE_PEONHARVESTPATHING
        
        private constant string  ERROR_MSG     = "Can't cast while not on water"
        // Define the error message shown if the ON_PATH check return false.

        private          player     TEMP_PLAYER
        private          boolexpr   ENEMY_FILTER
    endglobals

    // Defines the delay between each missile spawn.
    private constant function Spawn_Delay takes integer level returns real
        return 0.1              
    endfunction

    // Defines the duration of the spell. Number of the missiles is Duration / Spawn_Delay
    private constant function Duration takes integer level returns real
        return 2. + level       
    endfunction

    // Defines the missile's size scale, value 1. = 100%.
    private constant function Scale takes integer level returns real
        return level*.1 + .9    
    endfunction

    // Defines the radius of possible missile spawn area around caster.
    private constant function MisAOE takes integer level returns real
        return 150.      
    endfunction

    // Defines the damage dealt by each missile normally.
    private constant function HitDamage takes integer level returns real
        return level*3.333 + 10    
    endfunction

    // Defines the bonus damage dealt by each missile when explode.
    private constant function ExpDamage takes integer level returns real
        return level*3.333 + 10    
    endfunction

    // Defines the chance that the missile explode, ranges from 0 to 1, with value 1. = 100%.
    private constant function ExpChance takes integer level returns real
        return 0.3    
    endfunction

    // Defines the AOE of the spells, should match with the Area of Effect of the spell in Object Editor.
    private constant function Area takes integer level returns real
        return 200.    
    endfunction

    // Defines the AOE of normal damage when missile hit.
    private constant function HitAOE takes integer level returns real
        return 200.    
    endfunction

    // Defines the AOE of explosion damage when missile hit.
    private constant function ExpAOE takes integer level returns real
        return 200.    
    endfunction

    // Defines the missile's fly time. This mean all missiles will reach target point after a certain equal delay
    private constant function MisSpeed takes integer level returns real
        return 0.5
    endfunction

    // Defines the minimum flying height of the missile.
    private constant function MinHeight takes integer level returns real
        return 100.      
    endfunction

    // Defines the maximum flying height of the missile.
    private constant function MaxHeight takes integer level returns real
        return 400.
    endfunction

    // Defines the minimum horizontal deviation of the missile while moving.
    private constant function MinWidth takes integer level returns real
        return 100.      
    endfunction

    // Defines the maximum horizontal deviation of the missile while moving.
    private constant function MaxWidth takes integer level returns real
        return 200.      
    endfunction

    // use TEMP_PLAYER as reference to the caster's owner, and GetFilterUnit() as the filtered unit.
    // currently it targets enemy units, alive, not magic immune and not flying unit.
    private function Enemy_Check takes nothing returns boolean
        local unit u = GetFilterUnit()
        local boolean b = IsUnitEnemy(u,TEMP_PLAYER) and IsUnitType(u,UNIT_TYPE_DEAD) and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(u,UNIT_TYPE_FLYING)
        set u = null
        return b
    endfunction

    //===============================THE SPELL PART====================================//
    //==============================DON'T EDIT BELOW ==================================//

    // this function is used to determine the parabolic movement for both xy and z plane
    private constant function GetParabolaZ takes real x, real d, real h returns real
        return 4 * h * x * (d - x) / (d * d)
    endfunction

    // this struct is used both as missile struct and cast struct
    private struct Data
        unit cast
        player p
        timer t
        group g
        real del
        real spa
        real minh
        real maxh
        real mind
        real maxd
        real r
        real scale
        real msdelay
        real dmgA
        real dmgB
        real chance
        real aoeA
        real aoeB
        unit array missile[MAX_MISSILE]
        effect array sfx[MAX_MISSILE]
        real array sin[MAX_MISSILE]
        real array cos[MAX_MISSILE]
        real array sin_c[MAX_MISSILE]
        real array cos_c[MAX_MISSILE]
        real array speed[MAX_MISSILE]
        real array x1[MAX_MISSILE]
        real array y1[MAX_MISSILE]
        real array x2[MAX_MISSILE]
        real array y2[MAX_MISSILE]
        real array dis[MAX_MISSILE]
        real array dur[MAX_MISSILE]
        real array h[MAX_MISSILE]
        real array d[MAX_MISSILE]
        integer index
    endstruct

    // this part move the missile, and dealt the damage in the end.
    private function move_t takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local Data data = GetTimerData(t)
        local unit v
        local real x2
        local real y2
        local real d
        local real h
        local real x
        local integer i  = 1

        loop
            exitwhen i > data.index

            //substract "speed" from "remaining distance"
            set data.dur[i] = data.dur[i] - data.speed[i]

            set x2 = data.x2[i] + data.dur[i]*data.cos[i]
            set y2 = data.y2[i] + data.dur[i]*data.sin[i]
            set x  = SquareRoot((y2-data.y1[i])*(y2-data.y1[i])+(x2-data.x1[i])*(x2-data.x1[i]))

            //function GetParabolaZ is used to calculate curve both in xy and z plane.
            set d  = GetParabolaZ(x,data.dis[i],data.d[i])
            set h  = GetParabolaZ(x,data.dis[i],data.h[i])
            set x2 = x2 + d*data.cos_c[i]
            set y2 = y2 + d*data.sin_c[i]
            call SetUnitX(data.missile[i],x2)
            call SetUnitY(data.missile[i],y2)
            call SetUnitFlyHeight(data.missile[i],h,0)

            //if .dur[i] <= 0, means target is reached, deals damage and reindexing.
            if data.dur[i] <= 0 then

                // this part deals hit damage
                set TEMP_PLAYER = data.p
                call GroupClear(data.g)
                call GroupEnumUnitsInRange(data.g,x2,y2,data.aoeA,ENEMY_FILTER)
                loop
                    set v = FirstOfGroup(data.g)
                    exitwhen v == null
                    call GroupRemoveUnit(data.g,v)
                    call UnitDamageTarget(data.cast,v,data.dmgA,false,true,ATT_TYPE,DMG_TYPE,null)
                endloop
                call DestroyEffect(AddSpecialEffect(HIT_SFX,x2,y2))
                
                // this part check for explosion chance and damage
                if GetRandomReal(0,1) <= data.chance then
                    call GroupClear(data.g)
                    set TEMP_PLAYER = data.p
                    call GroupEnumUnitsInRange(data.g,x2,y2,data.aoeB,ENEMY_FILTER)
                    loop
                        set v = FirstOfGroup(data.g)
                        exitwhen v == null
                        call GroupRemoveUnit(data.g,v)
                        call UnitDamageTarget(data.cast,v,data.dmgB,false,true,ATT_TYPE,DMG_TYPE,null)
                    endloop
                    call DestroyEffect(AddSpecialEffect(EXPLODE_SFX,x2,y2))
                endif
            endif
            set i = i + 1
        endloop

        // reindexing
        set i = 1
        loop
            exitwhen i > data.index
            if data.dur[i] <= 0 then
                call DestroyEffect(data.sfx[i])
                call KillUnit(data.missile[i])
                call RemoveUnit(data.missile[i])
                set data.missile[i] = data.missile[data.index]
                set data.dur[i]     = data.dur[data.index]
                set data.sfx[i]     = data.sfx[data.index]
                set data.sin[i]     = data.sin[data.index]
                set data.cos[i]     = data.cos[data.index]
                set data.sin_c[i]   = data.sin_c[data.index]
                set data.cos_c[i]   = data.cos_c[data.index]
                set data.x1[i]      = data.x1[data.index]
                set data.y1[i]      = data.y1[data.index]
                set data.x2[i]      = data.x2[data.index]
                set data.y2[i]      = data.y2[data.index]
                set data.dis[i]     = data.dis[data.index]
                set data.h[i]       = data.h[data.index]
                set data.d[i]       = data.d[data.index]
                set data.speed[i]   = data.speed[data.index]
                set data.sfx[data.index] = null
                set data.missile[data.index] = null
                set data.index = data.index - 1
            endif
            set i = i + 1
        endloop
                
        //if index ever reaches zero, means spells end, cleanup.
        if data.index <= 0 then
            call ReleaseTimer(t)
            call ReleaseGroup(data.g)
            set data.cast = null
            set data.t = null
            set data.p = null
            set data.g = null
            call data.destroy()
        endif
                
        set t = null
    endfunction

    // this part checks for the channel part, and spawn the missile.
    private function effect_t takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local Data data = GetTimerData(t)
        local integer side
        local real dis
        local real ang
        
        // this part check if the caster is still alive AND channeling the spell, if not, spell effect ends.
        set data.dur[0] = data.dur[0] - data.del
        if data.dur[0] < 0 or IsUnitType(data.cast,UNIT_TYPE_DEAD) or (GetUnitCurrentOrder(data.cast) != OrderId(ORDER) and CHANNEL) then
            call ReleaseTimer(t)
            set t = null
            return
        endif

        //this part setup the missile trajectory
        set data.index = data.index + 1
        set dis = GetRandomReal(0,data.spa)
        set ang = GetRandomReal(0,360)
        set data.x1[data.index] = data.x1[0] + Cos(ang*0.0174533)*dis
        set data.y1[data.index] = data.y1[0] + Sin(ang*0.0174533)*dis
        set dis = GetRandomReal(0,data.r)
        set ang = GetRandomReal(0,360)
        set data.x2[data.index] = data.x2[0] + Cos(ang*0.0174533)*dis
        set data.y2[data.index] = data.y2[0] + Sin(ang*0.0174533)*dis
        set ang = Atan2((data.y2[data.index]-data.y1[data.index]),(data.x2[data.index]-data.x1[data.index]))

        //.dis is used for parabola calculation, while .dur is used to store instance duration in distance before impact
        set data.dis[data.index]  = SquareRoot((data.y2[data.index]-data.y1[data.index])*(data.y2[data.index]-data.y1[data.index])+(data.x2[data.index]-data.x1[data.index])*(data.x2[data.index]-data.x1[data.index]))
        set data.dur[data.index]  = data.dis[data.index]
        set data.h[data.index]    = GetRandomReal(data.minh,data.maxh)
        set data.d[data.index]    = GetRandomReal(data.mind,data.maxd)
        set data.speed[data.index]= data.dis[data.index]*PERIOD_T/data.msdelay

        //this determine whether the missile curve left or right side
        set side = GetRandomInt(0,1)
        if side == 0 then
            set side = -1
        endif
        set data.sin[data.index] = Sin(ang+bj_PI)
        set data.cos[data.index] = Cos(ang+bj_PI)
        set data.sin_c[data.index] = Sin(ang+(side*0.5*bj_PI))
        set data.cos_c[data.index] = Cos(ang+(side*0.5*bj_PI))
        
        call DestroyEffect(AddSpecialEffect(SPAWN_SFX,data.x1[data.index],data.y1[data.index]))
        
        //preparing the missile(s)
        set data.missile[data.index] = CreateUnit(data.p,DUMMY_ID,data.x1[data.index],data.y1[data.index],ang)
        set data.sfx[data.index] = AddSpecialEffectTarget(MISSILE_SFX,data.missile[data.index],MISSILE_ATT)
        call SetUnitScale(data.missile[data.index],data.scale,0,0)
        call UnitAddAbility(data.missile[data.index],FLY_ID)
        call UnitRemoveAbility(data.missile[data.index],FLY_ID)
        
        // to make sure the missile looks launched from ground/water or z = 0
        call SetUnitFlyHeight(data.missile[data.index],0,0)                

        //start missile timer if there is missile ready to run
        if data.index == 1 then
            call TimerStart(data.t,PERIOD_T,true,function move_t)
        endif

        set t = null
    endfunction

    // this part setup the variable and constant
    private function postcast_t takes nothing returns nothing
        local unit u     = GetTriggerUnit()
        local timer t    = NewTimer()
        local integer lv = GetUnitAbilityLevel(u,ABIL_ID)
        local Data data  = Data.create()
        
        //this part setup all parameter used in spell based on function provided at top
        set data.cast   = u
        set data.p      = GetOwningPlayer(u)
        set data.scale  = Scale(lv)
        set data.del    = Spawn_Delay(lv)
        set data.dur[0] = Duration(lv)
        set data.spa    = MisAOE(lv)
        set data.x1[0]  = GetUnitX(u)
        set data.y1[0]  = GetUnitY(u)
        set data.x2[0]  = GetSpellTargetX()
        set data.y2[0]  = GetSpellTargetY()
        set data.minh   = MinHeight(lv)
        set data.maxh   = MaxHeight(lv)
        set data.mind   = MinWidth(lv)
        set data.maxd   = MaxWidth(lv)
        set data.msdelay= MisSpeed(lv)
        set data.r      = Area(lv)
        set data.index  = 0
        set data.t = NewTimer()
        set data.g      = NewGroup()
        set data.dmgA   = HitDamage(lv)
        set data.dmgB   = ExpDamage(lv)
        set data.chance = ExpChance(lv)
        set data.aoeA   = HitAOE(lv)
        set data.aoeB   = ExpAOE(lv)

        //start the timer for channeling part
        call SetTimerData(t,data)
        call SetTimerData(data.t,data)
        call TimerStart(t,data.del,true,function effect_t)
        set t = null
        set u = null
    endfunction

    // this part does the pathingtype check and cancel the spell respectively, only if ON_PATH == true.
    static if ON_PATH then
        private function precast_t takes nothing returns boolean
            local unit u = GetTriggerUnit()
            if GetSpellAbilityId() == ABIL_ID and IsTerrainPathable(GetUnitX(u),GetUnitY(u),PATH_TYPE) then
                call PauseUnit(u,true)
                call IssueImmediateOrder(u,"stop")
                call PauseUnit(u,false)
                call SimError(GetOwningPlayer(u),ERROR_MSG)
            endif
            set u = null
            return false
        endfunction
    endif

    //this is the condition of the spell cast
    private function Abil_Check takes nothing returns boolean
        return GetSpellAbilityId() == ABIL_ID
    endfunction

    //this is the initialization of the spell trigger
    private function init takes nothing returns nothing
        local trigger trig

        //this part setup for the pre-cast check
        static if ON_PATH then
            set trig = CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ( trig, EVENT_PLAYER_UNIT_SPELL_CAST )
            call TriggerAddCondition( trig , Condition( function precast_t ) )
        endif

        //this part setup for the spell effect itself
        static if LIBRARY_SpellEffectEvent then
            call RegisterSpellEffectEvent(ABIL_ID, function postcast_t)
        else
            set trig = CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ( trig, EVENT_PLAYER_UNIT_SPELL_EFFECT )
            call TriggerAddCondition( trig, Condition( function Abil_Check ) )
            call TriggerAddAction( trig, function postcast_t )
        endif

        //this is setup for boolexpr used in group enumeration for damage effect
        set ENEMY_FILTER = Filter(function Enemy_Check)

        //preload ability and effect
        call XE_PreloadAbility(ABIL_ID)
        call Preload(MISSILE_SFX)
        call Preload(HIT_SFX)
        call Preload(EXPLODE_SFX)
        call Preload(SPAWN_SFX)

        set trig = null
    endfunction

endscope

Spell Information
Fully compatible with v1.24, MUI, configurable.
Requires JNGP
Uses:
- SimError
- xebasic
- xepreload
- TimerUtils
- GroupUtils

Supports usage of Bribe's SpellEffectEvent library

Credits goes to:
- Vexorian, for the SimError, xe, TimerUtils and dummy.mdx.
- Rising_Dusk, for the GroupUtils
A spell I made for BlizzVault Spell Contest#1, unfortunately the contest looks to be shutdown before judging.
141017-albums6809-picture74754.jpg
141017-albums6809-picture74755.jpg
141017-albums6809-picture74756.jpg
v1.00 <06/04/2010>
- Initial release
- Limited to contest-specific rules

v1.10 <28/09/2013>
- Now uses TimerUtils, GroupUtils, xe.
- Remove abilitypreload function, now use xepreload
- Changed missile movement from linear movement to parabolic movement
- Reworked from using one timer per instance of missile into one timer for all missiles

v1.11 <29/09/2013>
- Fixed missile movement sometimes going off its trajectory
- Changed configurable function to constant function
- Changed precast trigger creation to be based on ON_PATH
- Changed the default interval period of movement timer
- Changed damage's weapontype from WEAPON_TYPE_WHOKNOWS to null
- Added SpellEffectEvent as optional library
- Reformat the trigger to support more readable trigger
- Added "How to Import" instruction inside the map and site

v1.12 <05/10/2013>
- Fixed spell didn't actually release timer and destroy struct after spell ends
- Changed how the missile speed defined, now it use missile life time to calculate missile speed
- Now use IsUnitType(UNIT_TYPE_DEAD) check instead of GetWidgetLife
- Now precalculate some value which remains constant for each missile
- Added locust to dummy unit via Object Editor

v1.13 <06/10/2013>
- Now only use 1 struct per cast, previous version use 2 struct per cast.
- Optimized precast function
- Cleaned up the spell code from garbage left from older version
- Added Handle Counter in test map to check possible leak
- Attached screenshot to show spell in effect from different point of view


Keywords:
water, ball, ground, missile, vjass, parabola
Contents

Water Ball v1.13 (Map)

Reviews
Water Ball v1.11 | Reviewed by Maker | 29th Sep 2013 APPROVED A cool spell that is leakless and MUI Locust doesn't need to be configurable, add it in object editor. Units with locust do not need invulnerability...

Moderator

M

Moderator


Water Ball v1.11 | Reviewed by Maker | 29th Sep 2013
APPROVED


126248-albums6177-picture66521.png


  • A cool spell that is leakless and MUI
126248-albums6177-picture66523.png


  • Locust doesn't need to be configurable, add it in object editor.
    Units with locust do not need invulnerability ability
  • The pathing type could be checked when the order is issued
  • Instead of GetWidgetLife, use UNIT_TYPE_DEAD check
  • If the angle and side do not change, you can precalcuate stuff here
    set x2 = x2 + d*Cos(data.ang[i]+(data.side[i]*0.5*bj_PI)) set y2 = y2 + d*Sin(data.ang[i]+(data.side[i]*0.5*bj_PI))
[tr]



Water Ball v1.10 | Reviewed by Maker | 28th Sep 2013
NEEDS FIX


126248-albums6177-picture66522.png


  • You are lacking importing instructions (xebasic dummy unit type confguration)
  • Some of the projectiles fly not to the targeted area,
    but several hundreds of distance units away
126248-albums6177-picture66523.png


  • Consider using Spell Effect Event
    You could make it optional
  • You could merge actions with conditions, leaving you only with
    condition functions
  • You don't need p variable in precast_t
  • The pathing type could be checked when the order is issued
  • The whole precast_t could be wrapped in static if
    and don't create a trigger or register an event if the boolean is false
  • Instead of GetWidgetLife, use UNIT_TYPE_DEAD check
[tr]

07:45, 14th Apr 2010
TriggerHappy:

Not very original, but it's approvable.
  • You should be using TimerUtils or something for your timer attachment. Even better is it includes recycling which is far better than you destroying timers.
  • Remove those FirstOfGroup loops, they are fairly slow.
  • Improve your group usage by either using a global group or recycling groups (GroupUtils).
  • You can inline start_t.
  • Change if ON_PATH to static if ON_PATH then
 
Level 6
Joined
Nov 3, 2008
Messages
117
Ok Feedback.

Code looks ok, just some minor things.

no need to inline TriggerRegisterAnyUnitEventBJ!

maybe lower the Period_T from 0.04 to 0.03 because it looks kinda weird with itsi bitsi pauses in the movement :p

Maybe add a Channeling Animation to the Caster. He just watches the balls flying :O

JASS:
private function PreloadAbil takes integer id returns nothing
        local unit u = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE),DUMMY_ID,0,0,0)
        call UnitAddAbility(u,id)
        call UnitRemoveAbility(u,id)
        call RemoveUnit(u)
        set u = null
    endfunction

Pls don't make sth like that o_O. Import xepreload and use a static if.

Maybe i'll give you some more, only skimmed the code

Don't destroy Groups and Timers. Use TimerUtils and GroupUtils pls

Use SetUnitX and SetUnitY instead of SetUnitPosition

This:
JASS:
private function Enemy_Check takes nothing returns boolean
        local unit v = GetFilterUnit()
        local boolean b = false
        if IsUnitEnemy(v,TEMP_PLAYER) and GetWidgetLife(v) > .405 and IsUnitType(v,UNIT_TYPE_MAGIC_IMMUNE)==false and IsUnitType(v,UNIT_TYPE_FLYING) == false then
            set b = true
        endif
        set v = null
        return b
    endfunction

Can be:
JASS:
private function Enemy_Check takes nothing returns boolean
        return IsUnitEnemy(GetFilterUnit(),TEMP_PLAYER) and GetWidgetLife(GetFilterUnit()) > .405 and not IsUnitType(GetFilterUnit(),UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(GetFilterUnit(),UNIT_TYPE_FLYING)
    endfunction
 
Last edited:
Level 14
Joined
Nov 18, 2007
Messages
1,084
Use SetUnitX and SetUnitY instead of SetUnitPosition
You don't need to follow this suggestion if you want the unit's pathing to be considered, but you should know that using SetUnitX/Y is faster.

JASS:
private function Enemy_Check takes nothing returns boolean
     return IsUnitEnemy(GetFilterUnit(),TEMP_PLAYER) and GetWidgetLife(GetFilterUnit()) > .405 and not IsUnitType(GetFilterUnit(),UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(GetFilterUnit(),UNIT_TYPE_FLYING)
endfunction
Actually, it might be better if it was done your way with a few modifications since you use GetFilterUnit() multiple times.
JASS:
private function Enemy_Check takes nothing returns boolean
     local unit v = GetFilterUnit()
     local boolean b = IsUnitEnemy(v,TEMP_PLAYER) and GetWidgetLife(v) > .405 and IsUnitType(v,UNIT_TYPE_MAGIC_IMMUNE)==false and IsUnitType(v,UNIT_TYPE_FLYING) == false
     set v = null
     return b
endfunction
I think you should also know that the IsUnitType bug is fixed now.

You don't need two trigger variables in your init function, just reuse the first one.

FirstOfGroup loops are slower than doing the actions directly in the filter or with a ForGroup function.

This is my opinion, but you have many variables in your struct that don't really need to be there.

You could put constant in front of some of the functions, like Duration.

I think you don't need to null player variables.
 
Instead of using all those struct members, you should just save a member of ".lvl" and use that. (Actually, you don't even need to save a .lvl, you can just use the saved unit and then get the level when you need to)

Also try group recycling instead of creating a group each time. (Also switch from FirstOfGroup loops to Filter enumerations)

Basically:
JASS:
scope GroupExample

globals
    private constant group heheGroup = CreateGroup() //just create one group and use it
endglobals

function FilterEnumeration takes nothing returns boolean //returns boolean since it's used as a condition
    if GetWidgetLife(GetFilterUnit())>.405 then  //use GetFilterUnit(), this part is your boolean check
        //do your normal actions, pretend that it was a "ForGroup()"
        //except instead of using GetEnumUnit(), you use GetFilterUnit()
    endif
    return false //return false at the end, since it has to return boolean
endfunction

function UsageExample takes nothing returns nothing
    call GroupEnumUnitsInRect(g,GetWorldBounds(),Condition(function FilterEnumeration))
    //this is where you enumerate, just an example
    //the "Condition(function xxxx)" is where you are going to do all your actions
endfunction
//no need for destroying or clearing, enumerations auto-clear
//and you don't need to destroy since you'll just reuse the group

endscope

Just as an example. No need to really use GroupUtils since that is basically doing the above for you. (and it was to workaround with boolexpr null and leaks, but conditions/filters are auto-recycled and nulling as an input no longer leaks, so as long as you don't destroy boolexprs and know what you're doing, it should be fine)

You can also use TimerUtils as Inferior mentioned, but it isn't completely necessary.

Otherwise, nice job.

EDIT: I just looked at the spell and it looks awesome. Great eyecandy. =D
 
Level 9
Joined
Dec 12, 2007
Messages
489
[HIGHLIGHT]Update v1.10[/code]
Changes
- Now uses TimerUtils, GroupUtils, xe.
- Remove abilitypreload function
- Changed missile movement from linear movement to parabolic movement
- Reworked from using one timer per instance of missile into one timer for all missiles
- Old spell code provided in test map for comparison
All suggested changes have been considered and necessary changes is implemented.
except ForGroup, because for me, FirstOfGroup looping is better than ForGroup enumeration as it avoid the need to transfer to other function.


You updated it since in 2010 to 2013 xD.!
well yes, better late than never, its not funny for someone who encourage others to update their people spell if he didn't do it too. about the timespan, college can be time-consuming.
 
Level 9
Joined
Dec 3, 2010
Messages
162
You could merge your Action codes into the Condition for both your precast and postcast.
JASS:
// this part does the pathingtype check and cancel the spell respectively.
    private function precast_t takes nothing returns nothing
        local unit u
        local player p

        if GetSpellAbilityId() == ABIL_ID then
            set u = GetTriggerUnit()

            static if ON_PATH then
                if IsTerrainPathable(GetUnitX(u),GetUnitY(u),PATH_TYPE) then
                    call PauseUnit(u,true)
                    call IssueImmediateOrder(u,"stop")
                    call PauseUnit(u,false)
                    set p = GetOwningPlayer(u)
                    call SimError(p,ERROR_MSG)
                    set p = null
                endif
            endif
        endif
        set u = null

        return false
    endfunction

private function init takes nothing returns nothing
        local trigger precast  = CreateTrigger()
        local trigger postcast = CreateTrigger()

        //this part setup for the pre-cast check
        call TriggerRegisterAnyUnitEventBJ( precast, EVENT_PLAYER_UNIT_SPELL_CAST )
        call TriggerAddCondition( precast , Condition(function precast_t) )
        ...
endfunction


Your static if for ON_PATH should be checked when you actually create the trigger, rather than on every cast. This would prevent unnecessary codes from being fired every time the spell is cast when ON_PATH is false.

Is there a reason why you're not using methods? Since you're using a struct, might as well make use of methods (I personally like using thistype :p). Also, consider using array structs.

Your configuration functions could be constant functions.


precast_t
You don't need to declare the player variable, p. You're only calling GetOwningPlayer once.


effect_t
For movement timers, it's better utilize a timeout of 0.03125. I'd recommend you use either Timer32 or CTL. I prefer CTL though.


move_t
I realized you use data.g as a temporary group. If so, it's unnecessary for it to be a member. In fact, you don't even need to create a group. Simply use bj_lastCreatedGroup for all FirstOfGroup enumerations.

The way you're currently using FirstOfGroup is actually much slower than a ForGroup. FirstOfGroup is only faster if your filter is null. If not, that defeats the whole purpose of using a FirstOfGroup. Your "filtering" should be done within the FirstOfGroup loop itself, rather than using a enumeration filter.

You can leave weapon type for UnitDamageTarget as null.

You forgot to null your unit v.
 
Last edited:
Level 9
Joined
Dec 12, 2007
Messages
489
You could merge your Action codes into the Condition for both your precast and postcast.
JASS:
// this part does the pathingtype check and cancel the spell respectively.
    private function precast_t takes nothing returns nothing
        local unit u
        local player p

        if GetSpellAbilityId() == ABIL_ID then
            set u = GetTriggerUnit()

            static if ON_PATH then
                if IsTerrainPathable(GetUnitX(u),GetUnitY(u),PATH_TYPE) then
                    call PauseUnit(u,true)
                    call IssueImmediateOrder(u,"stop")
                    call PauseUnit(u,false)
                    set p = GetOwningPlayer(u)
                    call SimError(p,ERROR_MSG)
                    set p = null
                endif
            endif
        endif
        set u = null

        return false
    endfunction

private function init takes nothing returns nothing
        local trigger precast  = CreateTrigger()
        local trigger postcast = CreateTrigger()

        //this part setup for the pre-cast check
        call TriggerRegisterAnyUnitEventBJ( precast, EVENT_PLAYER_UNIT_SPELL_CAST )
        call TriggerAddCondition( precast , Condition(function precast_t) )
        ...
endfunction


Your static if for ON_PATH should be checked when you actually create the trigger, rather than on every cast. This would prevent unnecessary codes from being fired every time the spell is cast when ON_PATH is false.

good idea, will do.

Is there a reason why you're not using methods? Since you're using a struct, might as well make use of methods (I personally like using thistype :p). Also, consider using array structs.
I'm still learning about using method, so it might be better to left it in current way for now until I can implement method in it. by the way, I heard about "struct extend array" is better or what, but it require manual allocator/deallocator, and to implement it will require no struct method, so probably I will left it as it is for now and rework it whole later.

Your configuration functions could be constant functions.
I do remember constant configuration function mean that it can and will be inlined during compile time, probably will left it as it is, so users can modify the function to include several math operations. but I guess that can be done to the GetParabolaZ as its a fixed function, will update soon.

precast_t
You don't need to declare the player variable, p. You're only calling GetOwningPlayer once.
the only player variable p exist is only at precast_t which will be gone when changes above done.

effect_t
For movement timers, it's better utilize a timeout of 0.03125. I'd recommend you use either Timer32 or CTL. I prefer CTL though.
honestly, I refrain myself from using CTL or T32 or any other timer system other than TimerUtils. about the interval, that will be at user's discretion when implemented in their map, but default value won't hurt, will do.

move_t
I realized you use data.g as a temporary group. If so, it's unnecessary for it to be a member. In fact, you don't even need to create a group. Simply use bj_lastCreatedGroup for all FirstOfGroup enumerations.

The way you're currently using FirstOfGroup is actually much slower than a ForGroup. FirstOfGroup is only faster if your filter is null. If not, that defeats the whole purpose of using a FirstOfGroup. Your "filtering" should be done within the FirstOfGroup loop itself, rather than using a enumeration filter.
well that temp group is to maintain everything by specific instance of spell, will check if it won't hurt for multiple cast instance. about the FirstOfGroup loop, its slower factor is negligible, ForGroup enumeration will need another global variable to transfer the struct into enumeration, which I avoid doing so.

You can leave weapon type for UnitDamageTarget as null.
well it doesn't matter much because weapon type is used for sound on damage, but I think null won't hurt too, will do.

You forgot to null your unit v.
something I learn about FirstOfGroup loop is, to exit the loop, unit v must be null, so its already done automatically. no need to null it again.

Thank you for reviewing, iAyanami.
 
Level 9
Joined
Dec 3, 2010
Messages
162
I do remember constant configuration function mean that it can and will be inlined during compile time, probably will left it as it is, so users can modify the function to include several math operations. but I guess that can be done to the GetParabolaZ as its a fixed function, will update soon.

Constant functions are basically functions that do not call any non-constant functions and do not use the set operation. Thus, mathematical operations still qualify the function to be constant.

honestly, I refrain myself from using CTL or T32 or any other timer system other than TimerUtils. about the interval, that will be at user's discretion when implemented in their map, but default value won't hurt, will do.

Is there a reason for not using the systems? Or is it just personal preference. CTL and T32 would be more efficient than TimerUtils when a lot of instances of spells are simultaneously occurring.

well that temp group is to maintain everything by specific instance of spell, will check if it won't hurt for multiple cast instance. about the FirstOfGroup loop, its slower factor is negligible, ForGroup enumeration will need another global variable to transfer the struct into enumeration, which I avoid doing so.

Yeah, you won't need to use ForGroup. Simply put the filter within the FirstOfGroup loop and set the filter parameter to null in the GroupEnumUnitsInRage function. Will give you slightly better performance that way (better performance is always better right? :p). You'd need to change your filter function though (won't be able to use GetFilterUnit). Use a filter with 2 unit parameters, something like private function EnemyFilter takes unit caster, unit enemy returns boolean.

something I learn about FirstOfGroup loop is, to exit the loop, unit v must be null, so its already done automatically. no need to null it again.

Ah yes, forgot about that FirstOfGroup loop. My bad :p

Thank you for reviewing, iAyanami.

No problem :)
 
Level 9
Joined
Dec 12, 2007
Messages
489
Constant functions are basically functions that do not call any non-constant functions and do not use the set operation. Thus, mathematical operations still qualify the function to be constant.
that part which hinder the use of set is why I avoid constant function, but user who use this should understand basic JASS, so yes, will do on all possible functions.

Is there a reason for not using the systems? Or is it just personal preference. CTL and T32 would be more efficient than TimerUtils when a lot of instances of spells are simultaneously occurring.
that's fully because of personal preference, but maybe when I'm convinced to use it, I'll add it.

Yeah, you won't need to use ForGroup. Simply put the filter within the FirstOfGroup loop and set the filter parameter to null in the GroupEnumUnitsInRage function. Will give you slightly better performance that way (better performance is always better right? :p). You'd need to change your filter function though (won't be able to use GetFilterUnit). Use a filter with 2 unit parameters, something like private function EnemyFilter takes unit caster, unit enemy returns boolean.
actually I've done it like that before in my other map here, I found that the downside is the amount of function calls when a lot of units is inside the group to loop. that's why I use GroupEnumUnitsInRange with enum filter now.

[HIGHLIGHT]EDIT[/code]
I would like to respond to Maker's review,
Some of the projectiles fly not to the targeted area, but several hundreds of distance units away
I've already fixed that in my comp, its because I forgot to reindex data.x2 and data.y2 during reindexing.

The pathing type could be checked when the order is issued
I don't understand what this mean, judging from another review point, does that mean instead of catching spell cast event, it will be better to catch the orderid for pathing check?

Instead of GetWidgetLife , use UNIT_TYPE_DEAD check
I read that UNIT_TYPE_DEAD check is more unreliable because it has an update delay before labeled dead, unlike GetWidgetLife although latter has downside with setting dead unit's hitpoint above that 1.

[HIGHLIGHT]Update v1.11[/code]
Changes
- Fixed missile movement sometimes going off its trajectory
- Changed configurable function to constant function
- Changed precast trigger creation to be based on ON_PATH
- Changed the default interval period of movement timer
- Changed damage's weapontype from WEAPON_TYPE_WHOKNOWS to null
- Added SpellEffectEvent as optional library
- Reformat the trigger to support more readable trigger
- Added "How to Import" instruction inside the map and site
 
Last edited:
Level 37
Joined
Mar 6, 2006
Messages
9,240
I don't understand what this mean, judging from another review point, does that mean instead of catching spell cast event, it will be better to catch the orderid for pathing check?

A unit is issued an order event.

I read that UNIT_TYPE_DEAD check is more unreliable because it has an update delay before labeled dead

Where did you read that? I don't believe it until I see proof.

I tested with this, unit was labelled dead when it was supposed to.

  • Untitled Trigger 001
    • Events
      • Player - Player 1 (Red) Selects a unit
      • Unit - A unit Dies
    • Conditions
    • Actions
      • Unit - Kill (Triggering unit) // Enable/disable
      • Unit - Set life of (Triggering unit) to 0.00% // Enable/disable
      • Custom script: if IsUnitType(GetTriggerUnit(), UNIT_TYPE_DEAD) then
      • Game - Display to Player Group - Player 1 (Red) for 5.00 seconds the text: DEAD
      • Custom script: else
      • Game - Display to Player Group - Player 1 (Red) for 5.00 seconds the text: NOT DEAD
      • Custom script: endif
 
Level 9
Joined
Dec 12, 2007
Messages
489
Where did you read that? I don't believe it until I see proof.
I don't remember the exact post, but I remember read that in a thread in World Editor Help Zone few months ago. yeah, probably I misread that. but still, it won't matter much using GetWidgetLife on this matter unless there's certain drawback with its use in this ability.

about the latest review, will do another update regarding that angle calculation, and locust ability.

about that pathing check on order issued, haven't test this but if the caster is issued order to cast the ability to target point outside its casting range, will the check happen when start moving to get into range or after the caster get into casting range? and does it have advantage than using unit begin cast event?
 
Level 9
Joined
Dec 12, 2007
Messages
489
[HIGHLIGHT]Update v1.12[/code]
Changes
- Fixed spell didn't actually release timer and destroy struct after spell ends
- Changed how the missile speed defined, now it use missile life time to calculate missile speed
- Now use IsUnitType(UNIT_TYPE_DEAD) check instead of GetWidgetLife
- Now precalculate some value which remains constant for each missile
- Added locust to dummy unit
[HIGHLIGHT]Update v1.13[/code]
Changes
- Now only use 1 struct per cast, all previous versions use 2 struct per cast.
- Optimized precast function
- Cleaned up the spell code from garbage left from older version
- Added Handle Counter in test map to check possible leak
- Attached screenshot to show spell in effect from different point of view
 
Last edited:
Level 39
Joined
Feb 27, 2007
Messages
4,992
Seems this spell doesn't work anymore. Doesn't deal damage when cast on enemies.
Hilarious that in 6 years nobody bothered to see if this spell actually did anything. The last update uses the following filter function to find targets:
JASS:
    private function Enemy_Check takes nothing returns boolean
        local unit u = GetFilterUnit()
        local boolean b = IsUnitEnemy(u,TEMP_PLAYER) and IsUnitType(u,UNIT_TYPE_DEAD) and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(u,UNIT_TYPE_FLYING)
        set u = null
        return b
    endfunction
If you read that it actually only targets dead enemy non-flying units. Adding a "not" before the dead check will return the spell to normal working order, but be aware that it by default doesn't affect air units so if you test it on the dragons in the map it won't deal damage to them. The fixed line looks like this:

local boolean b = IsUnitEnemy(u,TEMP_PLAYER) and not IsUnitType(u,UNIT_TYPE_DEAD) and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(u,UNIT_TYPE_FLYING)
 
Level 9
Joined
Dec 12, 2007
Messages
489
Hilarious that in 6 years nobody bothered to see if this spell actually did anything. The last update uses the following filter function to find targets:
JASS:
    private function Enemy_Check takes nothing returns boolean
        local unit u = GetFilterUnit()
        local boolean b = IsUnitEnemy(u,TEMP_PLAYER) and IsUnitType(u,UNIT_TYPE_DEAD) and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(u,UNIT_TYPE_FLYING)
        set u = null
        return b
    endfunction
If you read that it actually only targets dead enemy non-flying units. Adding a "not" before the dead check will return the spell to normal working order, but be aware that it by default doesn't affect air units so if you test it on the dragons in the map it won't deal damage to them. The fixed line looks like this:

local boolean b = IsUnitEnemy(u,TEMP_PLAYER) and not IsUnitType(u,UNIT_TYPE_DEAD) and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(u,UNIT_TYPE_FLYING)

LOL, after 6 years, thanks for noticing that Pyro, that's what happen when I follow suggestion before actually fully understanding the suggestion first :grin:
 
Top