HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Lightning Blast Ball

08-23-2008, 10:33 PM#1
Gwypaas
vJass: Yes.
MUI: Yes.
Laggless: Yes.
Leakless: Yes
Requires: NewGen Editor

Description: (Is copied from tooltip)
Hidden information:
Summons a lightning ball that moves towards a target point enlarging and moves upwards.
When the ball reaches its target it crashes down and deals damage in an AoE

The damage increases the further from the caster the target is.

Screenshots:
Hidden information:



Code:
Collapse JASS:
struct LightningBlastBall // Requires TimerUtils 
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//              Implementing Instructions:
//  1: Create a trigger named LightningBlastBall and copy all this code into that trigger.
//  2: Import TimerUtils if you haven't them already imported into your map already.
//  3: Create the dummy unit and the ability
//  4: Set the SPELL_ID and DUMMY_ID to the ID's of your created dummy and ability.
//  5: Configure the spell the way you want it.
//
//
//  WARNING: If you see that the handle amount goes up in the handle counter that is NOT because this spell leaks.
//           It is because I do not remove the units, I kill them and then lets WC3 take care of them as they should be taken care of
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Configuration vars:
    private static constant integer SPELL_ID = 'A000' //the ID of the spell.   
    private static constant integer DUMMY_ID = 'h001' // the dummys ID
    
    private static constant real TIMER_PERIOD = 0.025      // How often the timer runs, increasing this will lower the speed of the balls.
    private static constant real SPEED = 10                // The speed of the lightning ball.
    private static constant real DAMAGE_INCREASE = 1.6     // This value is used to calculate the damage, the damage is calculated this way
                                                           // lvl*DAMAGE_INCREASE
    private static constant real HEIGHT_INCREASE = 7       // How much the dummy's height increases per interval.
    private static constant real DROP_SPEED_INCREASE = 7   // How much the drop speed increases per interval

    private static constant real START_SCALING = 0.40      // The size of the unit when the spell starts
    private static constant real SCALE_INCREASE = 0.03     // How much the units size increases every interval  
    
    private static constant integer MAX_AMOUNT_OF_DUMMIES_USED_IN_NOVA = 30 // The max amount of dummies the nova can use.
    
    private static constant string SOUND_PATH = "Abilities\\Spells\\Human\\StormBolt\\ThunderBoltMissileDeath.wav"
                                                           // The sound you want this spell to use.
    
// The damage filter, Edit this to make it target the types of units you want to
    private static constant method DamageFilter takes unit caster, unit target returns boolean
        return (IsUnitEnemy(target, GetOwningPlayer(caster))) and (IsUnitType(target, UNIT_TYPE_GROUND)) and (GetUnitState(target, UNIT_STATE_LIFE) > 0.405)
    endmethod 
    
// The Damage amount this spell deas, edit this to configure the damage you want it to deal.
    private static constant method DamageAmount takes integer lvl returns real
        return lvl*.DAMAGE_INCREASE
    endmethod

// The function that controls the amount of dummies that the nova creates. If you want it to spawn dummies based on level then just do something like: 20*lvl.
    private static constant method NovaDummyAmount takes integer lvl returns integer
        return 20
    endmethod
    
// The function that controls the radius of the Nova.    
    private static constant method NovaRadius takes integer lvl returns integer
        return 300
    endmethod  
    
// The function that controls the radius around every unit in the nova that should be damaged.
    private static constant method DamageRadius takes integer lvl returns integer
        return 30
    endmethod

// The function that controls the speed of the nove units.
    private static constant method NovaSpeed takes integer lvl returns integer
        return 15
    endmethod
    
// The function that controls the size of the dummies used in the nova.
    private static constant method NovaSize takes integer lvl returns integer
        return 8
    endmethod
    
// Don't edit below this line if you don't know what you're doing.
        private static boolexpr RETURN_TRUE
        private static location globalLoc = Location(0,0)
        private static LightningBlastBall array LBB
        private real currX
        private real currY
        private real distdone = 0
        private real totaldist
        private real cos
        private real sin
        private real angle
        private real currheight = 0
        private real totaldrop
        private unit whichUnit
        private real currscale = .START_SCALING
        private real dropspeed = .DROP_SPEED_INCREASE
        private unit damager
        private real currdamage = 0
        private unit array novaUnits[.MAX_AMOUNT_OF_DUMMIES_USED_IN_NOVA]
        private real array novasin[.MAX_AMOUNT_OF_DUMMIES_USED_IN_NOVA]
        private real array novacos[.MAX_AMOUNT_OF_DUMMIES_USED_IN_NOVA]
        private real array novaUnitsX[.MAX_AMOUNT_OF_DUMMIES_USED_IN_NOVA]
        private real array novaUnitsY[.MAX_AMOUNT_OF_DUMMIES_USED_IN_NOVA]
        private boolean novaStarted = false
        private group damagedUnits
        private timer t
        
    private static method H2I takes handle h returns integer i
        return h
        return 0
    endmethod
    
    private static method create takes nothing returns LightningBlastBall
        local LightningBlastBall LBBT = .allocate()
        set LBBT.damagedUnits = CreateGroup()
        return LBBT
    endmethod
    
    private method onDestroy takes nothing returns nothing
        local integer i = 0
        loop
            exitwhen i >= .MAX_AMOUNT_OF_DUMMIES_USED_IN_NOVA
            call ShowUnit(.novaUnits[i], false)
            call KillUnit(.novaUnits[i])
            set .novaUnits[i] = null
            set i = i+1
        endloop
        call DestroyGroup(.damagedUnits)
        set .damagedUnits = null
        set .whichUnit = null
        set .damager = null
        call ReleaseTimer(.t)
    endmethod

    private static method Nova takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local LightningBlastBall l = .LBB[.H2I(t)-0x100000]
        local real totalAngle = 0
        local real between
        local integer i = 0
        local real Nova_D_A = .NovaDummyAmount(GetUnitAbilityLevel(l.damager, .SPELL_ID))
        local real scale = (l.currscale/Nova_D_A)*.NovaSize(GetUnitAbilityLevel(l.damager, .SPELL_ID))
        local group unitsInRange = CreateGroup()
        local unit u
        local real D_R = .DamageRadius(GetUnitAbilityLevel(l.damager, .SPELL_ID))
        local real N_Speed 
        if l.novaStarted == false then
            set between = 360/Nova_D_A
            set N_Speed = .NovaSpeed(GetUnitAbilityLevel(l.damager, .SPELL_ID))
            loop
                exitwhen totalAngle >= 360
                set l.novacos[i] = N_Speed * Cos(bj_DEGTORAD * totalAngle)
                set l.novasin[i] = N_Speed * Sin(bj_DEGTORAD * totalAngle)
                set l.novaUnits[i] = CreateUnit(GetOwningPlayer(l.damager), .DUMMY_ID, l.currX, l.currY, totalAngle)
                set l.novaUnitsX[i] = GetUnitX(l.novaUnits[i])
                set l.novaUnitsY[i] = GetUnitY(l.novaUnits[i])
                call SetUnitScale(l.novaUnits[i], scale,scale,scale)
                set totalAngle = totalAngle+between
                set i = i+1
            endloop
            set i = 1
            set l.novaStarted = true
        endif
        if l.distdone >= .NovaRadius(GetUnitAbilityLevel(l.damager, .SPELL_ID)) then                
            call l.destroy()
        else
            loop
                exitwhen i >= Nova_D_A
                set l.novaUnitsX[i] = l.novaUnitsX[i] + l.novacos[i]
                set l.novaUnitsY[i] = l.novaUnitsY[i] + l.novasin[i]
                call SetUnitX(l.novaUnits[i], l.novaUnitsX[i])
                call SetUnitY(l.novaUnits[i], l.novaUnitsY[i])
                call GroupClear(unitsInRange)
                call GroupEnumUnitsInRange(unitsInRange, l.novaUnitsX[i], l.novaUnitsY[i], .DamageRadius(GetUnitAbilityLevel(l.damager, .SPELL_ID)), .RETURN_TRUE)
                loop
                    set u = FirstOfGroup(unitsInRange)
                    exitwhen u == null
                    if .DamageFilter(l.damager, u) == true and IsUnitInGroup(u, l.damagedUnits) == false then
                        call UnitDamageTarget(l.damager, u, l.currdamage , false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_UNIVERSAL, null)
                    endif
                    call GroupRemoveUnit(unitsInRange, u)
                    call GroupAddUnit(l.damagedUnits, u)
                    set u = null
                endloop
                set i = i+1
            endloop
            set l.distdone = l.distdone + .NovaSpeed(GetUnitAbilityLevel(l.damager, .SPELL_ID))
        endif
        set unitsInRange = null
        set u = null
        set t = null
    endmethod
    
    private static method Drop takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local sound s
        local LightningBlastBall l = .LBB[.H2I(t)-0x100000]
        if l.currheight >= l.totaldrop-.DROP_SPEED_INCREASE then
            set l.distdone = 0
            call PauseTimer(l.t)
            call TimerStart(l.t, .TIMER_PERIOD, true, function LightningBlastBall.Nova)
            call ShowUnit(l.whichUnit, false)
            call KillUnit(l.whichUnit)
            set s = CreateSound(.SOUND_PATH, false, true, true, 0,0,"a")
            call SetSoundPosition(s, l.currX, l.currY, 0)
            call SetSoundVolume(s, 110)
            if (s != null) then
                call StartSound(s)
            endif
            call KillSoundWhenDone(s)
        else
            call SetUnitFlyHeight(l.whichUnit, l.totaldrop-l.currheight, 0)
            set l.currheight = l.currheight + l.dropspeed
            set l.dropspeed = l.dropspeed + .DROP_SPEED_INCREASE
        endif
        set t = null
        set s = null
    endmethod
    
    private static method TimerAct takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local LightningBlastBall l = .LBB[.H2I(t)-0x100000]
        if l.distdone >= l.totaldist then
            set l.totaldrop = l.currheight
            set l.currheight = 0
            call PauseTimer(l.t)
            call TimerStart(l.t, .TIMER_PERIOD, true, function LightningBlastBall.Drop)
        else
            set l.currX = l.currX + l.cos
            set l.currY = l.currY + l.sin
            call SetUnitX(l.whichUnit, l.currX)
            call SetUnitY(l.whichUnit, l.currY)
            call SetUnitFlyHeight(l.whichUnit, l.currheight, 0)
            call SetUnitScale(l.whichUnit, l.currscale , l.currscale , l.currscale)
            set l.currscale = l.currscale + .SCALE_INCREASE
            set l.distdone = l.distdone + .SPEED
            set l.currheight = l.currheight + .HEIGHT_INCREASE
            set l.currdamage = l.currdamage + .DamageAmount(GetUnitAbilityLevel(l.damager, .SPELL_ID))
        endif
        set t = null
    endmethod
    
    private static method Actions takes nothing returns nothing
        local real tarx 
        local real tary
        local LightningBlastBall l = .create()
        local unit caster = GetSpellAbilityUnit()
        set l.t = NewTimer()
        set l.damager = caster
        set .globalLoc = GetSpellTargetLoc()
        set tarx = GetLocationX(.globalLoc)
        set tary = GetLocationY(.globalLoc)
        set l.currX = GetUnitX(caster)
        set l.currY = GetUnitY(caster)
        set l.angle = Atan2((tary - l.currY), (tarx - l.currX))
        set l.totaldist = SquareRoot((tarx-l.currX)*(tarx-l.currX) + (tary-l.currY)*(tary-l.currY))
        set l.sin = .SPEED * Sin(l.angle)
        set l.cos = .SPEED * Cos(l.angle)
        set l.whichUnit = CreateUnit(GetOwningPlayer(caster), .DUMMY_ID, l.currX, l.currY, l.angle*bj_RADTODEG)
        call SetUnitScale(l.whichUnit, .START_SCALING , .START_SCALING , .START_SCALING)
        set .LBB[.H2I(l.t)-0x100000] = l
        call TimerStart(l.t, .TIMER_PERIOD, true, function LightningBlastBall.TimerAct)
        set caster = null
        call RemoveLocation(.globalLoc)
        set .globalLoc = null
    endmethod
    
    private static method Cond takes nothing returns boolean
        return GetSpellAbilityId() == .SPELL_ID
    endmethod
    
    private static method BoolexprReturnTrue takes nothing returns boolean
        return true
    endmethod
    
    private static method onInit takes nothing returns nothing
        local trigger Trig = CreateTrigger()
        local integer i = 0
        set .RETURN_TRUE=Condition(function LightningBlastBall.BoolexprReturnTrue)
        loop
            call TriggerRegisterPlayerUnitEvent(Trig, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
            set i = i+1
            exitwhen i == bj_MAX_PLAYER_SLOTS
        endloop
        call TriggerAddCondition(Trig, Condition(function LightningBlastBall.Cond))
        call TriggerAddAction(Trig, function LightningBlastBall.Actions)

        endmethod
endstruct
Attached Images
File type: jpgLightningBallBlastNova.jpg (649.3 KB)
Attached Files
File type: w3xLightningBlastBall.w3x (67.8 KB)
08-24-2008, 02:48 AM#2
Pyrogasm
  • Why the nonstandard format? I say you should change it to being a scope like most sane people. While having it all contained in the struct works, it just makes more sense if you have a scope. Additionally, by not having this be a scope, it makes it harder for a user to change the name of the spell. (Can't just change the scope name, etc.)
  • Some of your constant globals should be changed into constant functions. For example: DAMAGE_INCREASE, SPEED, NOVA_RADIUS, DAMAGE_RADIUS. And then a few of your other constants could be changed though it wouldn't probably be needed.
  • I would say to make NOVA_DUMMY_AMOUNT into a constant function too, but the way you're using it makes that impossible, so....
  • The timer interval should be configurable.
  • In theory, all of this could be run off of one timer for all instances instead of one timer for each instance. This would continue to not use any attaching and also stop you from having do stupid stuff like this: local LightningBlastBall l = LightningBlastBall.LBB[LightningBlastBall.H2I(t)-0x100000]
  • Just work in radians directly instead of working in degrees and then converting.
  • You know, some BJs are good. TriggerRegisterAnyUnitEventBJ is one of them.
  • If you expanded your custom .create method, your Actions method could be cleaned up and made a bit more concise.
  • Using dynamic groups is, in theory, not needed anymore. Instead of local group unitsInRange = CreateGroup() you could just use a global group.
  • local real scale = (l.currscale/LightningBlastBall.NOVA_DUMMY_AMOUNT)*8 ?
There's probably more, but that's what I can see right now.
08-24-2008, 04:30 PM#3
Gwypaas
> Why the nonstandard format? I say you should change it to being a scope like most sane people. While having it all contained in the struct works, it just makes more sense if you have a scope. Additionally, by not having this be a scope, it makes it harder for a user to change the name of the spell. (Can't just change the scope name, etc.)

Because I wanted to try how this way worked out when I scripted it. (And I actully liked it.)

> Some of your constant globals should be changed into constant functions. For example: DAMAGE_INCREASE, SPEED, NOVA_RADIUS, DAMAGE_RADIUS. And then a few of your other constants could be changed though it wouldn't probably be needed.

Why would I change them into constant methods? What's the benefit of doing that?

> The timer interval should be configurable.

It was but I lost the map so I had to dl the old version so I forgot to add that. Changed it.

> Just work in radians directly instead of working in degrees and then converting.

I do work with radians? The only place I use bj_RADTODEG is when I create them and just makes them face to the target.


> You know, some BJs are good. TriggerRegisterAnyUnitEventBJ is one of them.

That was the only BJ in the spell so I felt like making it only using natives :P


> If you expanded your custom .create method, your Actions method could be cleaned up and made a bit more concise.

But then I would have to transfer all locations and caster data.. It's easier to do it in a method that can use the event responses.

The *8 part..

I'll make that a constant variable.. I used that to make the nova look good :P



Uploaded the updated version and code.
08-24-2008, 09:31 PM#4
Here-b-Trollz
The benefit of functions is that they take arguments (such as level).
08-24-2008, 09:48 PM#5
Vexorian
Thank you , thank you. I wanted to see people do this years ago. structs are cleaner and less hacky in what scoping is than scopes, though it reminds you of Java's ultra verbosity sometimes. It is just a style choice...

Is it possible to do private real currscale = .START_SCALING instead of private real currscale = LightningBlastBall.START_SCALING I think it would be better if you had to type only the main struct's name once, it can be hard when you have the data struct as well, well JESP allows a text replace to work anyway. This doesn't really a lot but it might be an indications a struct with static members can't replace a scope in convenience yet.
08-24-2008, 10:30 PM#6
Gwypaas
Yeah that's possible, but you need to use the prefix when you use them as functions.

One way to solve it would be creating the whole spell as a text macro but it wouldn't look clean.
08-24-2008, 10:31 PM#7
Vexorian
You need a screenshot attached to the thread.
08-24-2008, 10:36 PM#8
Gwypaas
Added.
08-31-2008, 09:58 PM#9
Vexorian
Sometimes bumps make it harder for us to see your threads.

Ok, might end up reviewing it.
08-31-2008, 10:18 PM#10
Vexorian
Ok, preliminary review, checked what the spell actually does: The idea is not bad, however, and this is very important, your candy is lacking right now, it makes no sense that the spell has no sound whatsoever, when you see a giantic lightning ball being created, then impacting the ground and creating 10 other small balls, you expect sound, yet when I played your spell it was very quiet. Edit: Oh, and the death animations look very unnatural.
09-01-2008, 02:28 PM#11
Gwypaas
Ok, I'll add a sound.

How do you add sounds without the use of the sound editor? I tried to do that before but failed.

What do you mean with the "death animation"?
09-01-2008, 03:51 PM#12
Vexorian
You can create sounds in Jass using 'labels' check out SimError.

But another way is to use a model that actually has sound in its animations.
10-02-2008, 10:49 PM#13
Anitarf
Can we get an update on this?
10-09-2008, 12:48 PM#14
Gwypaas
Oh sorry, I've forgotten about this, I'll update today or tomorrow :)


Edit - Updated
10-11-2008, 08:50 PM#15
Anitarf
There's still no sound, what did you even change?