Magnum Barrage v1.1

This bundle is marked as useful / simple. Simplicity is bliss, low effort and/or may contain minor bugs.
After charging for a moment, release a barrage of missiles to explode at an area. Any units within the way will cause the missile to explode dealing damage to the target and a fraction to enemies within the area.

Each level increases cast aoe, cast range, damage, number of missiles, missile speed, explosion aoe.

Credits are in the code.

JASS:
scope MagnumBarrage

/* Magnum Barrage v1.1
    Copy:
        Imports
            - dummy.mdx
        Objects
            - dummy unit
            - pause ability
            - magnum barrage ability
            
    Requires:
        Magtheridon
            - RegisterPlayerUnitEvent           http://hiveworkshop.com/forums/showthread.php?t=203338
        Nestharus
            - Alloc                             https://github.com/nestharus/JASS/tree/master/jass/Systems/Alloc
            - SharedList                        https://github.com/nestharus/JASS/tree/master/jass/Data%20Structures/SharedList
            - ErrorMessage                      https://github.com/nestharus/JASS/tree/master/jass/Systems/ErrorMessage
        Cohadar
            - TT                                http://www.thehelper.net/threads/timer-ticker.68189/
        TriggerHappy
            - TimedHandles                      http://www.wc3c.net/showthread.php?t=105456
        Bribe
            - SpellEffectEvent                  http://www.hiveworkshop.com/forums/jass-resources-412/snippet-spelleffectevent-187193/
            - Table                             http://www.hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
            
    Changelog:
        v1.0 
            - Initial upload
        v1.1 
            - Code minor cleanups
            - Code Documentation 
            - Now Requires TimedHandles to make sure missiles don't bug out groupEnum
            - Custom FilterUnit function for customizable filter
        
    Configure:
        ABIL_ID to magnum barrage ability id
        PAUSE_ABIL_ID to pause ability id
        DUMMY_ID to dummy unit id
        
        PAUSE_ABIL_ORDER_STRING is the order string for PAUSE_ABIL_ID
        
        CHARGE_FX are the offsets for the charge fx, because it the fx is attached toa dummy unit, you have to custom position it with each unit.
        MISSILE_Z is the height of the missile.
        
        MISSILE_FX is the string path of the effect missiles take
        CHARGE_FX is the string path of the effect used to charge up the ability
        EXPLODE_FX is the string path of the explosion effect
        
        LOAD_PER_SECOND is how many missiles the barrage loads up per second of charging
        FIRE_PER_SECOND is how many missiles are discharged per second upon releasing
        MISSILE_SIZE is how big the missiles are
        COLLISION_AOE is how big the missile collision radius is
        CAST_AOE is the aoe within missiles should be fired at
        SPLASH_AOE is how much area the damage should splash within
        SPLASH_DAMAGE is the ratio of damage that should be dealt
        SPEED is how fast the missile moves
        DAMAGE is how much damage is dealt upon collision
        STATS is how much stats is factored into the damage formula
        FILTER_UNIT what kind of units you want to target 
                ** by default filters out alive enemy units, then proceeds to check your filter **
*/

    globals
        public constant integer ABIL_ID                         = 'A000'
        public constant integer PAUSE_ABIL_ID                   = 'A001'
        public constant integer DUMMY_ID                        = 'e001'
        
        public constant string PAUSE_ABIL_ORDER_STRING          = "carrionswarm"
        
        public constant real CHARGE_FX_X                        = 60.
        public constant real CHARGE_FX_Y                        = 60.
        public constant real CHARGE_FX_Z                        = 60.
        public constant real MISSILE_Z                          = 60.
        
        public constant string MISSILE_FX                       = "Abilities\\Weapons\\SearingArrow\\SearingArrowMissile.mdl"
        public constant string CHARGE_FX                        = "Abilities\\Spells\\Orc\\Bloodlust\\BloodlustTarget.mdl"
        public constant string EXPLODE_FX                       = "abilities\\weapons\\catapult\\catapultmissile.mdl"
    endglobals

    private constant function LOAD_PER_SECOND takes integer i returns real
        return 6 + 1.5*i
    endfunction
    
    private constant function FIRE_PER_SECOND takes integer i returns real
        return 12.
    endfunction
    
    private constant function MISSILE_SIZE takes integer i returns real
        return .8 + .35*i
    endfunction
    
    private constant function COLLISION_AOE takes integer i returns real
        return 100. + 20*i
    endfunction
    
    private constant function CAST_AOE takes integer i returns real
        return 100 + 100.*i
    endfunction
    
    private constant function SPLASH_AOE takes integer i returns real
        return 100 + 70.*i
    endfunction
    
    private constant function SPLASH_DAMAGE takes integer i returns real
        return .65
    endfunction
    
    private constant function SPEED takes integer i returns real
        return 1000 +400.*i
    endfunction
    
    private constant function DAMAGE takes integer i, integer stats returns real
        return 5.*i + stats*i*.35
    endfunction
    
    private constant function STATS takes integer str, integer agi, integer int returns integer
        return agi
    endfunction
    
    private function FilterUnit takes unit u returns boolean
        return IsUnitType(u, UNIT_TYPE_HERO) or IsUnitType(u, UNIT_TYPE_GROUND)
    endfunction
    // End of COnfigurables
    // ================================================================================
    
    public struct Magnum extends array
        implement SharedList
        unit cast                   // Casting Unit
        real dmg                    // Damage Dealt upon Explosion
        unit mis                    // Missile Unit
        effect fx                   // Effect of charging model
        real currentDist            // Distance curently traveled 
        real splashAoe              // Aoe of Explosion
        real splashDmg              // Splash Damage Factor
        real speed                  // Speed of missile
        real finalDist              // Maximum Distance
        real collisionSize          // Collision Size
        real cos                    // Cos of angle
        real sin                    // Sin of angle
        
        static group g = CreateGroup()
        static unit fog = null
        static player filterPlayer = null
        
        static thistype l
        static timer tmr = CreateTimer()
        static real period = 0.025
        
        // By default pre filters alive, enemy units
        static method filter takes nothing returns boolean
            return IsUnitEnemy(GetFilterUnit(), filterPlayer) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_DEAD) and GetWidgetLife(GetFilterUnit()) > .405
        endmethod
        
        // Explode method separated to allow flexible explosions
        // Can explode with or without a target
        method explode takes unit hitTarget returns nothing
            // Set the effect coordinates by default to the location of the missile
            local real sx = GetUnitX(.mis)
            local real sy = GetUnitY(.mis)
            
            // First of Group loop to deal damage to all filtered units
            set filterPlayer = GetOwningPlayer(.cast)
            call GroupEnumUnitsInRange(g, GetUnitX(.mis), GetUnitY(.mis), .splashAoe, function thistype.filter)
            set fog = FirstOfGroup(g)
            loop
                exitwhen fog == null
                // For now only affect non target units
                if (fog != hitTarget and FilterUnit(fog)) then
                    call UnitDamageTarget(.cast, fog, .dmg *.splashDmg, false, false, null, null, null)
                endif
                call GroupRemoveUnit(g, fog)
                set fog = FirstOfGroup(g)
            endloop
            
            // If there is a target to explode on, change the effect coordinates and deal full damage to the target
            if (hitTarget != null) then
                call UnitDamageTarget(.cast, hitTarget, .dmg, false, false, null, null, null)
                set sx = GetUnitX(hitTarget)
                set sy = GetUnitY(hitTarget)
            endif
            
            // Add explosion effect, remove missile, null handles and remove node
            call DestroyEffect(AddSpecialEffect(EXPLODE_FX, sx, sy))
            call RemoveUnitTimed(.mis, 1.0)
            call DestroyEffect(.fx)
            set .fx = null
            set .mis = null
            set .cast = null
            call .remove()
        endmethod
        
        // Separated a method to check for target for better readability - but probably could be inlined
        method checkTarget takes nothing returns boolean
            // First of Group to check for a valid target
            set filterPlayer = GetOwningPlayer(.cast)
            call GroupEnumUnitsInRange(g, GetUnitX(.mis), GetUnitY(.mis), .collisionSize, function thistype.filter)
            set fog = FirstOfGroup(g)
            loop
                exitwhen fog == null
                // Check if unit is valid, if so clear the group and exit the loop
                if (fog != null and FilterUnit(fog)) then
                    call .explode(fog)
                    call GroupClear(g)
                    set fog = null
                    return true
                // Else keep checking
                else
                    call GroupRemoveUnit(g, fog)
                    set fog = FirstOfGroup(g)
                endif
            endloop
            
            return false
        endmethod
        
        // Periodic Method
        static method onLoop takes nothing returns nothing
            local thistype this = l.first
            local thistype nn
            
            // Loop through every node in the list
            loop
                // Safety first!
                exitwhen this == l.sentinel
                set nn = .next
                
                // If the next movement is the last, explode with no target
                if (.currentDist + .speed > .finalDist) then
                    // Set unit to accurate final coordinates
                    call SetUnitX(.mis, GetUnitX(.mis)+ (.finalDist - .currentDist) * .cos)
                    call SetUnitY(.mis, GetUnitY(.mis)+ (.finalDist - .currentDist) * .sin)
                    
                    // If there are no valid targets, explode without one
                    if (not .checkTarget()) then
                        call .explode(null)
                    endif
                else
                    // Move coordinates and update distance traveled info
                    call SetUnitX(.mis, GetUnitX(.mis)+ (.speed) * .cos)
                    call SetUnitY(.mis, GetUnitY(.mis)+ (.speed) * .sin)
                    set .currentDist = .currentDist + .speed
                    
                    // Also check for a valid target
                    call .checkTarget()
                endif
                
                set this = nn
            endloop
            
            if (l.first == l.sentinel) then
                call PauseTimer(tmr)
            endif
        endmethod
        
        // Method used to create, setup and launch missiles
        static method fireAt takes unit caster, real dmg, real x, real y, real sx, real sy, integer lvl, /*
                                 */real speed, real colSize, real splDmg, real splAoe, real size returns nothing
            // Some simple math
            local real yy = sy - y
            local real xx = sx - x
            local real direction = Atan2(yy, xx)
            
            // New node
            local thistype this = l.enqueue()
            
            // Setup variables
            set .cast = caster 
            set .dmg = dmg
            set .collisionSize = colSize
            set .splashDmg = splDmg
            set .splashAoe = splAoe
            set .cos = Cos(direction)
            set .sin = Sin(direction)
            set .mis = CreateUnit(GetOwningPlayer(.cast), DUMMY_ID, x, y, direction*bj_RADTODEG)
            call SetUnitFlyHeight(.mis, MISSILE_Z, 0.0)
            set .fx = AddSpecialEffectTarget(MISSILE_FX, .mis, "origin")
            call SetUnitScale(.mis, size, size, size)
            set .finalDist = SquareRoot(xx*xx+yy*yy)
            set .currentDist = 0.0
            set .speed = speed
            
            if (l.first == this) then
                call TimerStart(tmr, period, true, function thistype.onLoop)
            endif
        endmethod
        
        // onInit list creation
        static method onInit takes nothing returns nothing
            set l = create()
        endmethod
    endstruct
    
    public struct Data extends array
        implement Alloc
    
        boolean stop                // Stop charging, start firing
        unit cast                   // Caster unit
        unit fx                     // Charge effect dummy unit
        effect fxx                  // FX model attached to dummy unit
        real sx                     // Spell Starting LocationX
        real sy                     // Spell Starting LocationY
        real aoe                    // Aoe of Barrage
        real dmg                    // Damage
        real speed                  // Misile Speed
        real missileSize            // Missile Size
        real collisionSize          // Missile Collision Size
        real splashDmg              // Splash Damage Factor
        real splashAoe              // Splash Aoe
        real lps                    // Load Per Second
        real fps                    // Fire Per Second
        real time                   // Time Charged
        integer lvl                 // Ability Level
        
        static Table tb
        
        // Method for rapid firing missiles until charge duration runs out
        static method rapidFire takes nothing returns boolean
            // Get Index
            local thistype this = TT_GetData()
            local real rx 
            local real ry
            local real rd
            local real rr
            
            // If there is enough charge time keep firing
            if (.time > 0) then
                set rr = GetRandomReal(0, 360) * bj_DEGTORAD
                set rd = GetRandomReal(0, .aoe/2.)
                set rx = .sx + rd * Cos(rr)
                set ry = .sy + rd * Sin(rr)
                call SetUnitAnimation(.cast, "attack")
                call Magnum.fireAt(.cast, .dmg, GetUnitX(.fx),GetUnitY(.fx), rx, ry, .lvl, .speed, .collisionSize, .splashDmg, .splashAoe, .missileSize)
            // Else stop firing, clear/null handles, reset everything and stop timer
            else
                call UnitRemoveAbility(.cast, PAUSE_ABIL_ID)
                call KillUnit(.fx)
                call DestroyEffect(.fxx)
                call tb.integer.remove(GetHandleId(.cast))
                call SetUnitPropWindow(.cast, GetUnitDefaultPropWindow(.cast) * bj_DEGTORAD)
                
                set .fx = null
                set .fxx = null
                set .cast = null
                call .deallocate()
                
                return true
            endif
            
            // Remove timed based on fire rate
            set .time = .time - .fps
            // Set charge effect model unit scale to time, so as spell runs the unit gets smaller
            call SetUnitScale(.fx, .time, .time, .time)
            
            return false 
        endmethod
        
        // Function that takes care of units while they are charging
        static method charging takes nothing returns boolean
            local thistype this = TT_GetData()
            
            // If unit should stop charging, starting rapid firing at fire rate period and stop this timer
            // Also the "stop" order sometimes disallows the pause ability from being added, so by adding another command here you can
            // make sure the unit will be paused properly.
            if (.stop) then
                call IssueImmediateOrder(.cast, PAUSE_ABIL_ORDER_STRING)
                call TT_StartEx(function thistype.rapidFire, this, .fps)
                return true 
            endif
            
            // Increase the charge duration and set the charge effect model unit scale
            set .time = .time + .lps
            call SetUnitScale(.fx, .time, .time, .time)
            
            return false 
        endmethod
        
        // Function fired on cast
        static method onCast takes nothing returns nothing
            // Allocation and some pre instance variables
            local thistype this = allocate()
            local unit u = GetTriggerUnit()
            local real x = GetUnitX(u)
            local real y = GetUnitY(u)
            local real dir = Atan2(GetSpellTargetY()-y,GetSpellTargetX()-x)
            local integer lvl = GetUnitAbilityLevel(u, ABIL_ID)
            local integer stats = STATS(GetHeroStr(u, true), GetHeroAgi(u, true), GetHeroInt(u, true))
            
            // Setup some variables, precalculate and save all numbers so that we don't have to re calculate them later during 
            // missile creation
            call SetUnitPropWindow(u, 0.0)
            set tb.integer[GetHandleId(u)] = this
            set .stop = false
            set .cast = u
            set .fx = CreateUnit(GetOwningPlayer(.cast), DUMMY_ID, x+60*Cos(dir), y+60*Sin(dir), dir*bj_RADTODEG)
            call SetUnitFlyHeight(.fx, CHARGE_FX_Z, 0.0)
            set .fxx =  AddSpecialEffectTarget(CHARGE_FX, .fx, "origin")
            call SetUnitScale(.fx, 0, 0, 0)
            set .sx = GetSpellTargetX()
            set .sy = GetSpellTargetY()
            set .dmg = DAMAGE(lvl, stats)
            set .aoe = CAST_AOE(lvl)
            set .lps = 1./LOAD_PER_SECOND(lvl)
            set .fps = 1./FIRE_PER_SECOND(lvl)
            set .speed = SPEED(lvl) * Magnum.period
            set .missileSize = MISSILE_SIZE(lvl)
            set .collisionSize = COLLISION_AOE(lvl)
            set .splashDmg = SPLASH_DAMAGE(lvl)
            set .splashAoe = SPLASH_AOE(lvl)
            set .time = 0.0
            set .lvl = lvl
            
            // Start the charging function loop
            call TT_StartEx(function thistype.charging, this, .lps)
            set u = null
        endmethod
        
        // When spell is canceled or finished
        static method onCancel takes nothing returns nothing
            local unit u = GetTriggerUnit()
            local thistype this = tb.integer[GetHandleId(u)]
            
            // If canceld ability is Magnum Barrage, unit has a valid instance, and said instance has not started firing yet...
            if GetSpellAbilityId() == ABIL_ID or (this != 0 and not .stop) then
                // Stop charging, start firing and pause unit
                // Note that if you stop your caster by ordering "stop" then the pause will not work properly thus
                // We re issue the pause ability command when the charging function ceases to make sure unit pauses properly
                set .stop = true
                call UnitAddAbility(.cast, PAUSE_ABIL_ID)
                call IssueImmediateOrder(.cast, PAUSE_ABIL_ORDER_STRING)
            endif
            
            set u = null
        endmethod
        
        // OnInit
        static method onInit takes nothing returns nothing
            set tb = Table.create()
            call RegisterSpellEffectEvent(ABIL_ID, function thistype.onCast)
            call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_SPELL_ENDCAST, function thistype.onCancel)
        endmethod
    endstruct

endscope

Changelog:
v1.0
- Initial upload
v1.1
- Code minor cleanups
- Code Documentation
- Now Requires TimedHandles to make sure missiles don't bug out groupEnum
- Custom FilterUnit function for customizable filter

Thanks for looking. Please notify me of bugs and better ways to code.
:>

Keywords:
Gun Explosion Fire Searing Arrow Ranged Archer Artillery Missile Shotgun Revolver Magnum Shot Barrage Launcher
Contents

Magnum 1.1v (Map)

Reviews
13:06, 11th Jun 2015 BPower: Check my post
Level 19
Joined
Mar 18, 2012
Messages
1,717
Cool spell. I tested ingame.

KillUnit is not a good option to remove dummies. When I wrote Missile I found out, that after using KillUnit on a unit which has locust, you can enumerate that unit with GroupEnumUnitsInRange....

You could pause all of your dummy units, as paused units have less computation time then normal units.

Using Missile would increase the configurability for the users, add read-ability,
good cleanup and you don't need a data structure like SharedList for every spell you write having the same style like this one.
Missile uses a List like structure internally. Sorry to advertise my own resource in your thread.
JASS:
    native UnitAlive takes unit id returns boolean
    
    // damage source, source owner and data (in this case ability level) are already set.
    // Set up missile, before beeing launched.
    // Very readable setup. Can be done by the user
    private function ConfigureMissile takes Missile missile, unit caster, integer level returns nothing
        set missile.damage    = DAMAGE(level, STATS(GetHeroStr(caster, true), GetHeroAgi(caster, true), GetHeroInt(caster, true)))// Talk about optimization later.
        set missile.model     = MISSILE_FX// could be non constant, passed in caster and level gives good configurability.
        set missile.speed     = SPEED(level)*0.03125
        set missile.collision = COLLISION_AOE(level)
        set missile.scale     = MISSILE_SIZE(level)
      //set missile.acceleration = x More options?
    endfunction
    
    
    public struct Magnum extends array

        //more options
        //  - onTerrain
        //  - onDestructable/Filter
    
        static method explode takes Missile missile, unit hitTarget returns nothing
            local unit u
            local real x = missile.x
            local real y = missile.y
            call GroupEnumUnitsInRange(bj_lastCreatedGroup, x, y, missile.collision + Missile_MAXIMUM_COLLISION_SIZE, null)
            loop
                set u = FirstOfGroup(bj_lastCreatedGroup)
                exitwhen u == null
                call GroupRemoveUnit(bj_lastCreatedGroup, u)
                if (u != hitTarget) and IsUnitInRange(u, missile.dummy, missile.collision) and UnitAlive(u) and IsUnitEnemy(u, missile.owner) then
                    call UnitDamageTarget(missile.source, u, missile.damage*SPLASH_DAMAGE(missile.data), false, false, null, null, null)
                endif
            endloop
                
            if (hitTarget != null) then
                call UnitDamageTarget(missile.source, hitTarget, missile.damage, false, false, null, null, null)
                set x = GetUnitX(hitTarget)
                set y = GetUnitY(hitTarget)
            endif
            
            call DestroyEffect(AddSpecialEffect(EXPLODE_FX, x, y))

        endmethod
        
        static method onCollide takes Missile missile, unit hit returns boolean
            if UnitAlive(hit) and IsUnitEnemy(hit, missile.owner) then
                call explode(missile, hit)
                return true// Return true removes the Missile
            endif
            return false// return false let's the Missile fly further
        endmethod
        
        static method onFinish takes Missile missile returns boolean
            call explode(missile, null)
            return true// Required otherwise the Missile doesn't stop
        endmethod
        
        implement MissileStruct
                 
        static method fireAt takes unit caster, real x, real y, real sx, real sy, integer level returns nothing
            local Missile m = Missile.createXYZ(x, y, MISSILE_Z, sx, sy, MISSILE_Z)
            set m.source    = caster
            set m.owner     = GetOwningPlayer(caster)
            set m.data      = level
            call ConfigureMissile(m, caster, level)
            call launch(m)
        endmethod
        
    endstruct
 
Last edited:
KillUnit is not a good option to remove dummies. When I wrote Missile I found out, that after using KillUnit on a unit which has locust, you can enumerate that unit with GroupEnumUnitsInRange.
Does it have something to do with locust?
When a unit "does not raise and not decay" it should be completly removed after expiration of it's "deathtime", after it's killed.

But killingunit will still show the death animation, that might look better sometimes than just removing. (haven't tested the spell yet)

you can enumerate that unit with GroupEnumUnitsInRange
Wth EnumUnityByPlayer it should work, as well.

JASS:
real fd
real cs
real dir
^I can't imagine their purpose with reading their names.
 
Level 19
Joined
Mar 18, 2012
Messages
1,717
My first attempt to kill dummies in Missile was KillUnit. These units are normal dummies (locust + dummy.mdl, do not deay/raise)
In the test map missiles got then destroyed always one tick earlier then the previous missile (same launch angle)
So I found out that once you use KillUnit, dummies are enumerated by GroupEnumUnitsinRange aswell until they are removed.

Wth EnumUnityByPlayer it should work, as well.
I know. Maybe it was not clear, but that phrase is related to the problem stated above.

But killingunit will still show the death animation, that might look better sometimes than just removing. (haven't tested the spell yet)
That's true, but as mentioned KillUnit is not a good option.
In Missile, I wrote a delayed dummy recycler/remover where units are thrown in if you declare static method onRemove takes Missile this returns boolean and return true
Again I advertise Missile, since it solves all these small problems, if you wish them to be solved.
 
Level 7
Joined
Feb 3, 2013
Messages
277
Thanks ^, I've made all recommended changes except using Missile. Well I did actually have a working code with it, but by the time I got it to work properly I had already cleaned up my previous code. THe code length is really not that much of a difference anyways....

Thanks to all for their feedback.
 
Level 19
Joined
Mar 18, 2012
Messages
1,717
Looks good on a quick view. Sadly I can't check the demo map right now, maybe tomorrow.

Also sad that you gave up on Missile, however it was anyway just a optional recommendation from my side.
The main benefits comes, when a map maker want to have various different projectile spells or effects in his map.
Because the code accumilates quickly and also does the overall created amount of handles.
 
Level 19
Joined
Mar 18, 2012
Messages
1,717
In method explode you have an unnessesary GetUnitX/Y native call, as you already stored those values before.

set unit fog only inside the loop and not once before. Simply restructure that part into
fog should also be a local unit and not a static.
JASS:
set fog = FirstOfGroup(group)
exitwhen fog == null
call GroupRemoveUnit(group, fog)

Damage type and attack type could be different to null or make them constant variables.

When dealing with missiles, you need safety. Add either WorldBounds or mention that BoundSentinel is a useful library to have.

Overall coding is ok. You are using a lot of resources not seen so often to THW
Like TT and SharedList

Please remove Missile library related stuff from the demo map, if not used by your spell.
This just confuses the user. A submission should only cover, what is really required for the spell.
 
Top