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

Frostmyr v1.7

Intro
Hello. Have been a while since the last time I submitted a spell. This one is inspired by one of abilities in Digimon Rumble Arena on PlayStation 1. I forgot what's the name but it's one of Sakuyamon's ability. Where she summons and controls some swirling projectiles thingy.

This is the nastiest spell I have ever made so far, needs more than 3 days just to make the movement and the effect looks right. It's a bit weird tho to use "Frostmyr" for the name as it's my favorite nickname I always use in several online games I play. However, the name was actually designed for a relic weapon in my project, Guild Master. But meh, I will use it anyway for the spell name since I'm out of idea.

Basically, Frostmyr is a unique spell where the caster are able to control the movement of the spell (projectiles).
Description
The caster summons and controls 5 freezing projectiles which will periodically combusts his mana as cost. Every projectile deals damage periodically to nearby opponents. While controlling these projectiles, the caster will be unable to move and attack until it's ordered to stop. The spell will also stop once the caster's mana is depleted or it's dead.

Level 1 - Burns 20 mana points per second. Deals 80 damage per second.
Level 2 - Burns 30 mana points per second. Deals 160 damage per second.
Level 3 - Burns 40 mana points per second. Deals 240 damage per second.
To control the projectile you need to have the caster in your selection. Then simply right-click on the target location/unit. If you have a unit as target, the projectile will automatically follow it until it's dead.

Requirements
(Required)
• TimerUtils
• Stack
• WorldBounds
• UnitIndexer
• UnitZ
• RSound
(Optional)
• AutoFly

by Vexorian
by Nestharus
by Nestharus
by Nestharus
by Garfield1337
by Quilnez


by Nestharus

| wc3c.net/showthread.php?t=101322
| github.com/nestharus/JASS/tree/master/jass/Data%20Structures/Stack
| github.com/nestharus/JASS/tree/master/jass/Systems/WorldBounds
| hiveworkshop.com/forums/spells-569/unit-indexer-v5-3-0-1-a-260859/
| hiveworkshop.com/forums/jass-resources-412/snippet-getterrainz-unitz-236942/
| hiveworkshop.com/threads/snippet-rapidsound.258991/


| github.com/nestharus/JASS/tree/master/jass/Systems/AutoFly
How to Install
• Export all files in Import Manager to your map
• Copy dummy unit, main spell, and cancel spell to your map
• Copy Frostmyr and Scripts folders at Trigger Editor to your map
• Just delete duplicated external scripts
• Install all used scripts (libraries) properly
• Configure the spell properly (spell id, dummy id, etc.)
Code
JASS:
scope Frostmyr

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//                          Frostmyr v1.7
//                          ¯¯¯¯¯¯¯¯¯¯¯¯¯
//                       Created by: Quilnez
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//      1. Description
//          The caster summons and controls 5 freezing projectiles which will  periodically
//          combusts his mana as cost. Every projectile deals damage periodically to nearby
//          opponents.  While controlling these projectiles, the caster will  be unable  to
//          move  and attack until it's ordered to stop. The spell will also stop once  the
//          caster's mana is depleted or it's dead.
//
//              Level 1 - Burns 20 mana points per second. Deals 80  damage per second.
//              Level 2 - Burns 30 mana points per second. Deals 160 damage per second.
//              Level 3 - Burns 40 mana points per second. Deals 240 damage per second.
//
//          To control the  projectile you need to have the caster in your selection.  Then
//          simply  right-click on the target location/unit. If you have a unit as  target,
//          the projectile will automatically follow it until it's dead.
//
//      2. External scripts
//          (Required)
//              • TimerUtils    by Vexorian     | wc3c.net/showthread.php?t=101322
//              • Stack         by Nestharus    | github.com/nestharus/JASS/tree/master/jass/Data%20Structures/Stack
//              • World Bounds  by Nestharus    | github.com/nestharus/JASS/tree/master/jass/Systems/WorldBounds
//              • Unit Indexer  by Nestharus    | hiveworkshop.com/forums/spells-569/unit-indexer-v5-3-0-1-a-260859/
//              • UnitZ         by Garfield1337 | hiveworkshop.com/forums/jass-resources-412/snippet-getterrainz-unitz-236942/
//              • RapidSound    by Quilnez      | hiveworkshop.com/threads/snippet-rapidsound.258991/
//          (Optional)
//              • AutoFly       by Nestharus    | github.com/nestharus/JASS/tree/master/jass/Systems/AutoFly
//
//      3. How to Install
//          • Export all files in Import Manager to your map
//          • Copy dummy unit, main spell, and cancel spell to your map
//          • Copy Frostmyr and Scripts folders at Trigger Editor to your map
//          • Just delete duplicated external scripts
//          • Install all used scripts (libraries) properly
//          • Configure the spell properly (spell id, dummy id, etc.)
//
//      4. Credits
//          • BTNIce_by67chrome.blp by 67chrome
//          • Mini Comet.mdx        by 00110000
//          • dummy.mdx             by Vexorian
//          • Cold Wind ambient     by Google.com
//
//      5. Link
//          Visit this link to check out the newest update of this spell:
//            hiveworkshop.com/forums/spells.php?id=ixhbcf
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//                          CONFIGURATION
//                          ¯¯¯¯¯¯¯¯¯¯¯¯¯
        globals
//
//          A. General
//
//          1. Main spell's raw code
            private constant integer SPELL_ID       = 'A000'
//
//          2. Cancel spell's raw code
            private constant integer CANCEL_ID      = 'A001'
//
//          3. Dummy unit's raw code
            private constant integer DUMMY_ID       = 'h000'
//
//          4. Order id used to give order to missiles
            private constant integer ORDER_ID       = 851971 //smart
//
//          5. Played animation for caster
            private constant string  ANIMATION      = "channel"
//
//          6. Created special effect whenever a unit takes damage from spell
            private constant string  DAMAGE_SFX     = "war3mapImported\\ForstmyrTarget.mdx"
            private constant string  DAMAGE_SFX_PT  = "chest"
//
//          7. Played sound effect whenever a unit takes damage from spell
            private constant string  DAMAGE_SOUND   = "war3mapImported\\FrostmyrDamage.wav"
            private constant integer DSOUND_VOLUME  = 127
//
//          8. Times given for special effects to decay
            private constant real    SFX_DECAY_TIME = 5.0
//
//          9. Sound effect for missiles
            private constant string  SOUND_PATH     = "war3mapImported\\FrostmyrAmbient.wav"
            private constant real    PITCH          = 0.5
            private constant integer VOLUME         = 65
//
//          10. Dealt damage configurations
            private constant attacktype ATTACK_TYPE = ATTACK_TYPE_NORMAL
            private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_COLD
            private constant weapontype WEAPON_TYPE = WEAPON_TYPE_WHOKNOWS
//
//          B. Missile
//
//          1. Missile's model filepath
            private constant string  MODEL_PATH     = "war3mapImported\\Mini Comet.mdx"
//
//          2. If true missiles will be able to enter idle state
//              - Makes it easier to control
//              - Increases damage efficiency
            private constant boolean ALLOW_IDLE     = true
//
//          3. Default fly height for missiles
            private constant real    Z_OFFSET       = 125.0
//
//          4. Missile trajectory configurations
            private constant real    LENGTH         = 100.0
            private constant real    WIDTH          = 100.0
            private constant real    HEIGHT         = 100.0
//
//          5. Movespeed for missiles
            private constant real    MOVE_RATE      = 13.0
//
//          6. Turn rate for missiles
            private constant real    TURN_RATE      = 3.0*bj_DEGTORAD
//
//          7. Initial offset for missiles
            private constant real    SPAWN_OFFSET   = 100.0
//
//          8. Rotate (spin) rate for missiles
//              - Use negative value to inverse the rotation
            private constant real    SPIN_RATE      = 5*bj_DEGTORAD
//
//          9. Speed factor when missiles deaccelerate
//              - Only takes effect if ALLOW_IDLE is true
//              - Must be between 0 ~ 1
//              - The higher the slower
            private constant real    SLOW_RATE      = 0.95
//
//          10. Maximum range between missile and target to deal damage
            private constant real    COLLISION_SIZE = 120.0
//
//          Better not to touch this
            private constant real    INTERVAL       = 0.03125
//
        endglobals
//
//          C. Non-constant
//
//          1. Created missile per cast
            private function MissileCount takes integer level returns integer
                return 5
            endfunction
//
//          2. Damage dealt by every single missile
            private function DamageAmount takes integer level returns real
                return 10.0 * level
            endfunction
//
//          3. Delay before next damage will be dealt
            private function DamageDelay takes integer level returns real
                return 0.125
            endfunction
//
//          4. Combusted mana amount every certain times
            private function ManaCost takes integer level returns real
                return 2.5 + 2.5 *level
            endfunction
//
//          5. Delay between mana reduction
            private function CostDelay takes integer level returns real
                return 0.25
            endfunction
//
//          6. Target classification that can be hit by this spell
            private function filterTarget takes player caster, unit target returns boolean
                return IsUnitEnemy(target, caster) and not(IsUnitType(target, UNIT_TYPE_STRUCTURE) or IsUnitType(target, UNIT_TYPE_MECHANICAL))
            endfunction
//
//          7. This function let you to set up what condition to stop the channel
//             The caster will stop channeling when this function returns true
            private function stopChannel takes unit caster returns boolean
                return GetUnitAbilityLevel(caster, 'BSTN') > 0 // Stop when the caster is stunned
            endfunction
//
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//                          EVENT CATCHER
//                          ¯¯¯¯¯¯¯¯¯¯¯¯¯
        private module EventCatcher
//          Called whenever a unit takes damage from this spell
//              - caster : caster of the spell (damage source)
//              - target : damaged unit
//              - level  : caster's Frostmyr ability level
            static method onDamage takes unit caster, unit target, integer level returns nothing
            endmethod
        endmodule
//
//
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
    native UnitAlive takes unit id returns boolean
 
    private module InitModule
        private static method onInit takes nothing returns nothing
            call init()
        endmethod
    endmodule
 
    private struct FrostmyrMissile extends array
 
        effect sfx
        unit missile
     
        real hOffset
        real vOffset
     
        real hMax
        real vMax
     
        implement Stack
     
    endstruct
 
    private struct Frostmyr
     
        real locX
        real locY
        real targetX
        real targetY
        real missileX
        real missileY
     
        real angle
        real hOffset
        real vOffset
        real damage
        real damageDelay
        real damageDelayX
        real manaCost
        real costDelay
        real costDelayX
     
        real missileSpin
        real missileDistance
        real missileDistanceX
        real missileHPos
        real missileVPos
        real missileAngle
     
        boolean idle
        integer count
        integer level
     
        sound  sfx
        player owner
        unit   caster
        unit   target
        timer  tick
     
        FrostmyrMissile stack
     
        static RSound SoundFx
        static trigger OrderTrigg= CreateTrigger()
        static group TempGroup   = CreateGroup()
        static integer array Index
        static constant real TAU = bj_PI*2
        static constant real HP  = bj_PI/2
     
        implement optional EventCatcher
     
        method destroy takes nothing returns nothing
         
            local FrostmyrMissile node = .stack.first
         
            // Kill missiles
            loop
                exitwhen node == 0
                call UnitApplyTimedLife(node.missile, 'BTLF', SFX_DECAY_TIME)
                call DestroyEffect(node.sfx)
                set node.missile = null
                set node.sfx     = null
                set node         = node.next
            endloop
         
            call ReleaseTimer(.tick)
            call StopSound(.sfx, true, true)
            call .stack.destroy()
            call deallocate()
            // Remove leaks
            set .caster = null
            set .target = null
            set .tick   = null
            set .sfx    = null
         
        endmethod
     
        method stop takes nothing returns nothing
         
            // Reset animation if not dead
            if UnitAlive(.caster) then
                call SetUnitAnimation(.caster, "stand")
            endif
            set Index[GetUnitUserData(.caster)] = 0
            call UnitRemoveType(.caster, UNIT_TYPE_PEON)
            call UnitRemoveAbility(.caster, CANCEL_ID)
            call SetUnitAbilityLevel(.caster, SPELL_ID, .level)
            call destroy()
         
        endmethod
     
        static method onPeriodic takes nothing returns nothing
         
            local FrostmyrMissile node
            local thistype this = GetTimerData(GetExpiredTimer())
            local boolean  b
            local unit     u
         
            local real     a
            local real     a2
            local real     as
            local real     d
            local real     f
            local real     h
            local real     m
         
            local real     v
            local real     x
            local real     y
            local real     z
            local real     x2
            local real     y2
            local real     z2
            local real     sf
         
            // If have to stop channeling
            if not UnitAlive(.caster) or stopChannel(.caster) then
                call stop()
                return
            endif
         
            if .target != null then
                if UnitAlive(.target) then
                    set .targetX = GetUnitX(.target)
                    set .targetY = GetUnitY(.target)
                else
                    set .target  = null
                endif
            endif
         
            // Check if the projectiles need to move
            set b = not ALLOW_IDLE or (.targetX-.locX)*(.targetX-.locX)+(.targetY-.locY)*(.targetY-.locY) >= MOVE_RATE*MOVE_RATE
            if b then
                set a = Atan2(.targetY-.locY, .targetX-.locX)
                if TURN_RATE > 0 and Cos(.angle-a) < Cos(TURN_RATE) then
                    if Sin(a-.angle) >= 0 then
                        set .angle = .angle + TURN_RATE
                    else
                        set .angle = .angle - TURN_RATE
                    endif
                else
                    set .angle = a
                endif
                set .locX = .locX + MOVE_RATE * Cos(.angle)
                set .locY = .locY + MOVE_RATE * Sin(.angle)
                // Stop if the order point exceeds map boundaries
                if .locX >= WorldBounds.maxX or .locX <= WorldBounds.minX or .locY >= WorldBounds.maxY or .locY <= WorldBounds.minY then
                    call stop()
                    return
                endif
                call SetSoundPosition(.sfx, .locX, .locY, Z_OFFSET)
            endif
         
            set .damageDelay = .damageDelay - INTERVAL
            set .costDelay   = .costDelay - INTERVAL
            if .costDelay <= 0 then
                set m = GetUnitState(.caster, UNIT_STATE_MANA) - .manaCost
                call SetUnitState(.caster, UNIT_STATE_MANA, m)
                set .costDelay = .costDelayX
                if m <= 0 then
                    call stop()
                    return
                endif
            endif
         
            // If idle calculate deaccelerated speed
            if .idle then
                set sf = missileDistanceX/2
                set sf = 1-RAbsBJ(.missileDistance-sf)/sf*SLOW_RATE
            else
                set sf = 1
            endif
            set .missileDistance = .missileDistance + MOVE_RATE*sf
            if .missileDistance > .missileDistanceX then
                set .missileDistance = .missileDistanceX
            endif
         
            // Adjust control angle
            set a = Atan2(.locY-.missileY, .locX-.missileX)
            if TURN_RATE > 0 and Cos(.missileAngle-a) < Cos(TURN_RATE) then
                if Sin(a-.missileAngle) >= 0 then
                    set .missileAngle = .missileAngle + TURN_RATE
                else
                    set .missileAngle = .missileAngle - TURN_RATE
                endif
            else
                set .missileAngle = a
            endif
         
            // Update missiles
            set .missileSpin = .missileSpin + SPIN_RATE*.missileHPos
            set f    = (.missileDistanceX-.missileDistance)*(.missileDistance/.missileDistanceX)
            set node = .stack.first
            set as   = TAU/.count
            set a    = as /2
            loop
                exitwhen node == 0
             
                set node.hMax = WIDTH *Sin(a+.missileSpin)
                set node.vMax = HEIGHT*Cos(a+.missileSpin)
             
                set h  = (4*node.hMax/.missileDistanceX)*f
                set v  = (4*node.vMax/.missileDistanceX)*f
                set d  = SquareRoot(.missileDistance*.missileDistance+h*h)
                set a2 = .missileAngle-Atan(h/.missileDistance)
                set x  = .missileX + d * Cos(a2)
                set y  = .missileY + d * Sin(a2)
             
                if x < WorldBounds.maxX and x > WorldBounds.minX and y < WorldBounds.maxY and y > WorldBounds.minY then
                    call ShowUnit(node.missile, true)
                    call SetUnitX(node.missile, x)
                    call SetUnitY(node.missile, y)
                    call SetUnitFacing(node.missile, .missileAngle*bj_RADTODEG)
                    call SetUnitFlyHeight(node.missile, Z_OFFSET+v*.missileVPos, 0)
                 
                    set z  = GetUnitZ(node.missile)
                    if .damageDelay <= 0 then
                        call GroupEnumUnitsInRange(TempGroup, x, y, COLLISION_SIZE, null)
                        loop
                            set u = FirstOfGroup(TempGroup)
                            exitwhen u == null
                            call GroupRemoveUnit(TempGroup, u)
                            if UnitAlive(u) and filterTarget(.owner, u) then
                                set x2 = GetUnitX(u)
                                set y2 = GetUnitY(u)
                                set z2 = GetUnitZ(u)
                                set d  = SquareRoot((x-x2)*(x-x2)+(y-y2)*(y-y2))
                                set h  = RAbsBJ(z2-z)
                                // Spherical collision
                                if d*d+h*h < COLLISION_SIZE*COLLISION_SIZE then
                                    static if thistype.onDamage.exists then
                                        call onDamage(.caster, u, .level)
                                    endif
                                    call SoundFx.play(x2, y2, z2, DSOUND_VOLUME)
                                    call UnitDamageTarget(.caster, u, .damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
                                    call DestroyEffect(AddSpecialEffectTarget(DAMAGE_SFX, u, DAMAGE_SFX_PT))
                                endif
                            endif
                        endloop
                    endif
                else
                    // Temporary hide missile if exceeds map boundaries
                    call ShowUnit(node.missile, false)
                endif
             
                set a = a + as
                set node = node.next
            endloop
         
            if .damageDelay <= 0 then
                set .damageDelay = .damageDelayX
            endif
         
            if .missileDistance == .missileDistanceX then
                set .missileX        = x
                set .missileY        = y
                set .missileAngle    = Atan2(.locY-.missileY, .locX-.missileX)
                set .missileDistance = 0
                set .missileVPos     = -.missileVPos
                if b then
                    set .idle = false
                    set .missileHPos = -.missileHPos
                else
                    set .idle = true
                    set .missileHPos = 1
                endif
            endif
         
        endmethod
     
        static method orderStop takes nothing returns nothing
         
            local timer    t    = GetExpiredTimer()
            local thistype this = GetTimerData(t)
         
            call DisableTrigger(OrderTrigg)
            call IssueImmediateOrder(.caster, "stop")
            call SetUnitAnimation(.caster, ANIMATION)
            call EnableTrigger(OrderTrigg)
            call ReleaseTimer(t)
            set t = null
         
        endmethod
     
        static method onOrder takes nothing returns boolean
     
            local thistype this = Index[GetUnitUserData(GetTriggerUnit())]
            local integer  id
            local widget   w
            local unit     u
         
            // If unit is casting Frostmyr
            if this != 0 then
                set id = GetIssuedOrderId()
                if id == ORDER_ID then
                    set u = GetOrderTargetUnit()
                    if u == null then
                        set w = GetOrderTarget()
                        if w == null then
                            set .targetX = GetOrderPointX()
                            set .targetY = GetOrderPointY()
                        else
                            set .targetX = GetWidgetX(w)
                            set .targetY = GetWidgetY(w)
                        endif
                        set .target = null
                        set w = null
                    else
                        set .target = u
                        set u = null
                    endif
                    set .idle = false
                endif
                // This is done because seems like immediate (no target) order
                // is somehow unable to prevent attack move order immediately
                if id == 851983 then // If order is "attack"
                    call IssuePointOrder(.caster, "move", 0, 0)
                else
                    call TimerStart(NewTimerEx(this), 0, false, function thistype.orderStop)
                endif
            endif
         
            return false
        endmethod
     
        static method onCast takes nothing returns boolean
         
            local FrostmyrMissile node
            local thistype this
            local integer data = GetUnitUserData(GetTriggerUnit())
            local integer id   = GetSpellAbilityId()
            local integer i
            local integer h
            local integer v
            local real    a
            local real    as
         
            if id == SPELL_ID and Index[data] == 0 then
                set this         = allocate()
             
                set .caster      = GetTriggerUnit()
                set .owner       = GetTriggerPlayer()
                set .angle       = GetUnitFacing(.caster)*bj_DEGTORAD
                set .missileX    = GetUnitX(.caster) + SPAWN_OFFSET * Cos(.angle)
                set .missileY    = GetUnitY(.caster) + SPAWN_OFFSET * Sin(.angle)
                set .missileAngle= .angle
             
                set .locX        = .missileX + LENGTH * Cos(.angle)
                set .locY        = .missileY + LENGTH * Sin(.angle)
                set .targetX     = .locX
                set .targetY     = .locY
                set .idle        = ALLOW_IDLE
                set .hOffset     = 0
                set .vOffset     = 0
             
                set .level       = GetUnitAbilityLevel(.caster, SPELL_ID)
                set .damage      = DamageAmount(.level)
                set .damageDelayX= DamageDelay (.level)
                set .manaCost    = ManaCost    (.level)
                set .costDelayX  = CostDelay   (.level)
             
                set .count       = MissileCount(.level)
                set .stack       = FrostmyrMissile.create()
                set .damageDelay = .damageDelayX
                set .costDelay   = .costDelayX
                set as           = TAU/.count
                set a            = as/2
                set i            = .count
                set .missileSpin = 0
                loop
                    set i             = i - 1
                    set node          = .stack.push()
                    set node.missile  = CreateUnit(.owner, DUMMY_ID, .missileX, .missileY, .angle*bj_RADTODEG)
                    set node.sfx      = AddSpecialEffectTarget(MODEL_PATH, node.missile, "origin")
                    set node.hMax     = WIDTH *Sin(a)
                    set node.vMax     = HEIGHT*Cos(a)
                    static if not LIBRARY_AutoFly then
                        if UnitAddAbility(node.missile, 'Amrf') and UnitRemoveAbility(node.missile, 'Amrf') then
                        endif
                    endif
                    call PauseUnit(node.missile, true)
                    call SetUnitFlyHeight(node.missile, Z_OFFSET, 0)
                    set a             = a + as
                    exitwhen i        == 0
                endloop
             
                set .locX             = .targetX
                set .locY             = .targetY
                set .missileDistanceX = LENGTH*2
                set .missileDistance  = 0
                set .missileHPos      = 1
                set .missileVPos      = 1
                set .tick             = NewTimerEx(this)
                set .sfx              = CreateSound(SOUND_PATH, true, true, false, 10, 10, "")
             
                set Index[GetUnitUserData(.caster)] = this
                call SetSoundVolume(.sfx, VOLUME)
                call SetSoundPitch(.sfx, PITCH)
                call StartSound(.sfx)
                call UnitAddType(.caster, UNIT_TYPE_PEON)
                call SetUnitAnimation(.caster, ANIMATION)
                // Credits to Lambdadelta for this trick to hide main spell
                call SetUnitAbilityLevel(.caster, SPELL_ID, 999)
                call IncUnitAbilityLevel(.caster, SPELL_ID)
                call UnitAddAbility(.caster, CANCEL_ID)
                call TimerStart(.tick, INTERVAL, true, function thistype.onPeriodic)
            elseif id == CANCEL_ID and Index[data] != 0 then
                set this = Index[data]
                call stop()
            endif
         
            return false
        endmethod
     
        // Needed so that the sound effect will appear since the first cast
        static method preloadSound takes nothing returns nothing
         
            local sound s = CreateSound(SOUND_PATH, false, false, true, 12700, 12700, "")
         
            call StartSound(s)
            call StopSound(s, true, false)
            call ReleaseTimer(GetExpiredTimer())
            set s = null
         
        endmethod
     
        static method init takes nothing returns nothing
         
            local trigger t  = CreateTrigger()
            local integer i  = 0
            local player  p
         
            loop
                exitwhen i > 11
                set p = Player(i)
                call TriggerRegisterPlayerUnitEvent(t,  p, EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
                call TriggerRegisterPlayerUnitEvent(OrderTrigg, p, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(OrderTrigg, p, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(OrderTrigg, p, EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
                set i = i + 1
            endloop
         
            call TriggerAddCondition(t, Condition(function thistype.onCast))
            call TriggerAddCondition(OrderTrigg, Condition(function thistype.onOrder))
            call TimerStart(NewTimer(), 0, false,  function thistype.preloadSound)
         
            set SoundFx = RSound.create(DAMAGE_SOUND, true, true, 12700, 12700)
            set t = null
         
        endmethod
     
        implement InitModule
     
    endstruct
 
endscope
Credits
• BTNIce_by67chrome.blp
• Mini Comet.mdx
• dummy.mdx
• Cold Wind ambient

by 67chrome
by 00110000
by Vexorian
by Google.com
Changelog
(v1.7)
• Replaced ExSound with it's newer version, RapidSound, more robust


(v1.6)
• Fixed missile height bug without AutoFly


(v1.5)
• Fixed order glitches on "holdposition" and "attackmove" order


(v1.4)
• Fixed TimerUtils version
• Privatized demo functions
• Some reworks on idle state handling
• Order point speed and turn rate now sticks to missile's speed and turn rate to avoid faulty idle state
• Some adjustments on animation and mana cost
• Improved documentations


(v1.3)
• Added new configuration to allow user to decide when to stop channeling
• Improved documentations


(v1.2)
• Fixed non-unit widget target locking
• Some animation adjustments
• Fixed some documentation typos


(v1.1)
• Improved documentation
• Added custom on damage sound effect
• Balanced spell cooldown
• Fixed tooltips typo
• AutoFly is now optional

Keywords:
frostmyr
Contents

Frostmyr (Map)

Reviews
22:55, 18th Jan 2016 BPower: The spell is very unique in concept and fun to cast. The configuration possibilities are really great. Excellent coding and presentation. I couldn't find anything wrong in the code. Rated with 5/5

Moderator

M

Moderator

22:55, 18th Jan 2016
BPower:

The spell is very unique in concept and fun to cast.

The configuration possibilities are really great.
Excellent coding and presentation.
I couldn't find anything wrong in the code.

Rated with 5/5
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
What a splendor! I have just a little critique off the bat, but I am just looking for ways to help.

- Use a linked list or linear stack for looping all spell indices instead of TimerUtils. Or, since you use Nestharus' resources, Constant Timer Loop 32 is another golden option.

- Pause dummy units. It cuts their resource usage tremendously.
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
- Use a linked list or linear stack for looping all spell indices instead of TimerUtils. Or, since you use Nestharus' resources, Constant Timer Loop 32 is another golden option.
I have never tried it yet before. And I use TimerUtils for another purpose like delayed order. I use TimerUtils for it instead of creating some sort of order queue, to simplify things. Then for CTL, I think it's no good to have multiple timer resources in a single spell.
- Pause dummy units. It cuts their resource usage tremendously.
Will do in the next update.

Thanks for your suggestions. :)
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
It applies to any unit who isn't using any orders, though it must be unpaused to give it a timed life.

Interesting. Maybe because timed life has/uses/is a buff, since buffs are paused as well when the unit is paused. I wonder tho how the expiration progress bar will look like when the unit is paused. Would it be stuck at zero or not moving at all?
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Pausing is intended to disrupt all of a unit's normal game engine traffic. Orders are ignored, cooldowns cease to keep track of time elapsed, health and mana regeneration are halted, so yes a paused unit will be less taxing on the game engine because the engine isn't doing anything with it.

Full credit for the PauseUnit trick goes to Nestharus: http://www.hiveworkshop.com/forums/2122621-post27.html
 
Have you changed something? Anyways, it seems to work now.^

Edit: I got the issue. With Vexorian's JassHelper it compiles, though only works properly with Cohaders.

Instead of using 0-timeout timer , it could be used this trick to stop the caster:

JASS:
call DisableTrigger(trigger)
call PauseUnit(unit, true)
call IssueImmediateOrder(unit, "stop")
call PauseUnit(unit, false)
call EnableTrigger(trigger)
The currently used method works fine, too, but the method above might be more instant.

It would be really cool if the Frostmyr attack would have a freezing/slowing effect on targets.
You know where targets gets blue and are slowed slightly. It would fit to the "Frost" theme pretty well.

JASS:
// 5. Movespeed for missiles
       private constant real    MOVE_RATE      = 30.0


// 6. Missile's order point move rate
        private function MoveSpeed takes integer level returns real
            return 15.
        endfunction
For first could be added that it's per tick.
The second afterwerds might be a bit confusing. Could you maybe re-phrase it?

What do you think about adding a distance to caster check a for potential end of the spell?
I'm not sure if it was a useful feature for some case, or it would be not needed. Idea?
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Edit: I got the issue. With Vexorian's JassHelper it compiles, though only works properly with Cohaders.
It compiles both with vex's and coh's jass helper. But I always use vex's and never use cohadar's.

Instead of using 0-timeout timer , it could be used this trick to stop the caster:
Interesting. But why disable trigger tho? What trigger to disable?

It would be really cool if the Frostmyr attack would have a freezing/slowing effect on targets.
You know where targets gets blue and are slowed slightly. It would fit to the "Frost" theme pretty well.
I think it would be kinda overpowered. And in the original idea, it doesn't have slowing effect. That's why I added onDamage event so the user may attach any effect they like.

For first could be added that it's per tick.
The second afterwerds might be a bit confusing. Could you maybe re-phrase it?
I don't really understand this part.

What do you think about adding a distance to caster check a for potential end of the spell?
I'm not sure if it was a useful feature for some case, or it would be not needed. Idea?
Good idea ;] But it better has some sort of range indicator.
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Haven't touched it yet. :D Going to update it now.

I honestly don't really like the slow effect since it makes the spell kinda overpowered, doesn't it?

EDIT:

Btw, this one doesn't work:
JASS:
call DisableTrigger(trigger)
call PauseUnit(unit, true)
call IssueImmediateOrder(unit, "stop")
call PauseUnit(unit, false)
call EnableTrigger(trigger)

EDIT:
Updated. Just a small one. Eventually after some considerations, I only did some small changes.
For slow effect, I suppose it would make the spell too overpowered.
For the distance limit, without some sort of indicator, it will just make the player concerned too much about it.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
You eventually leak a local timer handle in onPeriodic.
I'm not sure why you catch that variable. Do instead local thistype this = GetTimerData(GetExpiredTimer())

Here is missing the "static" keyword --> if thistype.onDamage.exists then

set .sfx = CreateSound(SOUND_PATH, true, true, false, 10, 10, null)
I think "null" is not a valid argument for CreateSound. Go with "" instead

User will not be able to click save map, because in DEMO folder\\msg\\scope Test,
the first function name is not private and collides with the stack module. ( why so ever ).

Then again if I privatize function start and save the code, the spell is not working anymore at all.
There must a be a flaw somewhere in the code, which I couldn't detect by reading it.
It shouldn't be hard to locate with a few debug messages.

Overall very very readable code.

EDIT:
Took me 3 minutes to locate the problem :D.
You have set the preload quantity of TimerUtils set to >8190!!!!!!
This breaks the timer stack as TimerUtils tries to return an invalid array index on NewTimerEx()
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
You eventually leak a local timer handle in onPeriodic.
I'm not sure why you catch that variable. Do instead local thistype this = GetTimerData(GetExpiredTimer())
I'm used to use local timer variable since usually I release the timer in the periodic function. Seems like I forgot this time.

Took me 3 minutes to locate the problem :D.
You have set the preload quantity of TimerUtils set to >8190!!!!!!
This breaks the timer stack as TimerUtils tries to return an invalid array index on NewTimerEx()
Meh, I took that TimerUtils from my map Glideon. Which at certain version I need to create timer locally. But in order to avoid desync I use TimerUtils with that amount of timers to be preloaded so it won't desync whenever I call NewTimer() locally. But, I changed the way I trick it since it's not safe. And I forgot to revert the TimerUtils config back to normal.

I don't know, but perhaps I still use this TimerUtils in some of my other resources. lol :'D

I will fix the rest. Thanks!


That looks awesome. :vw_wtf:
Hehe. Thank you!

EDIT:
Updated.

That was quite a lot of changes. I hope everything's alright now. ;)
 
Last edited:
I'm not sure why it didn't work for you and for BPower.
I just tested it and it seems to work fine. Try this code:

JASS:
scope Frostmyr

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//                          Frostmyr v1.4
//                          ¯¯¯¯¯¯¯¯¯¯¯¯¯
//                       Created by: Quilnez
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//      1. Description
//          The caster summons and controls 5 freezing projectiles which will  periodically
//          combusts his mana as cost. Every projectile deals damage periodically to nearby
//          opponents.  While controlling these projectiles, the caster will  be unable  to
//          move  and attack until it's ordered to stop. The spell will also stop once  the
//          caster's mana is depleted or it's dead.
//
//              Level 1 - Burns 20 mana points per second. Deals 80  damage per second.
//              Level 2 - Burns 30 mana points per second. Deals 160 damage per second.
//              Level 3 - Burns 40 mana points per second. Deals 240 damage per second.
//
//          To control the  projectile you need to have the caster in your selection.  Then
//          simply  right-click on the target location/unit. If you have a unit as  target,
//          the projectile will automatically follow it until it's dead.
//
//      2. External scripts
//          (Required)
//              • TimerUtils    by Vexorian     | wc3c.net/showthread.php?t=101322
//              • Stack         by Nestharus    | github.com/nestharus/JASS/tree/master/jass/Data%20Structures/Stack
//              • World Bounds  by Nestharus    | github.com/nestharus/JASS/tree/master/jass/Systems/WorldBounds
//              • Unit Indexer  by Nestharus    | hiveworkshop.com/forums/spells-569/unit-indexer-v5-3-0-1-a-260859/
//              • UnitZ         by Garfield1337 | hiveworkshop.com/forums/jass-resources-412/snippet-getterrainz-unitz-236942/
//              • ExSound       by someone      | hiveworkshop.com/forums/submissions-414/snippet-extendedsound-258991/
//          (Optional)
//              • AutoFly       by Nestharus    | github.com/nestharus/JASS/tree/master/jass/Systems/AutoFly
//
//      3. How to Install
//          • Export all files in Import Manager to your map
//          • Copy dummy unit, main spell, and cancel spell to your map
//          • Copy Frostmyr and Scripts folders at Trigger Editor to your map
//          • Just delete duplicated external scripts
//          • Install all used scripts (libraries) properly
//          • Configure the spell properly (spell id, dummy id, etc.)
//
//      4. Credits
//          • BTNIce_by67chrome.blp by 67chrome
//          • Mini Comet.mdx        by 00110000
//          • dummy.mdx             by Vexorian
//          • Cold Wind ambient     by Google.com
//
//      5. Link
//          Visit this link to check out the newest update of this spell:
//            hiveworkshop.com/forums/spells.php?id=ixhbcf
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//                          CONFIGURATION
//                          ¯¯¯¯¯¯¯¯¯¯¯¯¯
        globals
//
//          A. General
//
//          1. Main spell's raw code
            private constant integer SPELL_ID       = 'A000'
//
//          2. Cancel spell's raw code
            private constant integer CANCEL_ID      = 'A001'
//
//          3. Dummy unit's raw code
            private constant integer DUMMY_ID       = 'h000'
//
//          4. Order id used to give order to missiles
            private constant integer ORDER_ID       = 851971 //smart
//
//          5. Played animation for caster
            private constant string  ANIMATION      = "channel"
//
//          6. Created special effect whenever a unit takes damage from spell
            private constant string  DAMAGE_SFX     = "war3mapImported\\ForstmyrTarget.mdx"
            private constant string  DAMAGE_SFX_PT  = "chest"
//
//          7. Played sound effect whenever a unit takes damage from spell
            private constant string  DAMAGE_SOUND   = "war3mapImported\\FrostmyrDamage.wav"
            private constant integer DSOUND_VOLUME  = 127
//
//          8. Times given for special effects to decay
            private constant real    SFX_DECAY_TIME = 5.0
//
//          9. Sound effect for missiles
            private constant string  SOUND_PATH     = "war3mapImported\\FrostmyrAmbient.wav"
            private constant real    PITCH          = 0.5
            private constant integer VOLUME         = 65
//
//          10. Dealt damage configurations
            private constant attacktype ATTACK_TYPE = ATTACK_TYPE_NORMAL
            private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_COLD
            private constant weapontype WEAPON_TYPE = WEAPON_TYPE_WHOKNOWS
//
//          B. Missile
//
//          1. Missile's model filepath
            private constant string  MODEL_PATH     = "war3mapImported\\Mini Comet.mdx"
//
//          2. If true missiles will be able to enter idle state
//              - Makes it easier to control
//              - Increases damage efficiency
            private constant boolean ALLOW_IDLE     = true
//
//          3. Default fly height for missiles
            private constant real    Z_OFFSET       = 125.0
//
//          4. Missile trajectory configurations
            private constant real    LENGTH         = 100.0
            private constant real    WIDTH          = 100.0
            private constant real    HEIGHT         = 100.0
//
//          5. Movespeed for missiles
            private constant real    MOVE_RATE      = 13.0
//
//          6. Turn rate for missiles
            private constant real    TURN_RATE      = 3.0*bj_DEGTORAD
//
//          7. Initial offset for missiles
            private constant real    SPAWN_OFFSET   = 100.0
//
//          8. Rotate (spin) rate for missiles
//              - Use negative value to inverse the rotation
            private constant real    SPIN_RATE      = 5*bj_DEGTORAD
//          
//          9. Speed factor when missiles deaccelerate
//              - Only takes effect if ALLOW_IDLE is true
//              - Must be between 0 ~ 1
//              - The higher the slower
            private constant real    SLOW_RATE      = 0.95
//
//          10. Maximum range between missile and target to deal damage
            private constant real    COLLISION_SIZE = 120.0
//
//          Better not to touch this
            private constant real    INTERVAL       = 0.03125
            
//
        endglobals
//
//          C. Non-constant
//
//          1. Created missile per cast
            private function MissileCount takes integer level returns integer
                return 5
            endfunction
//
//          2. Damage dealt by every single missile
            private function DamageAmount takes integer level returns real
                return 10.0 * level
            endfunction
//
//          3. Delay before next damage will be dealt
            private function DamageDelay takes integer level returns real
                return 0.125
            endfunction
//
//          4. Combusted mana amount every certain times
            private function ManaCost takes integer level returns real
                return 2.5 + 2.5 *level
            endfunction
//
//          5. Delay between mana reduction
            private function CostDelay takes integer level returns real
                return 0.25
            endfunction
//
//          6. Target classification that can be hit by this spell
            private function filterTarget takes player caster, unit target returns boolean
                return IsUnitEnemy(target, caster) and not(IsUnitType(target, UNIT_TYPE_STRUCTURE) or IsUnitType(target, UNIT_TYPE_MECHANICAL))
            endfunction
//
//          7. This function let you to set up what condition to stop the channel
//             The caster will stop channeling when this function returns true
            private function stopChannel takes unit caster returns boolean
                return GetUnitAbilityLevel(caster, 'BSTN') > 0 // Stop when the caster is stunned
            endfunction
//
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//                          EVENT CATCHER
//                          ¯¯¯¯¯¯¯¯¯¯¯¯¯
        private module EventCatcher
//          Called whenever a unit takes damage from this spell
//              - caster : caster of the spell (damage source)
//              - target : damaged unit
//              - level  : caster's Frostmyr ability level
            static method onDamage takes unit caster, unit target, integer level returns nothing
            endmethod
        endmodule
//
//
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    native UnitAlive takes unit id returns boolean
    
    private module InitModule
        private static method onInit takes nothing returns nothing
            call init()
        endmethod
    endmodule
    
    private struct FrostmyrMissile extends array
    
        effect sfx
        unit missile
        
        real hOffset
        real vOffset
        
        real hMax
        real vMax
        
        implement Stack
        
    endstruct
    
    private struct Frostmyr
        
        real locX
        real locY
        real targetX
        real targetY
        real missileX
        real missileY
        
        real angle
        real hOffset
        real vOffset
        real damage
        real damageDelay
        real damageDelayX
        real manaCost
        real costDelay
        real costDelayX
        
        real missileSpin
        real missileDistance
        real missileDistanceX
        real missileHPos
        real missileVPos
        real missileAngle
        
        boolean idle
        integer count
        integer level
        
        sound  sfx
        player owner
        unit   caster
        unit   target
        timer  tick
        
        FrostmyrMissile stack
        
        static ExSound SoundFx
        static group TempGroup   = CreateGroup()
        static integer array Index
        static constant real TAU = bj_PI*2
        static constant real HP  = bj_PI/2
        
        implement optional EventCatcher
        
        method destroy takes nothing returns nothing
            
            local FrostmyrMissile node = .stack.first
            
            // Kill missiles
            loop
                exitwhen node == 0
                call UnitApplyTimedLife(node.missile, 'BTLF', SFX_DECAY_TIME)
                call DestroyEffect(node.sfx)
                set node.missile = null
                set node.sfx     = null
                set node         = node.next
            endloop
            
            call ReleaseTimer(.tick)
            call StopSound(.sfx, true, true)
            call .stack.destroy()
            call deallocate()
            // Remove leaks
            set .caster = null
            set .target = null
            set .tick   = null
            set .sfx    = null
            
        endmethod
        
        method stop takes nothing returns nothing
            
            // Reset animation if not dead
            if UnitAlive(.caster) then
                call SetUnitAnimation(.caster, "stand")
            endif
            set Index[GetUnitUserData(.caster)] = 0
            call UnitRemoveType(.caster, UNIT_TYPE_PEON)
            call UnitRemoveAbility(.caster, CANCEL_ID)
            call SetUnitAbilityLevel(.caster, SPELL_ID, .level)
            call destroy()
            
        endmethod
        
        static method onPeriodic takes nothing returns nothing
            
            local FrostmyrMissile node
            local thistype this = GetTimerData(GetExpiredTimer())
            local boolean  b
            local unit     u
            
            local real     a
            local real     a2
            local real     as
            local real     d
            local real     f
            local real     h
            local real     m
            
            local real     v
            local real     x
            local real     y
            local real     z
            local real     x2
            local real     y2
            local real     z2
            local real     sf
            
            // If have to stop channeling
            if not UnitAlive(.caster) or stopChannel(.caster) then
                call stop()
                return
            endif
            
            if .target != null then
                if UnitAlive(.target) then
                    set .targetX = GetUnitX(.target)
                    set .targetY = GetUnitY(.target)
                else
                    set .target  = null
                endif
            endif
            
            // Check if the projectiles need to move
            set b = not ALLOW_IDLE or (.targetX-.locX)*(.targetX-.locX)+(.targetY-.locY)*(.targetY-.locY) >= MOVE_RATE*MOVE_RATE
            if b then
                set a = Atan2(.targetY-.locY, .targetX-.locX)
                if TURN_RATE > 0 and Cos(.angle-a) < Cos(TURN_RATE) then
                    if Sin(a-.angle) >= 0 then
                        set .angle = .angle + TURN_RATE
                    else
                        set .angle = .angle - TURN_RATE
                    endif
                else
                    set .angle = a
                endif
                set .locX = .locX + MOVE_RATE * Cos(.angle)
                set .locY = .locY + MOVE_RATE * Sin(.angle)
                // Stop if the order point exceeds map boundaries
                if .locX >= WorldBounds.maxX or .locX <= WorldBounds.minX or .locY >= WorldBounds.maxY or .locY <= WorldBounds.minY then
                    call stop()
                    return
                endif
                call SetSoundPosition(.sfx, .locX, .locY, Z_OFFSET)
            endif
            
            set .damageDelay = .damageDelay - INTERVAL
            set .costDelay   = .costDelay - INTERVAL
            if .costDelay <= 0 then
                set m = GetUnitState(.caster, UNIT_STATE_MANA) - .manaCost
                call SetUnitState(.caster, UNIT_STATE_MANA, m)
                set .costDelay = .costDelayX
                if m <= 0 then
                    call stop()
                    return
                endif
            endif
            
            // If idle calculate deaccelerated speed
            if .idle then
                set sf = missileDistanceX/2
                set sf = 1-RAbsBJ(.missileDistance-sf)/sf*SLOW_RATE
            else
                set sf = 1
            endif
            set .missileDistance = .missileDistance + MOVE_RATE*sf
            if .missileDistance > .missileDistanceX then
                set .missileDistance = .missileDistanceX
            endif
            
            // Adjust control angle
            set a = Atan2(.locY-.missileY, .locX-.missileX)
            if TURN_RATE > 0 and Cos(.missileAngle-a) < Cos(TURN_RATE) then
                if Sin(a-.missileAngle) >= 0 then
                    set .missileAngle = .missileAngle + TURN_RATE
                else
                    set .missileAngle = .missileAngle - TURN_RATE
                endif
            else
                set .missileAngle = a
            endif
            
            // Update missiles
            set .missileSpin = .missileSpin + SPIN_RATE*.missileHPos
            set f    = (.missileDistanceX-.missileDistance)*(.missileDistance/.missileDistanceX)
            set node = .stack.first
            set as   = TAU/.count
            set a    = as /2
            loop
                exitwhen node == 0
                
                set node.hMax = WIDTH *Sin(a+.missileSpin)
                set node.vMax = HEIGHT*Cos(a+.missileSpin)
                
                set h  = (4*node.hMax/.missileDistanceX)*f
                set v  = (4*node.vMax/.missileDistanceX)*f
                set d  = SquareRoot(.missileDistance*.missileDistance+h*h)
                set a2 = .missileAngle-Atan(h/.missileDistance)
                set x  = .missileX + d * Cos(a2)
                set y  = .missileY + d * Sin(a2)
                
                if x < WorldBounds.maxX and x > WorldBounds.minX and y < WorldBounds.maxY and y > WorldBounds.minY then
                    call ShowUnit(node.missile, true)
                    call SetUnitX(node.missile, x)
                    call SetUnitY(node.missile, y)
                    call SetUnitFacing(node.missile, .missileAngle*bj_RADTODEG)
                    call SetUnitFlyHeight(node.missile, Z_OFFSET+v*.missileVPos, 0)
                    
                    set z  = GetUnitZ(node.missile)
                    if .damageDelay <= 0 then
                        call GroupEnumUnitsInRange(TempGroup, x, y, COLLISION_SIZE, null)
                        loop
                            set u = FirstOfGroup(TempGroup)
                            exitwhen u == null
                            call GroupRemoveUnit(TempGroup, u)
                            if UnitAlive(u) and filterTarget(.owner, u) then
                                set x2 = GetUnitX(u)
                                set y2 = GetUnitY(u)
                                set z2 = GetUnitZ(u)
                                set d  = SquareRoot((x-x2)*(x-x2)+(y-y2)*(y-y2))
                                set h  = RAbsBJ(z2-z)
                                // Spherical collision
                                if SquareRoot(d*d+h*h) < COLLISION_SIZE then
                                    static if thistype.onDamage.exists then
                                        call onDamage(.caster, u, .level)
                                    endif
                                    call SoundFx.play(x2, y2, z2, DSOUND_VOLUME)
                                    call UnitDamageTarget(.caster, u, .damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
                                    call DestroyEffect(AddSpecialEffectTarget(DAMAGE_SFX, u, DAMAGE_SFX_PT))
                                endif
                            endif
                        endloop
                    endif
                else
                    // Temporary hide missile if exceeds map boundaries
                    call ShowUnit(node.missile, false)
                endif
                
                set a = a + as
                set node = node.next
            endloop
            
            if .damageDelay <= 0 then
                set .damageDelay = .damageDelayX
            endif
            
            if .missileDistance == .missileDistanceX then
                set .missileX        = x
                set .missileY        = y
                set .missileAngle    = Atan2(.locY-.missileY, .locX-.missileX)
                set .missileDistance = 0
                set .missileVPos     = -.missileVPos
                if b then
                    set .idle = false
                    set .missileHPos = -.missileHPos
                else
                    set .idle = true
                    set .missileHPos = 1
                endif
            endif
            
        endmethod
        
        static method orderStop takes nothing returns nothing
            
            local timer    t    = GetExpiredTimer()
            local thistype this = GetTimerData(t)
            
            call IssueImmediateOrder(.caster, "stop")
            call ReleaseTimer(t)
            set t = null
            
        endmethod
        
        static method onOrder takes nothing returns boolean
        
            local thistype this = Index[GetUnitUserData(GetTriggerUnit())]
            local widget   w
            local unit     u
            
            // If unit is casting Frostmyr
            if this != 0 then
                if GetIssuedOrderId() == ORDER_ID then
                    set u = GetOrderTargetUnit()
                    if u == null then
                        set w = GetOrderTarget()
                        if w == null then
                            set .targetX = GetOrderPointX()
                            set .targetY = GetOrderPointY()
                        else
                            set .targetX = GetWidgetX(w)
                            set .targetY = GetWidgetY(w)
                        endif
                        set .target = null
                        set w = null
                    else
                        set .target = u
                        set u = null
                    endif
                    set .idle = false
                endif
                call DisableTrigger(orderTrigger)
                call PauseUnit(.caster, true)
                call IssueImmediateOrder(.caster, "stop")
                call PauseUnit(.caster, false)
                call EnableTrigger(orderTrigger)
            endif
            
            return false
        endmethod
        
        static method onCast takes nothing returns boolean
            
            local FrostmyrMissile node
            local thistype this
            local integer data = GetUnitUserData(GetTriggerUnit())
            local integer id   = GetSpellAbilityId()
            local integer i
            local integer h
            local integer v
            local real    a
            local real    as
            
            if id == SPELL_ID and Index[data] == 0 then
                set this         = allocate()
                
                set .caster      = GetTriggerUnit()
                set .owner       = GetTriggerPlayer()
                set .angle       = GetUnitFacing(.caster)*bj_DEGTORAD
                set .missileX    = GetUnitX(.caster) + SPAWN_OFFSET * Cos(.angle)
                set .missileY    = GetUnitY(.caster) + SPAWN_OFFSET * Sin(.angle)
                set .missileAngle= .angle
                
                set .locX        = .missileX + LENGTH * Cos(.angle)
                set .locY        = .missileY + LENGTH * Sin(.angle)
                set .targetX     = .locX
                set .targetY     = .locY
                set .idle        = ALLOW_IDLE
                set .hOffset     = 0
                set .vOffset     = 0
                
                set .level       = GetUnitAbilityLevel(.caster, SPELL_ID)
                set .damage      = DamageAmount(.level)
                set .damageDelayX= DamageDelay (.level)
                set .manaCost    = ManaCost    (.level)
                set .costDelayX  = CostDelay   (.level)
                
                set .count       = MissileCount(.level)
                set .stack       = FrostmyrMissile.create()
                set .damageDelay = .damageDelayX
                set .costDelay   = .costDelayX
                set as           = TAU/.count
                set a            = as/2
                set i            = .count
                set .missileSpin = 0
                loop
                    set i             = i - 1
                    set node          = .stack.push()
                    set node.missile  = CreateUnit(.owner, DUMMY_ID, .missileX, .missileY, .angle*bj_RADTODEG)
                    set node.sfx      = AddSpecialEffectTarget(MODEL_PATH, node.missile, "origin")
                    set node.hMax     = WIDTH *Sin(a)
                    set node.vMax     = HEIGHT*Cos(a)
                    call PauseUnit(node.missile, true)
                    call SetUnitFlyHeight(node.missile, Z_OFFSET, 0)
                    static if not LIBRARY_AutoFly then
                        if UnitAddAbility(node.missile, 'Amrf') and UnitRemoveAbility(node.missile, 'Amrf') then
                        endif
                    endif
                    set a             = a + as
                    exitwhen i        == 0
                endloop
                
                set .locX             = .targetX
                set .locY             = .targetY
                set .missileDistanceX = LENGTH*2
                set .missileDistance  = 0
                set .missileHPos      = 1
                set .missileVPos      = 1
                set .tick             = NewTimerEx(this)
                set .sfx              = CreateSound(SOUND_PATH, true, true, false, 10, 10, "")
                
                set Index[GetUnitUserData(.caster)] = this
                call SetSoundVolume(.sfx, VOLUME)
                call SetSoundPitch(.sfx, PITCH)
                call StartSound(.sfx)
                call UnitAddType(.caster, UNIT_TYPE_PEON)
                call SetUnitAnimation(.caster, ANIMATION)
                // Credits to Lambdadelta for this trick to hide main spell
                call SetUnitAbilityLevel(.caster, SPELL_ID, 999)
                call IncUnitAbilityLevel(.caster, SPELL_ID)
                call UnitAddAbility(.caster, CANCEL_ID)
                call TimerStart(.tick, INTERVAL, true, function thistype.onPeriodic)
            elseif id == CANCEL_ID and Index[data] != 0 then
                set this = Index[data]
                call stop()
            endif
            
            return false
        endmethod
        
        // Needed so that the sound effect will appear since the first cast
        static method preloadSound takes nothing returns nothing
            
            local sound s = CreateSound(SOUND_PATH, false, false, true, 12700, 12700, "")
            
            call StartSound(s)
            call StopSound(s, true, false)
            call ReleaseTimer(GetExpiredTimer())
            set s = null
            
        endmethod
        
        private static trigger orderTrigger = CreateTrigger()
        
        static method init takes nothing returns nothing
            
            local trigger t  = CreateTrigger()
            local integer i  = 0
            local player  p
            
            loop
                exitwhen i > 11
                set p = Player(i)
                call TriggerRegisterPlayerUnitEvent(t,  p, EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
                call TriggerRegisterPlayerUnitEvent(orderTrigger, p, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(orderTrigger, p, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
                set i = i + 1
            endloop
            
            call TriggerAddCondition(t,  Condition(function thistype.onCast))
            call TriggerAddCondition(orderTrigger, Condition(function thistype.onOrder))
            call TimerStart(NewTimer(), 0, false,  function thistype.preloadSound)
            
            set SoundFx = ExSound.create(DAMAGE_SOUND, true, true, 12700, 12700)
            
            set t  = null
            
        endmethod
        
        implement InitModule
        
    endstruct
    
endscope

I only declared the global trigger above init and used the mentioned method.
The orderStop function is not in use anymore.
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
I'm not sure why it didn't work for you and for BPower.
I just tested it and it seems to work fine. Try this code:

JASS:
scope Frostmyr

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//                          Frostmyr v1.4
//                          ¯¯¯¯¯¯¯¯¯¯¯¯¯
//                       Created by: Quilnez
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//      1. Description
//          The caster summons and controls 5 freezing projectiles which will  periodically
//          combusts his mana as cost. Every projectile deals damage periodically to nearby
//          opponents.  While controlling these projectiles, the caster will  be unable  to
//          move  and attack until it's ordered to stop. The spell will also stop once  the
//          caster's mana is depleted or it's dead.
//
//              Level 1 - Burns 20 mana points per second. Deals 80  damage per second.
//              Level 2 - Burns 30 mana points per second. Deals 160 damage per second.
//              Level 3 - Burns 40 mana points per second. Deals 240 damage per second.
//
//          To control the  projectile you need to have the caster in your selection.  Then
//          simply  right-click on the target location/unit. If you have a unit as  target,
//          the projectile will automatically follow it until it's dead.
//
//      2. External scripts
//          (Required)
//              • TimerUtils    by Vexorian     | wc3c.net/showthread.php?t=101322
//              • Stack         by Nestharus    | github.com/nestharus/JASS/tree/master/jass/Data%20Structures/Stack
//              • World Bounds  by Nestharus    | github.com/nestharus/JASS/tree/master/jass/Systems/WorldBounds
//              • Unit Indexer  by Nestharus    | hiveworkshop.com/forums/spells-569/unit-indexer-v5-3-0-1-a-260859/
//              • UnitZ         by Garfield1337 | hiveworkshop.com/forums/jass-resources-412/snippet-getterrainz-unitz-236942/
//              • ExSound       by someone      | hiveworkshop.com/forums/submissions-414/snippet-extendedsound-258991/
//          (Optional)
//              • AutoFly       by Nestharus    | github.com/nestharus/JASS/tree/master/jass/Systems/AutoFly
//
//      3. How to Install
//          • Export all files in Import Manager to your map
//          • Copy dummy unit, main spell, and cancel spell to your map
//          • Copy Frostmyr and Scripts folders at Trigger Editor to your map
//          • Just delete duplicated external scripts
//          • Install all used scripts (libraries) properly
//          • Configure the spell properly (spell id, dummy id, etc.)
//
//      4. Credits
//          • BTNIce_by67chrome.blp by 67chrome
//          • Mini Comet.mdx        by 00110000
//          • dummy.mdx             by Vexorian
//          • Cold Wind ambient     by Google.com
//
//      5. Link
//          Visit this link to check out the newest update of this spell:
//            hiveworkshop.com/forums/spells.php?id=ixhbcf
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//                          CONFIGURATION
//                          ¯¯¯¯¯¯¯¯¯¯¯¯¯
        globals
//
//          A. General
//
//          1. Main spell's raw code
            private constant integer SPELL_ID       = 'A000'
//
//          2. Cancel spell's raw code
            private constant integer CANCEL_ID      = 'A001'
//
//          3. Dummy unit's raw code
            private constant integer DUMMY_ID       = 'h000'
//
//          4. Order id used to give order to missiles
            private constant integer ORDER_ID       = 851971 //smart
//
//          5. Played animation for caster
            private constant string  ANIMATION      = "channel"
//
//          6. Created special effect whenever a unit takes damage from spell
            private constant string  DAMAGE_SFX     = "war3mapImported\\ForstmyrTarget.mdx"
            private constant string  DAMAGE_SFX_PT  = "chest"
//
//          7. Played sound effect whenever a unit takes damage from spell
            private constant string  DAMAGE_SOUND   = "war3mapImported\\FrostmyrDamage.wav"
            private constant integer DSOUND_VOLUME  = 127
//
//          8. Times given for special effects to decay
            private constant real    SFX_DECAY_TIME = 5.0
//
//          9. Sound effect for missiles
            private constant string  SOUND_PATH     = "war3mapImported\\FrostmyrAmbient.wav"
            private constant real    PITCH          = 0.5
            private constant integer VOLUME         = 65
//
//          10. Dealt damage configurations
            private constant attacktype ATTACK_TYPE = ATTACK_TYPE_NORMAL
            private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_COLD
            private constant weapontype WEAPON_TYPE = WEAPON_TYPE_WHOKNOWS
//
//          B. Missile
//
//          1. Missile's model filepath
            private constant string  MODEL_PATH     = "war3mapImported\\Mini Comet.mdx"
//
//          2. If true missiles will be able to enter idle state
//              - Makes it easier to control
//              - Increases damage efficiency
            private constant boolean ALLOW_IDLE     = true
//
//          3. Default fly height for missiles
            private constant real    Z_OFFSET       = 125.0
//
//          4. Missile trajectory configurations
            private constant real    LENGTH         = 100.0
            private constant real    WIDTH          = 100.0
            private constant real    HEIGHT         = 100.0
//
//          5. Movespeed for missiles
            private constant real    MOVE_RATE      = 13.0
//
//          6. Turn rate for missiles
            private constant real    TURN_RATE      = 3.0*bj_DEGTORAD
//
//          7. Initial offset for missiles
            private constant real    SPAWN_OFFSET   = 100.0
//
//          8. Rotate (spin) rate for missiles
//              - Use negative value to inverse the rotation
            private constant real    SPIN_RATE      = 5*bj_DEGTORAD
//          
//          9. Speed factor when missiles deaccelerate
//              - Only takes effect if ALLOW_IDLE is true
//              - Must be between 0 ~ 1
//              - The higher the slower
            private constant real    SLOW_RATE      = 0.95
//
//          10. Maximum range between missile and target to deal damage
            private constant real    COLLISION_SIZE = 120.0
//
//          Better not to touch this
            private constant real    INTERVAL       = 0.03125
            
//
        endglobals
//
//          C. Non-constant
//
//          1. Created missile per cast
            private function MissileCount takes integer level returns integer
                return 5
            endfunction
//
//          2. Damage dealt by every single missile
            private function DamageAmount takes integer level returns real
                return 10.0 * level
            endfunction
//
//          3. Delay before next damage will be dealt
            private function DamageDelay takes integer level returns real
                return 0.125
            endfunction
//
//          4. Combusted mana amount every certain times
            private function ManaCost takes integer level returns real
                return 2.5 + 2.5 *level
            endfunction
//
//          5. Delay between mana reduction
            private function CostDelay takes integer level returns real
                return 0.25
            endfunction
//
//          6. Target classification that can be hit by this spell
            private function filterTarget takes player caster, unit target returns boolean
                return IsUnitEnemy(target, caster) and not(IsUnitType(target, UNIT_TYPE_STRUCTURE) or IsUnitType(target, UNIT_TYPE_MECHANICAL))
            endfunction
//
//          7. This function let you to set up what condition to stop the channel
//             The caster will stop channeling when this function returns true
            private function stopChannel takes unit caster returns boolean
                return GetUnitAbilityLevel(caster, 'BSTN') > 0 // Stop when the caster is stunned
            endfunction
//
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//                          EVENT CATCHER
//                          ¯¯¯¯¯¯¯¯¯¯¯¯¯
        private module EventCatcher
//          Called whenever a unit takes damage from this spell
//              - caster : caster of the spell (damage source)
//              - target : damaged unit
//              - level  : caster's Frostmyr ability level
            static method onDamage takes unit caster, unit target, integer level returns nothing
            endmethod
        endmodule
//
//
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    native UnitAlive takes unit id returns boolean
    
    private module InitModule
        private static method onInit takes nothing returns nothing
            call init()
        endmethod
    endmodule
    
    private struct FrostmyrMissile extends array
    
        effect sfx
        unit missile
        
        real hOffset
        real vOffset
        
        real hMax
        real vMax
        
        implement Stack
        
    endstruct
    
    private struct Frostmyr
        
        real locX
        real locY
        real targetX
        real targetY
        real missileX
        real missileY
        
        real angle
        real hOffset
        real vOffset
        real damage
        real damageDelay
        real damageDelayX
        real manaCost
        real costDelay
        real costDelayX
        
        real missileSpin
        real missileDistance
        real missileDistanceX
        real missileHPos
        real missileVPos
        real missileAngle
        
        boolean idle
        integer count
        integer level
        
        sound  sfx
        player owner
        unit   caster
        unit   target
        timer  tick
        
        FrostmyrMissile stack
        
        static ExSound SoundFx
        static group TempGroup   = CreateGroup()
        static integer array Index
        static constant real TAU = bj_PI*2
        static constant real HP  = bj_PI/2
        
        implement optional EventCatcher
        
        method destroy takes nothing returns nothing
            
            local FrostmyrMissile node = .stack.first
            
            // Kill missiles
            loop
                exitwhen node == 0
                call UnitApplyTimedLife(node.missile, 'BTLF', SFX_DECAY_TIME)
                call DestroyEffect(node.sfx)
                set node.missile = null
                set node.sfx     = null
                set node         = node.next
            endloop
            
            call ReleaseTimer(.tick)
            call StopSound(.sfx, true, true)
            call .stack.destroy()
            call deallocate()
            // Remove leaks
            set .caster = null
            set .target = null
            set .tick   = null
            set .sfx    = null
            
        endmethod
        
        method stop takes nothing returns nothing
            
            // Reset animation if not dead
            if UnitAlive(.caster) then
                call SetUnitAnimation(.caster, "stand")
            endif
            set Index[GetUnitUserData(.caster)] = 0
            call UnitRemoveType(.caster, UNIT_TYPE_PEON)
            call UnitRemoveAbility(.caster, CANCEL_ID)
            call SetUnitAbilityLevel(.caster, SPELL_ID, .level)
            call destroy()
            
        endmethod
        
        static method onPeriodic takes nothing returns nothing
            
            local FrostmyrMissile node
            local thistype this = GetTimerData(GetExpiredTimer())
            local boolean  b
            local unit     u
            
            local real     a
            local real     a2
            local real     as
            local real     d
            local real     f
            local real     h
            local real     m
            
            local real     v
            local real     x
            local real     y
            local real     z
            local real     x2
            local real     y2
            local real     z2
            local real     sf
            
            // If have to stop channeling
            if not UnitAlive(.caster) or stopChannel(.caster) then
                call stop()
                return
            endif
            
            if .target != null then
                if UnitAlive(.target) then
                    set .targetX = GetUnitX(.target)
                    set .targetY = GetUnitY(.target)
                else
                    set .target  = null
                endif
            endif
            
            // Check if the projectiles need to move
            set b = not ALLOW_IDLE or (.targetX-.locX)*(.targetX-.locX)+(.targetY-.locY)*(.targetY-.locY) >= MOVE_RATE*MOVE_RATE
            if b then
                set a = Atan2(.targetY-.locY, .targetX-.locX)
                if TURN_RATE > 0 and Cos(.angle-a) < Cos(TURN_RATE) then
                    if Sin(a-.angle) >= 0 then
                        set .angle = .angle + TURN_RATE
                    else
                        set .angle = .angle - TURN_RATE
                    endif
                else
                    set .angle = a
                endif
                set .locX = .locX + MOVE_RATE * Cos(.angle)
                set .locY = .locY + MOVE_RATE * Sin(.angle)
                // Stop if the order point exceeds map boundaries
                if .locX >= WorldBounds.maxX or .locX <= WorldBounds.minX or .locY >= WorldBounds.maxY or .locY <= WorldBounds.minY then
                    call stop()
                    return
                endif
                call SetSoundPosition(.sfx, .locX, .locY, Z_OFFSET)
            endif
            
            set .damageDelay = .damageDelay - INTERVAL
            set .costDelay   = .costDelay - INTERVAL
            if .costDelay <= 0 then
                set m = GetUnitState(.caster, UNIT_STATE_MANA) - .manaCost
                call SetUnitState(.caster, UNIT_STATE_MANA, m)
                set .costDelay = .costDelayX
                if m <= 0 then
                    call stop()
                    return
                endif
            endif
            
            // If idle calculate deaccelerated speed
            if .idle then
                set sf = missileDistanceX/2
                set sf = 1-RAbsBJ(.missileDistance-sf)/sf*SLOW_RATE
            else
                set sf = 1
            endif
            set .missileDistance = .missileDistance + MOVE_RATE*sf
            if .missileDistance > .missileDistanceX then
                set .missileDistance = .missileDistanceX
            endif
            
            // Adjust control angle
            set a = Atan2(.locY-.missileY, .locX-.missileX)
            if TURN_RATE > 0 and Cos(.missileAngle-a) < Cos(TURN_RATE) then
                if Sin(a-.missileAngle) >= 0 then
                    set .missileAngle = .missileAngle + TURN_RATE
                else
                    set .missileAngle = .missileAngle - TURN_RATE
                endif
            else
                set .missileAngle = a
            endif
            
            // Update missiles
            set .missileSpin = .missileSpin + SPIN_RATE*.missileHPos
            set f    = (.missileDistanceX-.missileDistance)*(.missileDistance/.missileDistanceX)
            set node = .stack.first
            set as   = TAU/.count
            set a    = as /2
            loop
                exitwhen node == 0
                
                set node.hMax = WIDTH *Sin(a+.missileSpin)
                set node.vMax = HEIGHT*Cos(a+.missileSpin)
                
                set h  = (4*node.hMax/.missileDistanceX)*f
                set v  = (4*node.vMax/.missileDistanceX)*f
                set d  = SquareRoot(.missileDistance*.missileDistance+h*h)
                set a2 = .missileAngle-Atan(h/.missileDistance)
                set x  = .missileX + d * Cos(a2)
                set y  = .missileY + d * Sin(a2)
                
                if x < WorldBounds.maxX and x > WorldBounds.minX and y < WorldBounds.maxY and y > WorldBounds.minY then
                    call ShowUnit(node.missile, true)
                    call SetUnitX(node.missile, x)
                    call SetUnitY(node.missile, y)
                    call SetUnitFacing(node.missile, .missileAngle*bj_RADTODEG)
                    call SetUnitFlyHeight(node.missile, Z_OFFSET+v*.missileVPos, 0)
                    
                    set z  = GetUnitZ(node.missile)
                    if .damageDelay <= 0 then
                        call GroupEnumUnitsInRange(TempGroup, x, y, COLLISION_SIZE, null)
                        loop
                            set u = FirstOfGroup(TempGroup)
                            exitwhen u == null
                            call GroupRemoveUnit(TempGroup, u)
                            if UnitAlive(u) and filterTarget(.owner, u) then
                                set x2 = GetUnitX(u)
                                set y2 = GetUnitY(u)
                                set z2 = GetUnitZ(u)
                                set d  = SquareRoot((x-x2)*(x-x2)+(y-y2)*(y-y2))
                                set h  = RAbsBJ(z2-z)
                                // Spherical collision
                                if SquareRoot(d*d+h*h) < COLLISION_SIZE then
                                    static if thistype.onDamage.exists then
                                        call onDamage(.caster, u, .level)
                                    endif
                                    call SoundFx.play(x2, y2, z2, DSOUND_VOLUME)
                                    call UnitDamageTarget(.caster, u, .damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
                                    call DestroyEffect(AddSpecialEffectTarget(DAMAGE_SFX, u, DAMAGE_SFX_PT))
                                endif
                            endif
                        endloop
                    endif
                else
                    // Temporary hide missile if exceeds map boundaries
                    call ShowUnit(node.missile, false)
                endif
                
                set a = a + as
                set node = node.next
            endloop
            
            if .damageDelay <= 0 then
                set .damageDelay = .damageDelayX
            endif
            
            if .missileDistance == .missileDistanceX then
                set .missileX        = x
                set .missileY        = y
                set .missileAngle    = Atan2(.locY-.missileY, .locX-.missileX)
                set .missileDistance = 0
                set .missileVPos     = -.missileVPos
                if b then
                    set .idle = false
                    set .missileHPos = -.missileHPos
                else
                    set .idle = true
                    set .missileHPos = 1
                endif
            endif
            
        endmethod
        
        static method orderStop takes nothing returns nothing
            
            local timer    t    = GetExpiredTimer()
            local thistype this = GetTimerData(t)
            
            call IssueImmediateOrder(.caster, "stop")
            call ReleaseTimer(t)
            set t = null
            
        endmethod
        
        static method onOrder takes nothing returns boolean
        
            local thistype this = Index[GetUnitUserData(GetTriggerUnit())]
            local widget   w
            local unit     u
            
            // If unit is casting Frostmyr
            if this != 0 then
                if GetIssuedOrderId() == ORDER_ID then
                    set u = GetOrderTargetUnit()
                    if u == null then
                        set w = GetOrderTarget()
                        if w == null then
                            set .targetX = GetOrderPointX()
                            set .targetY = GetOrderPointY()
                        else
                            set .targetX = GetWidgetX(w)
                            set .targetY = GetWidgetY(w)
                        endif
                        set .target = null
                        set w = null
                    else
                        set .target = u
                        set u = null
                    endif
                    set .idle = false
                endif
                call DisableTrigger(orderTrigger)
                call PauseUnit(.caster, true)
                call IssueImmediateOrder(.caster, "stop")
                call PauseUnit(.caster, false)
                call EnableTrigger(orderTrigger)
            endif
            
            return false
        endmethod
        
        static method onCast takes nothing returns boolean
            
            local FrostmyrMissile node
            local thistype this
            local integer data = GetUnitUserData(GetTriggerUnit())
            local integer id   = GetSpellAbilityId()
            local integer i
            local integer h
            local integer v
            local real    a
            local real    as
            
            if id == SPELL_ID and Index[data] == 0 then
                set this         = allocate()
                
                set .caster      = GetTriggerUnit()
                set .owner       = GetTriggerPlayer()
                set .angle       = GetUnitFacing(.caster)*bj_DEGTORAD
                set .missileX    = GetUnitX(.caster) + SPAWN_OFFSET * Cos(.angle)
                set .missileY    = GetUnitY(.caster) + SPAWN_OFFSET * Sin(.angle)
                set .missileAngle= .angle
                
                set .locX        = .missileX + LENGTH * Cos(.angle)
                set .locY        = .missileY + LENGTH * Sin(.angle)
                set .targetX     = .locX
                set .targetY     = .locY
                set .idle        = ALLOW_IDLE
                set .hOffset     = 0
                set .vOffset     = 0
                
                set .level       = GetUnitAbilityLevel(.caster, SPELL_ID)
                set .damage      = DamageAmount(.level)
                set .damageDelayX= DamageDelay (.level)
                set .manaCost    = ManaCost    (.level)
                set .costDelayX  = CostDelay   (.level)
                
                set .count       = MissileCount(.level)
                set .stack       = FrostmyrMissile.create()
                set .damageDelay = .damageDelayX
                set .costDelay   = .costDelayX
                set as           = TAU/.count
                set a            = as/2
                set i            = .count
                set .missileSpin = 0
                loop
                    set i             = i - 1
                    set node          = .stack.push()
                    set node.missile  = CreateUnit(.owner, DUMMY_ID, .missileX, .missileY, .angle*bj_RADTODEG)
                    set node.sfx      = AddSpecialEffectTarget(MODEL_PATH, node.missile, "origin")
                    set node.hMax     = WIDTH *Sin(a)
                    set node.vMax     = HEIGHT*Cos(a)
                    call PauseUnit(node.missile, true)
                    call SetUnitFlyHeight(node.missile, Z_OFFSET, 0)
                    static if not LIBRARY_AutoFly then
                        if UnitAddAbility(node.missile, 'Amrf') and UnitRemoveAbility(node.missile, 'Amrf') then
                        endif
                    endif
                    set a             = a + as
                    exitwhen i        == 0
                endloop
                
                set .locX             = .targetX
                set .locY             = .targetY
                set .missileDistanceX = LENGTH*2
                set .missileDistance  = 0
                set .missileHPos      = 1
                set .missileVPos      = 1
                set .tick             = NewTimerEx(this)
                set .sfx              = CreateSound(SOUND_PATH, true, true, false, 10, 10, "")
                
                set Index[GetUnitUserData(.caster)] = this
                call SetSoundVolume(.sfx, VOLUME)
                call SetSoundPitch(.sfx, PITCH)
                call StartSound(.sfx)
                call UnitAddType(.caster, UNIT_TYPE_PEON)
                call SetUnitAnimation(.caster, ANIMATION)
                // Credits to Lambdadelta for this trick to hide main spell
                call SetUnitAbilityLevel(.caster, SPELL_ID, 999)
                call IncUnitAbilityLevel(.caster, SPELL_ID)
                call UnitAddAbility(.caster, CANCEL_ID)
                call TimerStart(.tick, INTERVAL, true, function thistype.onPeriodic)
            elseif id == CANCEL_ID and Index[data] != 0 then
                set this = Index[data]
                call stop()
            endif
            
            return false
        endmethod
        
        // Needed so that the sound effect will appear since the first cast
        static method preloadSound takes nothing returns nothing
            
            local sound s = CreateSound(SOUND_PATH, false, false, true, 12700, 12700, "")
            
            call StartSound(s)
            call StopSound(s, true, false)
            call ReleaseTimer(GetExpiredTimer())
            set s = null
            
        endmethod
        
        private static trigger orderTrigger = CreateTrigger()
        
        static method init takes nothing returns nothing
            
            local trigger t  = CreateTrigger()
            local integer i  = 0
            local player  p
            
            loop
                exitwhen i > 11
                set p = Player(i)
                call TriggerRegisterPlayerUnitEvent(t,  p, EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
                call TriggerRegisterPlayerUnitEvent(orderTrigger, p, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(orderTrigger, p, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
                set i = i + 1
            endloop
            
            call TriggerAddCondition(t,  Condition(function thistype.onCast))
            call TriggerAddCondition(orderTrigger, Condition(function thistype.onOrder))
            call TimerStart(NewTimer(), 0, false,  function thistype.preloadSound)
            
            set SoundFx = ExSound.create(DAMAGE_SOUND, true, true, 12700, 12700)
            
            set t  = null
            
        endmethod
        
        implement InitModule
        
    endstruct
    
endscope

I only declared the global trigger above init and used the mentioned method.
The orderStop function is not in use anymore.

Sorry but I can't use this trick since it also prevents the caster to use any other orders, including the cancel ability. :(
When I tried to add exceptions for the cancel ability order id, there comes another weird glitches like double casted spell, etc.

EDIT:
Updated with a hotfix!
 
Level 1
Joined
May 14, 2016
Messages
5
Nice Job! this spell is very good, i liked but i can't install because of errors in folder scripts when i copy from my Scenario... you can help me?
 
Level 1
Joined
May 14, 2016
Messages
5
JASSHelper - Step 1 (textmacros, libraries, scopes)

Compile error.

Line 12682: Unable to find textmacro: "optional"

\\ this is in the WorldBounds Trigger Category

Hey, i have other spells with Unit Indexer and TimerUtils triggers... i only rename the old spells with Unit and Timer triggers, this is a problem?
 
Last edited by a moderator:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
JASSHelper - Step 1 (textmacros, libraries, scopes)

Compile error.

Line 12682: Unable to find textmacro: "optional"

\\ this is in the WorldBounds Trigger Category

Hey, i have other spells with Unit Indexer and TimerUtils triggers... i only rename the old spells with Unit and Timer triggers, this is a problem?
Try to use Vexorian's JassHelper instead.
I think renaming a trigger won't be a problem.

I'm terribly sorry for very late response. I must had forgotten to reply or something. :(
 
Top