Spell Lightning Cannon

Flare

Stops copies me!
Reaction score
662
Name may need some improving ^_^

Technical details:
Made in vJASS
Fully MUI
Requires NewGen WE and Strilanc's Dedicated Unit Indexer
Leak-free (to my knowledge)
Lag-free (for me)
Importing instructions located in the map

Lightning Cannon
The hero begins charging a large ball of electricity. When the hero stops, the ball will travel towards the target point, unleashing bolts of lightning at random, and damage units in its path.

Screenshots:
lightningcannon1ft8.jpg
lightningcannon2xf3.jpg
Testing the spell for itself is essential, the lightning bolts are nowhere near as awesome with still images

Code:
JASS:
//Lightning Cannon
//By Flare
//Requires: NewGen WE
//          Strilanc's Dedicated Unit Indexer
//          Lightning Cannon [dummy] (h000)
//          Lightning Cannon [abil] (A000)

//NOTE: DO NOT APPLY A CUSTOM VALUE TO THE DUMMY UNIT, IT'S ALREADY IN USE

library LightningCannon initializer SpellInit requires DUI
//Configurables
globals
    private constant real TIMER_INT = 0.03125
    private constant integer SID = 'A000'
    private constant integer DID = 'h000'
//Fly height of the dummy
    private constant real D_HEIGHT = 150
//Scaling size of the dummy
    private constant real BASE_SCALE = 0.75
//Scaling modifier, based on current power as a % of max power
    private constant real SCALE_MOD = 1.25
//Special effect created when a unit touches orb
    private constant string DAMAGE_EFFECT = "Abilities\\Weapons\\Bolt\\BoltImpact.mdl"
//Area of effect for orb damage
    private constant real ORB_RADIUS = 125
//Determines whether lightning effects are spawned
    private constant boolean CREATE_LIGHT = true
//Lightning ID
    private constant string LIGHT_ID = "CLPB"
//Determines if the lightning effects move
//Direction of travel is the angle between the orb and lightning target AT LIGHTNING CREATION
    private constant boolean MOVE_LIGHT = true
//The speed (per sec) travelled by the lightning effects (assuming MOVE_LIGHT is true)
    private constant real LIGHT_SPEED = 400
//Frequency of lightning effects
    private constant real LIGHT_FREQ = 0.2
//Randomization of lightning target position
    private constant real LIGHT_OFFSET_LOW = 100
    private constant real LIGHT_OFFSET_HIGH = 300
//Start/end values for lightning alpha
    private constant real START_ALPHA = 1
    private constant real END_ALPHA = 0
//RGB values for the lightning
    private constant real R = 1
    private constant real G = 1
    private constant real B = 1
//Duration of the lightning
    private constant real LIGHT_DUR = 0.3
//Frequency of special effects generated at lightning end
//Also determines how often the lightning deals damage
    private constant real LIGHT_EFFECT_FREQ = 0.0625
//Area of effect for random lightning damage
    private constant real LIGHT_DMG_RADIUS = 90
//Effect generated at lightning end
    private constant string LIGHT_SFX = "Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl"
//Projectile speed
    private constant real SPEED = 800
//Attack, damage, weapon types
    private constant attacktype AT = ATTACK_TYPE_MAGIC
    private constant damagetype DT = DAMAGE_TYPE_NORMAL
    private constant weapontype WT = WEAPON_TYPE_WHOKNOWS
endglobals

//Proportion of power that is converted to damage dealt
private constant function Damage takes integer level returns real
    return 50. + level * 50.
endfunction

//Default amount of power stored by the orb
private constant function MinPower takes integer level returns real
    return 100. + level * 50.
endfunction

//Maximum energy storable by the orb
private constant function MaxPower takes integer level returns real
    return 300. + level * 150.
endfunction

//Rate of power gain
private constant function StoreRate takes integer level returns real
    return 25. + level * 25.
endfunction

//Amount of damage dealt by random lightning bolts
private constant function LightningDamage takes integer level returns real
    return 3. + level * 2.
endfunction

//End of configurables
//Don't modify anything below this point
private keyword LData
private keyword SData


globals
    private timer ST = CreateTimer ()
    private integer SN = 0
    private SData array SDC
    private SData array SDD
    private timer LT = CreateTimer ()
    private integer LN = 0
    private LData array LD
    private group Enum = CreateGroup ()
    private group Casters = CreateGroup ()
    private group Movers = CreateGroup ()
    private LData lz
    private SData sz
    private boolexpr DamageFunc
    private constant real UNITS_PER_INT = SPEED * TIMER_INT
    private location Loc = Location (0, 0)
endglobals


private struct LData
    lightning l
    unit u
    real tx
    real ty
    real tz
    real dur
    real alpha
    real fadeamt
    real fxticker
    real lightdmg
    real coffset
    real soffset
endstruct


private struct SData
    unit caster
    unit orb
    real curpower
    real maxpower
    real cosoffset
    real sinoffset
    real dist
    real lticker
    group affected
    boolean moving
    real dmg
    real storerate
    method onDestroy takes nothing returns nothing
        call GroupClear (.affected)
        call KillUnit (.orb)
    endmethod
endstruct

private function GetCoordZ takes real x, real y returns real
    call MoveLocation (Loc, x, y)
    return GetLocationZ (Loc)
endfunction


private function LightDamage takes nothing returns boolean
    local unit f = GetFilterUnit ()
    if IsUnitEnemy (f, GetOwningPlayer (lz.u)) and IsUnitType (f, UNIT_TYPE_DEAD) == false then
        call UnitDamageTarget (lz.u, f, LightningDamage (GetUnitUserData (lz.u)), false, true, AT, DT, WT)
    endif
    set f = null
    return false
endfunction

private function LightCallback takes nothing returns nothing
    local integer i = LN
    local real ux
    local real uy
    local LData a
    loop
    exitwhen i == 0
        set a = LD<i>
        set ux = GetUnitX (a.u)
        set uy = GetUnitY (a.u)
        set a.alpha = a.alpha - a.fadeamt
        set a.dur = a.dur - TIMER_INT
        set a.tx = a.tx + a.coffset
        set a.ty = a.ty + a.soffset
        set a.fxticker = a.fxticker + TIMER_INT
        if a.fxticker &gt;= LIGHT_EFFECT_FREQ then
            call DestroyEffect (AddSpecialEffect (LIGHT_SFX, a.tx, a.ty))
            set lz = a
            call GroupEnumUnitsInRange (Enum, a.tx, a.ty, LIGHT_DMG_RADIUS, Condition (function LightDamage))
        endif
        
        if a.alpha &gt; 1 then
            set a.alpha = 1
        elseif a.alpha &lt; 0 then
            set a.alpha = 0
        endif
        call SetLightningColor (a.l, R, G, B, a.alpha)
        call MoveLightningEx (a.l, true, ux, uy, GetUnitFlyHeight (a.u), a.tx, a.ty, a.tz)
        if a.dur &lt;= 0 then
            call DestroyLightning (a.l)
            call a.destroy ()
            set LD<i> = LD[LN]
            set LN = LN - 1
            if LN == 0 then
                call PauseTimer (LT)
            endif
        endif
        set i = i - 1
    endloop
endfunction



private function AddTimedLightning takes string id, real duration, unit u returns nothing
    local LData a = LData.create ()
    local real r1 = GetRandomReal (0, bj_PI * 2)
    local real r2 = GetRandomReal (LIGHT_OFFSET_LOW, LIGHT_OFFSET_HIGH)
    local real c = Cos (r1)
    local real s = Sin (r1)
    local real x1 = GetUnitX (u)
    local real y1 = GetUnitY (u)
    local real z1 = GetUnitFlyHeight (u)
    local real x2 = x1 + c * r2
    local real y2 = y1 + s * r2
    local real z2 = GetCoordZ (x2, y2)
    set a.tx = x2
    set a.ty = y2
    set a.tz = z2
    set a.fxticker = 0
    if MOVE_LIGHT then
        set a.coffset = c * LIGHT_SPEED * TIMER_INT
        set a.soffset = s * LIGHT_SPEED * TIMER_INT
    else
        set a.coffset = 0
        set a.soffset = 0
    endif
    set a.u = u
    set a.l = AddLightningEx (id, true, x1, y1, z1, x2, y2, z2)
    set a.alpha = START_ALPHA
    set a.dur = duration
    set a.fadeamt = (TIMER_INT/duration) * (START_ALPHA - END_ALPHA)
    call DestroyEffect (AddSpecialEffect (LIGHT_SFX, a.tx, a.ty))
    set lz = a
    call GroupEnumUnitsInRange (Enum, a.tx, a.ty, LIGHT_DMG_RADIUS, Condition (function LightDamage))
    if LN == 0 then
        call TimerStart (LT, TIMER_INT, true, function LightCallback)
    endif
    set LN = LN + 1
    set LD[LN] = a
endfunction


private function OrbDamage takes nothing returns boolean
    local unit u = GetFilterUnit ()
    if IsUnitEnemy (u, GetOwningPlayer (sz.caster)) and IsUnitType (u, UNIT_TYPE_DEAD) == false and IsUnitInGroup (u, sz.affected) == false then
        call UnitDamageTarget (sz.caster, u, (sz.curpower/sz.maxpower) * sz.dmg, false, true, AT, DT, WT)
        call DestroyEffect (AddSpecialEffect (DAMAGE_EFFECT, GetUnitX (u), GetUnitY (u)))
        call GroupAddUnit (sz.affected, u)
    endif
    set u = null
    return false
endfunction

private function MoveFunc takes nothing returns nothing
    local SData a = SDD[DUI_GetUnitIndex (GetEnumUnit ())]
    local real x = GetUnitX (a.orb)
    local real y = GetUnitY (a.orb)
    set x = x + a.cosoffset * UNITS_PER_INT
    set y = y + a.sinoffset * UNITS_PER_INT
    call SetUnitPosition (a.orb, x, y)
    set a.dist = a.dist - UNITS_PER_INT
    set sz = a
    call GroupEnumUnitsInRange (Enum, x, y, ORB_RADIUS, DamageFunc)
    
    set a.lticker = a.lticker + TIMER_INT
    if a.lticker &gt;= LIGHT_FREQ and CREATE_LIGHT then
        set a.lticker = a.lticker - LIGHT_FREQ
        call AddTimedLightning (LIGHT_ID, LIGHT_DUR, a.orb)
    endif
    
    if a.dist &lt;= 0 then
        call GroupRemoveUnit (Movers, a.orb)
        call a.destroy ()
        set SN = SN - 1
        if SN == 0 then
            call PauseTimer (ST)
        endif
    endif
endfunction


private function CasterFunc takes nothing returns nothing
    local SData a = SDC[DUI_GetUnitIndex (GetEnumUnit ())]
    local real x = GetUnitX (a.orb)
    local real y = GetUnitY (a.orb)
    local real s
    set a.lticker = a.lticker + TIMER_INT
    if a.lticker &gt;= LIGHT_FREQ and CREATE_LIGHT then
        set a.lticker = a.lticker - LIGHT_FREQ
        call AddTimedLightning (LIGHT_ID, LIGHT_DUR, a.orb)
    endif
    
    set a.curpower = a.curpower + a.storerate
    if a.curpower &gt; a.maxpower then
        set a.curpower = a.maxpower
    endif
    set s = BASE_SCALE + SCALE_MOD * (a.curpower/a.maxpower)
    call SetUnitScale (a.orb, s, s, s)
endfunction


private function SpellCallback takes nothing returns nothing
    call ForGroup (Casters, function CasterFunc)
    call ForGroup (Movers, function MoveFunc)
endfunction


private function SpellCond takes nothing returns boolean
    return GetSpellAbilityId () == SID
endfunction


private function SpellActions takes nothing returns nothing
    local SData a
    local unit c = GetTriggerUnit ()
    local location l = GetSpellTargetLoc ()
    local real cx = GetUnitX (c)
    local real cy = GetUnitY (c)
    local real tx = GetLocationX (l)
    local real ty = GetLocationY (l)
    local real dx = tx - cx
    local real dy = ty - cy
    local real angle = Atan2 (dy, dx)
    local integer lvl = GetUnitAbilityLevel (c, SID)
    if GetTriggerEventId () == EVENT_PLAYER_UNIT_SPELL_EFFECT then
        set a = SData.create ()
        set a.caster = c
        set a.orb = CreateUnit (GetOwningPlayer (c), DID, cx, cy, angle * bj_DEGTORAD)
        call GroupAddUnit (Casters, c)
        set SDC[DUI_GetUnitIndex(c)] = a
        call SetUnitUserData (a.orb, lvl)
        call SetUnitPathing (a.orb, false)
        call SetUnitFlyHeight (a.orb, D_HEIGHT, 0)
        set a.dist = SquareRoot (dx * dx + dy * dy)
        set a.lticker = 0
        set a.curpower = MinPower (lvl)
        set a.maxpower = MaxPower (lvl)
        set a.storerate = StoreRate (lvl)
        set a.moving = false
        set a.cosoffset = Cos (angle)
        set a.sinoffset = Sin (angle)
        set a.dmg = Damage (lvl)
        if a.affected == null then
            set a.affected = CreateGroup ()
        endif
        if SN == 0 then
            call TimerStart (ST, TIMER_INT, true, function SpellCallback)
        endif
        set SN = SN + 1
    elseif GetTriggerEventId () == EVENT_PLAYER_UNIT_SPELL_ENDCAST then
        set a = SDC[DUI_GetUnitIndex (c)]
        set SDD[DUI_GetUnitIndex (a.orb)] = a
        call GroupRemoveUnit (Casters, c)
        call GroupAddUnit (Movers, a.orb)
    endif
    call RemoveLocation (l)
    set l = null
    set c = null
endfunction


private function SpellInit takes nothing returns nothing
    local trigger t = CreateTrigger ()
    call TriggerRegisterAnyUnitEventBJ (t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerRegisterAnyUnitEventBJ (t, EVENT_PLAYER_UNIT_SPELL_ENDCAST)
    call TriggerAddCondition (t, Condition (function SpellCond))
    call TriggerAddAction (t, function SpellActions)
    set DamageFunc = Condition (function OrbDamage)
endfunction

endlibrary</i></i>


Credits:
If you use this spell, please credit me for making it

Update log:
v1 - Initial release
v2 - Fixed a bug where same unit couldn't repeatedly cast
v3 - Reduced the channelling time to 5 seconds, did a bit of tweaking to the lightning config
v4 - Modified the lightning SFX generation a little, first SFX spawns at lightning creation, rather than LIGHT_EFFECT_FREQ seconds afterwards
v5 - Lightning now capable of moving, added some new constants to control it
v6 - Made the orb damage a bit more sensible (now deals up to X damage, based on current power as a % of max power)
 

Attachments

  • Lightning Cannon (v6).w3x
    32.4 KB · Views: 553

Exide

I am amazingly focused right now!
Reaction score
448
It's a cool spell and a good idea.
I'm not sure if channeling it longer does anything, though? (It should do more damage the longer it's channeled, imo.)

I also made it bug. :p
I first selected every hero one by one and set the ability level to 1.
Then I selected all heroes and ordered them to use the ability on a point. -Several times.
Which made it bug. Try and see if you can re-create it.

Also I believe the hero can channel the ability forever (I didn't let him channel for that long, but I'm sure he could've gone on for quite some time.)
Maybe give it a max of 5 seconds to channel?
 

Flare

Stops copies me!
Reaction score
662
It's a cool spell and a good idea.
I'm not sure if channeling it longer does anything, though? (It should do more damage the longer it's channeled, imo.)

I also made it bug. :p
I first selected every hero one by one and set the ability level to 1.
Then I selected all heroes and ordered them to use the ability on a point. -Several times.
Which made it bug. Try and see if you can re-create it.

Also I believe the hero can channel the ability forever (I didn't let him channel for that long, but I'm sure he could've gone on for quite some time.)
Maybe give it a max of 5 seconds to channel?

1) It does - the longer it channels, the bigger it gets, the more damage the projectile does

2) Ahm... in what way did it bug? Just telling me 'it bugs' isn't much help, since I can't recreate a situation if I don't know what that situation is supposed to look like :p

3) It's 20 seconds Follow Through Time in the Object Editor, so it's not forever. Will change it though

EDIT: Ah, found the bug, gonna be tricky to fix it though
 

Larcenist

REP: Respect, Envy, Prosperity?
Reaction score
211
Seems like you just rushed on to get finished with it to me, your description on xfire gave me a totally different idea of this spell. Quit eneat still, but the original idea was way better.

>1) It does - the longer it channels, the bigger it gets, the more damage the projectile does

I did not notice this at all, and from what I saw, the only additional damage came from the lightnings while channeling, not by the time channeled.
 

Flare

Stops copies me!
Reaction score
662
In my seemingly infinite wisdom :)P), I think I've solved the bug you mentioned, Exide (assuming it was where the projectiles would stop moving if the same caster used the ability again before their previous projectile died)

@Larcenist: Modify the DamageFactor function then? Channel for different durations? Also, I decided that I didn't really want to add more Object Editor stuff (like a dummy ability, buff, etc), and normal lightning owns green lightning, so that settled that issue :D
 

Romek

Super Moderator
Reaction score
963
The name's great. No need to change it :D

Is there any reason why this is a library, not a scope?
Though that's not really a big problem.

Also, maybe you could provide a link to DUI?
I've only heard of it once really. And It's not really popular around here.

Actually, is there any reason why you didn't use PUI? (You always used to use it!)

And I don't see anything wrong with the code at the moment. Which is always a good sign. :D
 

Flare

Stops copies me!
Reaction score
662
Is there any reason why this is a library, not a scope?
Further reminder that the spell requires DUI :p

Also, maybe you could provide a link to DUI?
I did... check the Technical Details section again, I linked to both DUI and NewGen threads

Actually, is there any reason why you didn't use PUI?
I'll let Strilanc do the explaining on this one :p
Strilanc said:
To be honest, the main reason I don't like using unit custom data for indexing is because I like to abstract things out. I like to write libraries for things like spawning, pathing, etc, that don't know anything about the map they are being used in. I like to be able to open map A, and drop any library from it into map B (with dependencies), and have things still work in map B. That's why I'm not particularly fond of libraries that use custom data, libraries that require things in the object editor (eg. SetUnitMax), or libraries that need a ton of information about the map before they work (AMHS is pretty bad at this).
In my opinion, the less requirements a spell has, the better (and, if at all possible, I would've done this without DUI for that very reason, but it would've just been really lame)

(You always used to use it!)
I used PUI once (I think)... not sure how that translates to "always"

And I don't see anything wrong with the code at the moment
There is one small little thing (not a big deal, but it's noticeable in-game), going to fix that shortly :D


UPDATED - SEE UPDATE LOG FOR CHANGES
 

Viikuna

No Marlo no game.
Reaction score
265
I'll let Strilanc do the explaining on this one

Quote:
Originally Posted by Strilanc
To be honest, the main reason I don't like using unit custom data for indexing is because I like to abstract things out. I like to write libraries for things like spawning, pathing, etc, that don't know anything about the map they are being used in. I like to be able to open map A, and drop any library from it into map B (with dependencies), and have things still work in map B. That's why I'm not particularly fond of libraries that use custom data, libraries that require things in the object editor (eg. SetUnitMax), or libraries that need a ton of information about the map before they work (AMHS is pretty bad at this).

In my opinion, the less requirements a spell has, the better (and, if at all possible, I would've done this without DUI for that very reason, but it would've just been really lame)

This is very true when you make spells and dont know where they are going to be used.

But when making your own map things are of course different. I dont even need any attaching systems in my map, and I can recycle all my units so indexing is easy too.
 

Romek

Super Moderator
Reaction score
963
JASS:
set a = LD

You could just use LD directly there. One less variable.
Though it really brings down the readability, so it's not really a major problem.

Approved
 

Romek

Super Moderator
Reaction score
963
> I could, but if you had to type LD 34 times in a single function, you would understand why it's there
Yeah. I thought about that :D

You could CnP. Or Find + Replace.
Either way. It's not a problem. :)
 
General chit-chat
Help Users
  • No one is chatting at the moment.

      The Helper Discord

      Staff online

      Members online

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top