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

Storm v1.04a

Needs JNGP with the most recent Jasshelper.

Storm
Conjures a storm which deals damage to enemy units within a target area and extra damage to those near the center of the storm every second for 6 seconds. The storm will destroy trees and create a fire that will burn nearby enemy units.
Level 1 - Deals 40 full damage and 25 center damage. Fire deals 5 damage every second for 6 seconds.
Level 2 - Deals 55 full damage and 35 center damage. Fire deals 8 damage every second for 8 seconds.
Level 3 - Deals 70 full damage and 45 center damage. Fire deals 11 damage every second for 10 seconds.

Please give me credit if you use this spell in your map.
Credits to:
~ Vexorian for vJass, the dummy model, and xe system
~ moyack for TimedEffects library and his "How to Develop Spells with Effects over Time" tutorial
~ RisingDusk for GroupUtils library
~ PitzerMike for the Warcraft III Ability Guide, DestructableLib
~ TriggerHappy for TimedHandles

Please give any kind of feedback on how I could improve the spell.
This is not the first spell I have created, but it is the first spell I have uploaded to the Hive Workshop.

JASS:
//==========================================================================================
// Storm v1.04 by watermelon_1234
//******************************************************************************************
// Libraries required: (Libraries with * are optional)
//  - DestructableLib
//  - TimerUtils
//  - TimedHandles
//  - xe system (xebasic and xefx)
//  * GroupUtils
//##########################################################################################
// Importing:
//  1. Copy the ability, Diabolic Countdown.
//  2. Copy this trigger.
//  3. Implement the required libraries.
//  4. Configure the spell.
//==========================================================================================
scope Storm
    
    native UnitAlive takes unit id returns boolean // It's not neccessary, but you can remove this line if it was already declared
    
    // Provided as an easier way to use another method that detects if a unit is alive.
    private function IsUnitAlive takes unit u returns boolean
        return UnitAlive(u) // GetWidgetLife(u) > 0.405
    endfunction
 
    globals
        private constant integer    SPELL_ID            = 'A000' // The raw id of the Storm ability
        private constant real       STORM_TIME_LOOP     = 1 // How often the storm will hit.
        private constant real       FIRE_TIME_LOOP      = 0.25 // How often the fire will damage nearby enemy units. How long the FIRE_DMG_SFX will be shown is also equal to this.        
        private constant real       FIRE_AREA           = 100.  // The area where the fire will damage units.
        // SFX settings
        private constant boolean    DO_PRELOAD              = true // Determines whether or not to preload the special effects.
        private constant string     BOLT_PATH               = "FORK" // This determines what kind of lightning will be created for the bolt
        private constant real       BOLT_DUR                = 0.5 // How long the bolt will be seen
        private constant real       BOLT_HEIGHT             = 2000. // The Height, or z the bolt will be created from at its starting point
        private constant real       BOLT_DEVIATE            = 200. // How much deviation the bolt can have in its x and y from its starting point
        private constant string     IMPACT_SFX              = "Abilities\\Spells\\Human\\Thunderclap\\ThunderClapCaster.mdl" // An extra SFX that plays when the bolt is created
        private constant string     SHOCK_SFX               = "Abilities\\Weapons\\Bolt\\BoltImpact.mdl" // An extra SFX to be played when the bolt is created
        private constant string     FIRE_SFX                = "Doodads\\Cinematic\\TownBurningFireEmitter\\TownBurningFireEmitter.mdl" // Path for the fire
        private constant real       FIRE_SCALE              = .7 // Scaling for the fire sfx
        private constant string     FIRE_DMG_SFX            = "Abilities\\Spells\\Other\\ImmolationRed\\ImmolationRedDamage.mdl" // Path for the sfx that damages units by the fire. Played every FIRE_TIME_LOOP.
        private constant string     FIRE_DMG_SFX_ATTACH     = "origin" // Attachment point for the sfx.        
        // Damage Settings (Both bolt and fire)
        private constant attacktype ATK_TYPE = ATTACK_TYPE_NORMAL
        private constant damagetype DMG_TYPE = DAMAGE_TYPE_UNIVERSAL 
        private constant weapontype WPN_TYPE = null
    endglobals 
    
    // Determines which units will be damaged by the bolt. Default targets: alive, enemy units, and non-magic immune units
    private function BoltAffectedTargets takes unit targ, player owner returns boolean
        return IsUnitAlive(targ) and IsUnitEnemy(targ,owner) and not IsUnitType(targ,UNIT_TYPE_MAGIC_IMMUNE)
    endfunction   
    
    // Determines which units can be hurt by the fire. Default targets: alive, enemy units, and non-flying units
    private function FireAffectedTargets takes unit targ, player owner returns boolean
        return IsUnitAlive(targ) and IsUnitEnemy(targ,owner) and not IsUnitType(targ,UNIT_TYPE_FLYING)
    endfunction
     
    // The entire area of the spell where all units in this AoE will get hurt.
    private function FullArea takes integer lvl returns real
        return 250. +0*lvl
    endfunction 
    
    // The damage all units will receive if they're in the FullArea
    private function FullDamage takes integer lvl returns real        
        return 25.+15*lvl
    endfunction
    
    // This determines the center area where targets will get extra damage
    private function CenterArea takes integer lvl returns real        
        return 100. +0*lvl
    endfunction 
    
    // The extra damage targets will receive if they're in the CenterArea
    private function CenterDamage takes integer lvl returns real        
        return 15.+10*lvl
    endfunction    
    
    // How much damage the fire deals per second
    private constant function FireDamage takes integer lvl returns real
        return 2. + 3*lvl
    endfunction
        
    // The duration of the spell, how long the storm will last.
    private function StormDuration takes integer lvl returns real        
        return 6.
    endfunction      
    
    // How long the fire will last. 
    private function FireDuration takes integer lvl returns real
        return 4.+2*lvl
    endfunction

//==========================================================================================
// END OF CONFIGURATION
//==========================================================================================

    globals
        private rect TempRect = Rect(0,0,0,0) // Used to destroy trees in.
        private location TempLoc = Location(0,0) // Used for the lightning height.
        private group g // Used if GroupUtils is not in the map.
    endglobals  
    
    // Creates a fire whenever a tree is destroyed; damages nearby enemy ground units.
    private struct Fire
        unit cast
        xefx fire        
        real dmg
        real dur
        real count = 0
        timer tim
        static thistype temp 
        
        static method groupActions takes nothing returns boolean
            local unit u = GetFilterUnit()
            if FireAffectedTargets(u,GetOwningPlayer(temp.cast)) then
                call DestroyEffectTimed(AddSpecialEffectTarget(FIRE_DMG_SFX,u,FIRE_DMG_SFX_ATTACH),FIRE_TIME_LOOP)
                call UnitDamageTarget(temp.cast,u,temp.dmg,false,true,ATK_TYPE,DMG_TYPE,WPN_TYPE)
            endif
            set u = null
            return false
        endmethod
        
        static method onLoop takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            if .count < .dur then
                set .count = .count + FIRE_TIME_LOOP
                set temp = this
                static if LIBRARY_GroupUtils then
                    call GroupEnumUnitsInArea(ENUM_GROUP,.fire.x,.fire.y,FIRE_AREA,Filter(function thistype.groupActions))   
                else
                    call GroupEnumUnitsInRange(g,.fire.x,.fire.y,FIRE_AREA,Filter(function thistype.groupActions))   
                endif
            else
                call .fire.destroy()
                call ReleaseTimer(.tim)
                call .destroy()
            endif
        endmethod
        
        static method create takes unit c, integer l, real x, real y returns thistype
            local thistype this = thistype.allocate()
            set .cast = c
            set .dmg = FireDamage(l)*FIRE_TIME_LOOP
            set .dur = FireDuration(l)
            
            set .fire = xefx.create(x,y,0)
            set .fire.fxpath = FIRE_SFX
            set .fire.scale = FIRE_SCALE
            
            set .tim = NewTimer()
            call SetTimerData(.tim,this)
            call TimerStart(.tim,FIRE_TIME_LOOP,true,function thistype.onLoop)
            return this
        endmethod
    endstruct
    
    // The main code for the storm
    private struct Data        
        unit cast 
        integer lvl 
        real x 
        real y 
        real area 
        real count = 0 // This will be used to count how many times the spell should run
        timer tim
        static thistype temp
        
        //Starts the spell
        static method create takes unit c, real x, real y returns thistype
            local thistype this = thistype.allocate()
            set .cast = c
            set .x = x
            set .y = y
            set .lvl = GetUnitAbilityLevel(.cast,SPELL_ID)
            set .area = FullArea(.lvl)            
            set .tim = NewTimer()
            call SetTimerData(.tim,this)
            call TimerStart(.tim,STORM_TIME_LOOP,true,function thistype.onLoop)
            return this
        endmethod
        
        // This method deals with the fire created from trees
        static method treeActions takes nothing returns nothing            
            local destructable d = GetEnumDestructable()      
            if not IsDestructableDead(d) and IsDestructableTree(d) then  
                call KillDestructable(d)
                call Fire.create(temp.cast,temp.lvl,GetDestructableX(d),GetDestructableY(d))
            endif               
            set d = null
        endmethod
        
        static method groupActions takes nothing returns boolean
            local unit u = GetFilterUnit()
            if BoltAffectedTargets(u,GetOwningPlayer(temp.cast)) then
                call UnitDamageTarget(temp.cast,u,FullDamage(temp.lvl)*STORM_TIME_LOOP,false,true,ATK_TYPE,DMG_TYPE,WPN_TYPE)
                // Deals extra damage if u is still alive and within CenterArea.
                if IsUnitAlive(u) then
                    if IsUnitInRangeXY(u,temp.x,temp.y,CenterArea(temp.lvl)) then 
                        call UnitDamageTarget(temp.cast,u,CenterDamage(temp.lvl)*STORM_TIME_LOOP,false,true,ATK_TYPE,DMG_TYPE,WPN_TYPE)
                    endif
                endif
            endif
            set u = null
            return false
        endmethod
        
        // Basically where the storm is done.
        static method onLoop takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())  
            if .count < StormDuration(.lvl) then
                set .count = .count + STORM_TIME_LOOP                 
                set temp = this
                
                call SetRect(TempRect,.x-.area,.y-.area,.x+.area,.y+.area)
                call EnumDestructablesInRect(TempRect,null,function thistype.treeActions)         
                
                call MoveLocation(TempLoc,.x,.y)
                call DestroyLightningTimed(AddLightningEx(BOLT_PATH,true,GetRandomReal(-BOLT_DEVIATE,BOLT_DEVIATE)+.x,GetRandomReal(-BOLT_DEVIATE,BOLT_DEVIATE)+.y,BOLT_HEIGHT,.x,.y,GetLocationZ(TempLoc)),BOLT_DUR)
                call DestroyEffect(AddSpecialEffect(IMPACT_SFX,.x,.y))
                call DestroyEffect(AddSpecialEffect(SHOCK_SFX,.x,.y)) 
                
                static if LIBRARY_GroupUtils then
                    call GroupEnumUnitsInArea(ENUM_GROUP,.x,.y,.area,Filter(function thistype.groupActions))   
                else
                    call GroupEnumUnitsInRange(g,.x,.y,.area,Filter(function thistype.groupActions))   
                endif                
            else
                call ReleaseTimer(.tim)
                call .destroy()
            endif
        endmethod
        
        static method spellActions takes nothing returns boolean
            if GetSpellAbilityId() == SPELL_ID then 
                call thistype.create(GetTriggerUnit(),GetSpellTargetX(),GetSpellTargetY())
            endif
            return false
        endmethod
        
        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.spellActions))
            
            static if not LIBRARY_GroupUtils then
                set g = CreateGroup()
            endif
            
            static if DO_PRELOAD then
                call Preload(IMPACT_SFX)        
                call Preload(SHOCK_SFX)
                call Preload(FIRE_SFX)
                call Preload(FIRE_DMG_SFX)
            endif
        endmethod
    endstruct
endscope

1.00
Released

1.01
Revamp: Updated Actions according to TriggerHappy's Review.

1.02
Revamp: No longer uses FirstOfGroup loops. The FireImmolation ability and FireDuration can be customized by the level of the spell now.
Note that these changes do not apply to the StormBasic trigger. I probably won't update that one anymore unless someone wants me to.

1.02a
Minor Fix: Forgot to remove the unneeded variable u in method damage.

1.02b
Minor Fix: Set the t variable in Actions to null.
Description Fix: Changed comments and some names to help clarify the spell.

1.03
Revamp: Attempted to enumerate destructables in a circle by checking distance.
Minor Fix: Removed an unneeded method.

1.03a
Remove: Apparently, that was not the correct way to enumerate destructables. Commented the distance check for further references.
Tooltip Fix: Changed the tooltips to match up the fire's customization.

1.03b
No big change to the spell.
Description Fix: Capitalized all constant global variables. Removed the commented distance check for destructables. Placed a header in the spell trigger.
Test Map Change: Removed the player "Enemy."

1.03c
No change to the spell.
Updated GroupUtils. Storm no longer requires GroupEnumUnitsInArea library.

1.03d
Leak Fix: Added set D.caster = null before the struct would be destroyed.

1.04
Completely recoded to fit my current standards.
Major changes:
  • GroupUtils is now an optional requirement.
  • Uses TimedHandles instead of custom one.
  • Uses DestructableLib to detect trees.
  • Uses xefx for the fire. Allows the fire to be scaled.
  • Fire damage is now configurable within the code. It no longer requires an ability.
  • Fire damage can stack.

1.04a
Uses IsUnitInRangeXY instead of manually checking distance.



Keywords:
storm,bolt,lightning,fire,tree
Contents

Storm (Map)

Reviews
18:58, 19th Dec 2009 TriggerHappy: Review for Spell Good enough to be approved. Your actions function could be shortened to this; private function Actions takes nothing returns nothing local timer t = NewTimer() call...

Moderator

M

Moderator

18:58, 19th Dec 2009
TriggerHappy:


Review for Spell

Good enough to be approved.

  • Your actions function could be shortened to this;
    JASS:
     private function Actions takes nothing returns nothing
        local timer t = NewTimer()
        call SetTimerData(t,Data.create(GetTriggerUnit(),GetUnitAbilityLevel(GetTriggerUnit(),SpellID),GetSpellTargetX(),GetSpellTargetY()))
        call TimerStart(t,TimerLoop,true, function Data.onLoop)
    endfunction
  • FirstOfGroup loops are the slowest method of going through a
    group AFAIK. It's no big deal but you should probably know that.

Status

Feel free to message me here if you have any issues with
my review or if you have updated your resource and want it reviewed again.

Approved
 
Level 11
Joined
Sep 30, 2009
Messages
697
Your spell is ok, but you should really use a single timer for all instances instead of one per instance. Also SquareRoot isn't very good for performance.

JASS:
(GetUnitX(u)-temp.x)*(GetUnitX(u)-temp.x) + (GetUnitY(u)-temp.y)*(GetUnitY(u)-temp.y) <= CenterArea(temp.lvl)* CenterArea(temp.lvl)
Would be better I think. Also using some more locals could be better sometimes^^
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
It would become a bit more complicated and lose a little more precision to use a single timer per instance which is why I like using single timers per instance.

I'll update using that check when I can. Other than having variables for the filtered units' coordinates, did you see any other situation that would have been better if it used local variables?
I just realized I should have just used IsUnitInRangeXY since the distance check I'm doing is not even better.
 
Last edited:
Top