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

Blizzard Alternative v1.2.1

Blizzard


Calls down waves of freezing ice shards that damage units in a target area.


Obligatory Requirements

Blizzard

JASS:
//===========================================================================
// Blizzard ( Ability ) by BPower.
//     - Version 1.2.1
//===========================================================================
//
//    Calls down waves of freezing ice shards that damage units in a target area. 
//
//===========================================================================
//
//    Requirements:
//
//        CTL                 -    github.com/nestharus/JASS/tree/master/jass/Systems/ConstantTimerLoop32
//        Table               -    hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
//        SpellEffectEvent    -    hiveworkshop.com/forums/jass-resources-412/snippet-spelleffectevent-187193/ 
//
//===========================================================================
//
//    Credits:
//
//        To Nestharus for CTL
//        To Bribe for Table and SpellEffectEvent
//        To Vexorian for dummy.mdx
//
//===========================================================================

native UnitAlive takes unit id returns boolean

scope Blizzard initializer Init

    // User settings:
    // ==============
    
    globals        
        // Object editor fields:
        private constant integer    BLIZZARD_ABILITY      = 'A000'
        private constant integer    DUMMY_UNIT_ID         = 'dumi'
        
        // Damage options:
        private constant attacktype ATTACK_TYPE           = ATTACK_TYPE_NORMAL
        private constant damagetype DAMAGE_TYPE           = DAMAGE_TYPE_MAGIC
        
        // Effect options:
        private constant real       NEEDLE_FX_DURATION    = 0.8// Requires testing. Try and error.
        private constant string     NEEDLE_FX             = "war3mapImported\\SnowyBlizzardTarget.mdx"
        private constant string     GROUND_FX             = "war3mapImported\\FreezingField.mdx"
        
        // Balance options:
        // Bring structure into the chaos, by dividing the area of effect into sectors.
        // New needles will be created in random unique sectors to
        // ensure a better distribution of spawned effects.
        private constant integer    DISTRIBUTION_FACTOR   = 10
        
        // Accuracy options:
        // Maximum collision size in your map.
        private constant real       MAX_COLLISION_SIZE    = 64.
        private constant real       NEEDLE_COLLISION_SIZE = 48.
    endglobals
    
    // Filter valid targets.
    private function TargetFilter takes unit target, player owner returns boolean
        return UnitAlive(target) and not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) and IsUnitEnemy(target, owner)
    endfunction
    
    // Returns the damage amount per needle. 
    private function GetImpactDamage takes integer level returns real
        return GetRandomReal(10., 15.) + 20.*level
    endfunction
    
    // Returns the amount of needles created per seconds. 
    private constant function GetNeedlesPerSecond takes integer level returns integer
        return 1 + 2*level
    endfunction
    
    // Returns the total spell duration in seconds.
    private constant function GetSpellDuration takes integer level returns real
        return 3. + 2.*level
    endfunction
     
    // Returns the radius of the blizzard field.
    private constant function GetFieldRadius takes integer level returns real
        return 200. + 50.*level
    endfunction
    
    // Returns the duration of the frost buff.
    private constant function GetBuffDuration takes integer level returns real
        return 4. + 0.*level
    endfunction
    
    // Returns the proper scaling of the ground effect. Eventually requires try and error.
    private constant function GetFieldEffectScale takes real fieldSize returns real
        return fieldSize/400.
    endfunction
    
    private function IsChanneling takes unit caster, integer level returns boolean
        return true
    endfunction
    
    // Global condition to keep the spell running.
    private function CasterCondition takes unit caster returns boolean
        return UnitAlive(caster)
    endfunction
    
    // The following struct "Buff" creates the buff- and frostnova effect.
    // Adjust the constants to your needs.     
    private struct Buff 
        // Buff type settings. 
        private static constant integer BUFF_ORDER_ID  = 852226
        private static constant integer BUFF_ABILITY   = 'A001'
        private static constant integer BUFF           = 'Bfro'
        //
        private static constant integer CASTER_UNIT_ID = 'n000'
        private static constant player  NEUTRAL_PLAYER = Player(bj_PLAYER_NEUTRAL_EXTRA)
        private static constant timer   TIMER          = CreateTimer()
        private static constant real    TIMER_TIMEOUT  = .03125
        private static constant integer LOCUST_ABILITY = 'Aloc'
    
        private static thistype array prev
        private static thistype array next
    
        private static unit  caster 
        private static Table table
    
        private unit source
        private real time
        
        static method unitHasBuff takes unit whichUnit returns boolean
            return GetUnitAbilityLevel(whichUnit, BUFF) != 0
        endmethod
        
        method destroy takes nothing returns nothing
            call UnitRemoveAbility(source, BUFF)
            call table.remove(GetHandleId(source))
            call deallocate()
        
            set next[prev[this]] = next[this]
            set prev[next[this]] = prev[this]
            if next[0] == 0 then
                call PauseTimer(TIMER)
            endif
            
            set source = null
        endmethod
    
        private static method onPeriodic takes nothing returns nothing
            local thistype this = next[0]
        
            loop
                exitwhen this == 0
                if time > 0. and UnitAlive(source) and unitHasBuff(source) then
                    set time = time - TIMER_TIMEOUT
                else
                     call destroy()
                endif
                set this = next[this]
            endloop
        endmethod
        
        static method apply takes player owner, unit target, real duration returns nothing
            local integer  id = GetHandleId(target)
            local thistype this 
        
            if table.has(id) then
                set this = table[id]
                if unitHasBuff(source) then
                    set time = duration
                    return
                endif
                call destroy()
            endif
        
            call IssueTargetOrderById(caster, BUFF_ORDER_ID, target)
            set this = allocate()
            set table[id] = this
            set source = target
            set time = duration
            
            set next[this] = 0
            set prev[this] = prev[0]
            set next[prev[0]] = this
            set prev[0] = this
            if prev[this] == 0 then
                call TimerStart(TIMER, TIMER_TIMEOUT, true, function thistype.onPeriodic)
            endif
        endmethod
    
        private static method onInit takes nothing returns nothing
            set table = Table.create()
            set caster = CreateUnit(NEUTRAL_PLAYER, CASTER_UNIT_ID, 0., 0., 0.)
            call SetUnitPosition(caster, 2147483647, 2147483647)
            call UnitAddAbility(caster, BUFF_ABILITY)
            call UnitAddAbility(caster, LOCUST_ABILITY)
            call ShowUnit(caster, false)
        endmethod
    endstruct
    
//================================================================
// Blizzard source code. Make changes carefully.
//================================================================
    
    private function Random takes nothing returns real
        return GetRandomReal(0., 1.)
    endfunction
    
    private function GetRandomRange takes real radius returns real
        local real r = Random() + Random()
        if r > 1. then 
            return (2 - r)*radius
        endif
        return r*radius
    endfunction

    private struct Needle
        real x
        real y
        real time
        
        static method create takes real centerX, real centerY, real maxRange, integer factor returns thistype
            local thistype this = thistype.allocate()
            local real spacing  = 2*bj_PI/DISTRIBUTION_FACTOR
            local real min      = spacing*factor
            local real max      = min + spacing
            local real theta    = GetRandomReal(min, max)
            local real radius   = GetRandomRange(maxRange)
            
            set x    = centerX + radius*Cos(theta)
            set y    = centerY + radius*Sin(theta)
            set time = NEEDLE_FX_DURATION
            call DestroyEffect(AddSpecialEffect(NEEDLE_FX, x, y))
            
            return this
        endmethod
        
    endstruct
    
    private struct Blizzard extends array
        static constant group   ENUM_GROUP       = CreateGroup()
        static constant real    TIMER_TIMEOUT    = .03125
        static constant real    DUMMY_DEATH_TIME = .01
        static constant integer TIMED_LIFE       = 'BTLF'
        static constant integer TABLE_OFFSET     = 0x2710
        
        Table   table
        integer size
        integer sectors

        unit    source
        unit    dummy
        effect  fx
        player  user
        
        real    radius
        real    time
        real    timeout
        real    centerX
        real    centerY
        real    duration
        integer level
        integer order 
        boolean channel
    
        method getRandomSector takes nothing returns integer
            local integer random
            local integer sector
            
            if DISTRIBUTION_FACTOR == 1 then
                return 0
            
            // Fills up the table with unique integers from 0 to DISTRIBUTION_FACTOR.
            elseif sectors == 0 then
                loop
                    exitwhen sectors == DISTRIBUTION_FACTOR
                    set table[TABLE_OFFSET + sectors] = sectors
                    set sectors = sectors + 1
                endloop
            endif
            
            // Get an unique integer from the table.
            set sectors = sectors - 1
            set random  = GetRandomInt(0, sectors) 
            set sector  = table[TABLE_OFFSET + random]
            set table[TABLE_OFFSET + random] = table[TABLE_OFFSET + sectors]
            
            return sector
        endmethod
    
        implement CTL
            local unit u
            local Needle needle
            local integer index
            local real damage
            local real slow
        implement CTLExpire
        
            if not CasterCondition(source) or duration <= 0. or (channel and GetUnitCurrentOrder(source) != order) then
                if size == 0 then
                    call destroy()
                    call table.destroy()
                    call DestroyEffect(fx)
                    call UnitApplyTimedLife(dummy, TIMED_LIFE, DUMMY_DEATH_TIME)
                    
                    set fx = null
                    set user = null
                    set dummy = null
                    set source = null
                endif
            else
                set duration = duration - TIMER_TIMEOUT
                set time = time + TIMER_TIMEOUT
                loop
                    exitwhen time < timeout
                    set table[size] = Needle.create(centerX, centerY, radius, getRandomSector())
                    set size = size + 1
                    set time = time - timeout
                endloop
            endif

            // Loop over all falling needles.
            set index = 0
            loop
                exitwhen index == size
                set needle = table[index]
                
                if needle.time > 0. then
                    set needle.time = needle.time - TIMER_TIMEOUT
                    set index = index + 1
                else
                    set damage = GetImpactDamage(level)
                    set slow   = GetBuffDuration(level)
                
                    call GroupEnumUnitsInRange(ENUM_GROUP, needle.x, needle.y, NEEDLE_COLLISION_SIZE + MAX_COLLISION_SIZE, null)
                    loop
                        set u = FirstOfGroup(ENUM_GROUP)
                        exitwhen u == null
                        call GroupRemoveUnit(ENUM_GROUP, u)
    
                        if IsUnitInRangeXY(u, needle.x, needle.y, NEEDLE_COLLISION_SIZE*1.5) and TargetFilter(u, user) then
                            if UnitDamageTarget(source, u, damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, null) then
                                call Buff.apply(user, u, slow)
                            endif
                        endif
                    endloop
                    
                    call needle.destroy()
                    set size = size - 1
                    set table[index] = table[size]
                endif
            endloop
        implement CTLEnd
    
        static method onEffect takes nothing returns nothing
            local thistype this = thistype.create()
            
            set user     = GetTriggerPlayer()
            set source   = GetTriggerUnit()
            set centerX  = GetSpellTargetX()
            set centerY  = GetSpellTargetY()
            set level    = GetUnitAbilityLevel(source, BLIZZARD_ABILITY)
            set radius   = GetFieldRadius(level)
            set duration = GetSpellDuration(level)
            set timeout  = 1./IMaxBJ(1, GetNeedlesPerSecond(level))
            set channel  = IsChanneling(source, level)
            set order    = GetUnitCurrentOrder(source)
            set dummy    = CreateUnit(user, DUMMY_UNIT_ID, centerX, centerY, 2*bj_PI*Random())
            set fx       = AddSpecialEffectTarget(GROUND_FX, dummy, "origin")
            set table    = Table.create()
            set time     = 0.
            set sectors  = 0// Available sectors. Like cutting a piece of cake.
            call SetUnitScale(dummy, GetFieldEffectScale(radius), 0., 0.)
        endmethod

    endstruct
    
    private function Init takes nothing returns nothing
        debug local string text
        debug if DISTRIBUTION_FACTOR <= 0 then
            debug set text = "|cffffa500Error:|r |cff99b4d1In trigger Blizzard, scope Blizzard!|r\n"
            debug set text = text + "|cffffa500Global user settings: |cff99b4d1constant integer DISTRIBUTION_FACTOR = " + I2S(DISTRIBUTION_FACTOR) + "!|r\n" 
            debug set text = text + "|cffffa500Solution: |cff99b4d1DISTRIBUTION_FACTOR must be bigger than 0!|r"
        
            debug call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.00, 0.00, 3600.00, text)
        debug endif
        //
        call RegisterSpellEffectEvent(BLIZZARD_ABILITY, function Blizzard.onEffect)
    endfunction
    
endscope

Credits:
Nestharus, Bribe​

Keywords:
Blizzard, ice, snow, cold, slow
Contents

Blizzard ( Ability ) (Map)

Reviews
Blizzard Alternative v1.0.0.0 | Reviewed by Maker | 02.01.14 Concept[/COLOR]] Recreating an already existing spell is not extremely exciting but it can be useful since one has more flexibility over the standard spell The standard...

Moderator

M

Moderator


Blizzard Alternative v1.0.0.0 | Reviewed by Maker | 02.01.14

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

Concept[/COLOR]]
126248-albums6177-picture66521.png
  • Recreating an already existing spell is not extremely exciting
    but it can be useful since one has more flexibility over the
    standard spell
126248-albums6177-picture66523.png
  • The standard Blizzard is channeled so you could add an option for that
Triggers[/COLOR]]
126248-albums6177-picture66521.png
  • Very good coding. High performance, leakless, MUI
    and overall very clear
126248-albums6177-picture66523.png
  • Regarding damage types, cold, fire, poison etc. are all of the same main category, magical.
    I recommend using DAMAGE_TYPE_MAGIC,
    but it works just the same as cold
  • Nulling the local trigger is not needed
    but it won't make a difference if you do null it
Objects[/COLOR]]
126248-albums6177-picture66521.png
  • Everything seems to be in order here
    Though it is a bit weird that the hotkey is not coloured
    in the learned tooltip
Effects[/COLOR]]
126248-albums6177-picture66521.png
  • The effects are fine, could not ask for more
Rating[/COLOR]]
CONCEPTTRIGGERSOBJECTSEFFECTSRATINGSTATUS
126248-albums6177-picture75358.jpg
126248-albums6177-picture75360.jpg
126248-albums6177-picture75360.jpg
126248-albums6177-picture75360.jpg
126248-albums6177-picture75359.jpg
APPROVED
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I personally would just hide the unit on initialization rather than settings it's position to the edge of the map.
Hidden units can't issue orders (going to test that). So I would have to check for every spell cast if the unit is hidden or run a timer after map init and unhide it.

SetUnitPos is called one time per game, it shouldn't matter at all nor noticeable prolong the loading time.
 
Level 16
Joined
Dec 15, 2011
Messages
1,423
Hello.

I just came across this one by chance and I noticed that it is quite similiar to my own http://www.hiveworkshop.com/forums/spells-569/jass-freezing-field-v1-1d-228101/. Reckon that it is the "written once before" spell you mentioned. I may be wrong though so please forgive if I misunderstood.

This spell has overall better performance than mine for sure but I think that the ice shards' positioning could be improved a bit so that it can spread equally around the AoE (which more or less lessens the chance an enemy can pass through this unharmed). I am perfectly alright if you don't approve of my suggestion though, don't worry :)
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Hi Doomlord,

indeed it is your resource I am refering to. After checking a lot of potential Blizzard ice shard models, I didn't found one which could be used for a acceptable missle.

So I sticked to the idea you already had, by just using an effect and making the falling time not configurable.

... the ice shards' positioning could be improved a bit so that it can spread equally around the AoE ...
Yes true I'll think about it, shouldn't be too difficult. :)

If you want I can add a link to your spell in the description as good alternative. Afterall it has no requirements and is written in JASS which can be plus.
 
Level 16
Joined
Dec 15, 2011
Messages
1,423
Hi Doomlord,

indeed it is your resource I am refering to. After checking a lot of potential Blizzard ice shard models, I didn't found one which could be used for a acceptable missle.

So I sticked to the idea you already had, by just using an effect and making the falling time not configurable.

If you want I can add a link to your spell in the description as good alternative. Afterall it has no requirements and is written in JASS which can be plus.

If that is alright with you then I am more than willing to accept. Thank you :)

Good luck with your work on this spell :grin:
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I was quite sure, that I'll never update this spell ... :/

Small update:

1. The spell can now also be channeling, similar to the Archmage Blizzard spell.

2. Improved documentation.

3. MissileRecycler is now an optional requirement. This will allow you to scale the ground effect.

4. Slightly improved the code.
JASS:
        static if not LIBRARY_SpellEffectEvent then
            private static method condition takes nothing returns boolean
                return (GetSpellAbilityId() == ABILITY_ID) and thistype.onCast()
            endmethod
        endif

5. Removed alloc module as requirement.

6. Cleaned up the demo map a bit.

7. Imapct damage is now more dynamic, as it is computated now everytime before the group enum, instead of once on cast.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Changelog:
  • Distribution is now circular and uniform. Thanks to Flux.
  • The dummy recycler is no longer required. It's just one unit per spell cast.
  • I put the buff into a seperate struct to allow better user configuration.
  • Added a distribution factor to add a balancing factor.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Unlike in the classic blizzard spell, the effects are chaotic in this one.
They can pop up anywhere in the circle around the cast point.
So eventually two effects overlap or are close to each other.

What is new in this update is that you can cut the circle/cake into equal pieces.
I named the variable distribution factor. Let's take a value of 3.
The result is that the next 3 needles are spawned somewhere in
piece of cake 1,2 and 3. One needle per sector.

153757d1460201147-blizzard-alternative-v1-2-fractionparts.png]


JASS:
set table[size] = Needle.create(centerX, centerY, radius, getRandomSector())

The sectors are not returned clockwise or anticlockwise, but
from a bucket of unique integers.
For the example above it could return: 0-2-1 ... 0-1-2 ... 2-1-0 etc
TABLE_OFFSET is just there because I store also all needles of this instance in the same table.
JASS:
        method getRandomSector takes nothing returns integer
            local integer random
            local integer sector
            
            if DISTRIBUTION_FACTOR == 1 then
                return 0
            
            // Fills up the table with unique integers from 0 to DISTRIBUTION_FACTOR.
            elseif sectors == 0 then
                loop
                    exitwhen sectors == DISTRIBUTION_FACTOR
                    set table[TABLE_OFFSET + sectors] = sectors
                    set sectors = sectors + 1
                endloop
            endif
            
            // Get an unique integer from the table.
            set sectors = sectors - 1
            set random  = GetRandomInt(0, sectors) 
            set sector  = table[TABLE_OFFSET + random]
            set table[TABLE_OFFSET + random] = table[TABLE_OFFSET + sectors]
            
            return sector
        endmethod

The following is the creation of a needle.
JASS:
        static method create takes real centerX, real centerY, real maxRange, integer sector returns thistype
            local thistype this = thistype.allocate()
            local real spacing  = 2*bj_PI/DISTRIBUTION_FACTOR
            local real min      = spacing*sector
            local real max      = min + spacing
            local real theta    = GetRandomReal(min, max)
            local real radius   = GetRandomRange(maxRange)
            
            set x    = centerX + radius*Cos(theta)
            set y    = centerY + radius*Sin(theta)
            set time = NEEDLE_FX_DURATION
            call DestroyEffect(AddSpecialEffect(NEEDLE_FX, x, y))
            
            return this
        endmethod

Edit:

The buff duration is not configurable and working properly.
 

Attachments

  • FractionParts.png
    FractionParts.png
    3.4 KB · Views: 145
Last edited:
Level 4
Joined
Oct 9, 2013
Messages
84
shiet I don't understand this coding thing, it would be easier if you guys make simple triggers instead of text coding
 
Level 39
Joined
Feb 27, 2007
Messages
4,992
shiet I don't understand this coding thing, it would be easier if you guys make simple triggers instead of text coding
No, trying to make this sort of a spell with a "simple trigger" is massively more complicated for the author. GUI forces you to bend over backwards to do things that JASS can do with ease (see: timers). You don't have to understand the underlying code to change the configuration section and use a resource like this.
 
Top