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

[Spell] Touch of Light

This is my Spell Touch of Light requested by Aelita...

Touch of Light
Summon orbs that fly in an area, draining life from enemies.
When their time has come they return and heal the caster and his nearby allies!
The Spell is MUI, leak and lagg free and in vJASS so
Jass NewGen Pack is required to get this spell working...

A "How to Import" is included of course...

Look at the beginning of the code to see which things can be customized...

v.2.0: new screenshot and changings on the effects...
improved the code


JASS:
library TouchOfLight initializer Init
// Spell Touch of Light by The_Witcher
//
// Creates orbs that patrol in a area, draining life from enemies. 
// When their time has come they return and heal the caster and his allies!
//
// THE SETUP PART:

globals
    // the rawcode of the spell
    private constant integer SPELL_ID = 'A000'
    
    // the rawcode of the dummy used as orb
    private constant integer ORB_ID = 'h001'
    
    // the range in which the orbs can drain
    private constant integer ORB_DRAIN_RANGE = 150
    
    // the timer interval (increase if laggy)
    private constant real INTERVAL = 0.02
    
    // the flying height of the orbs
    private constant real ORB_HEIGHT = 80
    
    // the time it takes a orb to drain life
    private constant real DRAIN_TIME = 0.5
    
    // false = only caster is healed
    // true = caster and near allies are healed
    private constant boolean AOE_HEAL = true
    
    // the sfx created on units while healing
    private constant string HEAL_SFX = "Abilities\\Spells\\Other\\HealingSpray\\HealBottleMissile.mdl"  
    
    // the attachement point for HEAL_SFX
    private constant string HEAL_SFX_TARGET = "origin"
    
    // the sfx created on units while draining
    private constant string DRAIN_SFX = "Abilities\\Spells\\Human\\Slow\\SlowCaster.mdl"  
    
    // the sfx created on a orb when it returns to the caster
    private constant string EXPLODE_SFX = "Abilities\\Spells\\Human\\HolyBolt\\HolyBoltSpecialArt.mdl"   
    
    // the lightning created while draining
    private constant string LIGHTNING = "HWPB"
    
    // false = lightning is normal
    // true = lightnings endings are switched
    private constant boolean LIGHTNING_TURNED = true
endglobals

private function GetOrbAmount takes integer lvl returns integer
    return 4 * lvl                     // 4 orbs more each level !!CAN NEVER BE HIGHER THAN 50!!
endfunction

private function GetOrbDrainCooldown takes integer lvl returns real
    return 1.1 - 0.1 * lvl                     // 1sec/0.9sec/0.8sec
endfunction

private function GetOrbAreaSize takes integer lvl returns real
    return 300. + 50 * lvl                     // 350/400/450 radius for the area where the orbs do their work
endfunction

private function GetDrainPercentage takes integer lvl returns real
    return 0.08 * lvl                      //draines 8% more each level
endfunction

private function GetDuration takes integer lvl returns real
    return 5. * lvl                      //lasts for 4/8/12/... seconds
endfunction

private function GetHealPercentage takes integer lvl returns real
    return 0.1 * lvl                      //heals 10% more each level
endfunction

private function GetHealRange takes integer lvl returns real
    return 300. + 100 * lvl                      //heals in a AOE of 300/400/500/...
endfunction

private function GetOrbSpeed takes integer lvl returns real
    return 5. + 1. * lvl                      //  6/7/8 per interval...
endfunction

//---------------------------------------------------------
//-----------Don't modify anything below this line---------
//---------------------------------------------------------

private struct data
    unit               u
    real               x
    real               y
    integer            lvl
    unit      array    orb       [50]
    unit      array    target    [50]
    integer   array    phase     [50]
    real      array    drained   [50]
    real      array    delay     [50]
    lightning array    light     [50]
    timer              tim       = CreateTimer()
    integer            orbs      = 0
    real               temp      = 19
    real               t         = 0
endstruct

globals
    private hashtable h = InitHashtable()
    private data DATA
    private group g = CreateGroup()
    private location loc = Location(0,0)
endglobals

private function AngleBetweenCoords takes real x, real y, real xx, real yy returns real
    return Atan2(yy - y, xx - x)
endfunction

private function DistanceBetweenCoords takes real x , real y , real xx , real yy returns real
    return SquareRoot((x-xx)*(x-xx)+(y-yy)*(y-yy))
endfunction

private function DistanceBetweenUnits takes unit a, unit b returns real
    local real dx = GetUnitX(b) - GetUnitX(a)
    local real dy = GetUnitY(b) - GetUnitY(a)
    return SquareRoot(dx * dx + dy * dy)
endfunction

private function AngleBetweenUnits takes unit a, unit b returns real
    return Atan2(GetUnitY(b) - GetUnitY(a), GetUnitX(b) - GetUnitX(a))
endfunction

private function EnemiesOnly takes nothing returns boolean
    return IsUnitEnemy(GetFilterUnit(),GetOwningPlayer(DATA.u)) and not (IsUnitType(GetFilterUnit(),UNIT_TYPE_DEAD) or GetUnitTypeId(GetFilterUnit()) == 0 )
endfunction

private function AlliesOnly takes nothing returns boolean
    return IsUnitAlly(GetFilterUnit(),GetOwningPlayer(DATA.u)) and not (IsUnitType(GetFilterUnit(),UNIT_TYPE_DEAD) or GetUnitTypeId(GetFilterUnit()) == 0 )
endfunction

private function AOEheal takes nothing returns nothing
    call SetWidgetLife(GetEnumUnit(),GetWidgetLife(GetEnumUnit()) + DATA.temp * GetHealPercentage(DATA.lvl))
    call DestroyEffect(AddSpecialEffectTarget(HEAL_SFX,GetEnumUnit(),HEAL_SFX_TARGET))
endfunction

function RandomEnemyUnit takes real x, real y, real range returns unit
    local unit a=null
    local unit i
    local integer b=0
    call GroupClear(g)
    call GroupEnumUnitsInRange(g,x,y,range,Condition(function EnemiesOnly))
    loop
        set i=FirstOfGroup(g)
        exitwhen i==null
        set b=b+1
        if (GetRandomInt(1,b) == 1) then
            set a = i
        endif
        call GroupRemoveUnit(g,i)
    endloop
    set bj_groupRandomCurrentPick=a
    set a=null
    return bj_groupRandomCurrentPick
endfunction

private function Execute takes nothing returns nothing
    local data dat = LoadInteger(h,GetHandleId(GetExpiredTimer()),0)
    local real X 
    local real Y
    local real a
    local integer i = 0
    //****check whether caster is alive****
    if (IsUnitType(dat.u,UNIT_TYPE_DEAD) or GetUnitTypeId(dat.u) == 0 ) then
        loop
            exitwhen i >= dat.orbs
            call RemoveUnit(dat.orb[i])
            call DestroyLightning(dat.light[i])
            set i = i + 1
        endloop
        set dat.orbs = 0
    else
        //****increase time****
        set dat.t = dat.t + INTERVAL
        //****new orbs****
        if dat.orbs < IMinBJ(GetOrbAmount(dat.lvl),50) and dat.t <= GetDuration(dat.lvl) then
            set dat.temp = dat.temp + 1
            if dat.temp == 20 then 
                set dat.orb[dat.orbs] = CreateUnit(GetOwningPlayer(dat.u),ORB_ID,GetUnitX(dat.u),GetUnitY(dat.u),0)
                call UnitAddAbility(dat.orb[dat.orbs], 'Aloc')
                call UnitAddAbility(dat.orb[dat.orbs], 'Amrf')
                call SetUnitFlyHeight(dat.orb[dat.orbs],ORB_HEIGHT,0)
                set dat.temp = 0
                set DATA = dat
                set dat.target[dat.orbs] = RandomEnemyUnit(dat.x,dat.y,GetOrbAreaSize(dat.lvl))       
                set dat.orbs = dat.orbs + 1
            endif
        endif              
        //****process all active orbs****
        loop
            exitwhen i >= dat.orbs
            if dat.phase[i] == 0 then    
            //****search a target and drain****
                //****reduce cooldown****
                if dat.delay[i] > 0 then
                    set dat.delay[i] = dat.delay[i] - INTERVAL
                    if dat.delay[i] < 0 then
                        set dat.delay[i] = 0
                    endif
                    if dat.delay[i] <= GetOrbDrainCooldown(dat.lvl) then
                        if IsUnitPaused(dat.orb[i]) then
                            call DestroyLightning(dat.light[i])
                            call PauseUnit(dat.orb[i],false)
                            //****new target****
                            set DATA = dat
                            set dat.target[i] = RandomEnemyUnit(dat.x,dat.y,GetOrbAreaSize(dat.lvl))    
                        endif
                    else
                        call MoveLocation(loc,GetUnitX(dat.orb[i]),GetUnitY(dat.orb[i]))
                        set a = GetLocationZ(loc)
                        call MoveLocation(loc,GetUnitX(dat.target[i]),GetUnitY(dat.target[i]))
                        if LIGHTNING_TURNED then
                            call MoveLightningEx(dat.light[i],true,GetUnitX(dat.target[i]),GetUnitY(dat.target[i]),GetLocationZ(loc)+50,GetUnitX(dat.orb[i]),GetUnitY(dat.orb[i]),a+ORB_HEIGHT)
                        else
                            call MoveLightningEx(dat.light[i],true,GetUnitX(dat.orb[i]),GetUnitY(dat.orb[i]),a+ORB_HEIGHT,GetUnitX(dat.target[i]),GetUnitY(dat.target[i]),GetLocationZ(loc)+50)
                        endif
                    endif
                endif
                //****if time is up and the orb isn't draining, start returning to the caster****
                if dat.delay[i] <= GetOrbDrainCooldown(dat.lvl) and dat.t > GetDuration(dat.lvl) then
                    set dat.phase[i] = 1
                elseif dat.target[i] != null then
                //****distance to target check*****
                    if DistanceBetweenUnits(dat.target[i],dat.orb[i]) < ORB_DRAIN_RANGE then
                        //****no cooldown left?****         
                        if dat.delay[i] == 0 then
                            //****drain****
                            set X = GetUnitX(dat.target[i])
                            set Y = GetUnitY(dat.target[i])
                            set a = GetWidgetLife(dat.target[i]) * GetDrainPercentage(dat.lvl)
                            set dat.drained[i] = dat.drained[i] + a
                            call SetWidgetLife(dat.target[i], GetWidgetLife(dat.target[i]) - a)
                            //****effects****
                            call DestroyEffect(AddSpecialEffect(DRAIN_SFX,X,Y)) 
                            if LIGHTNING_TURNED then
                                set dat.light[i] = AddLightningEx(LIGHTNING,true,X,Y,GetUnitFlyHeight(dat.target[i])+50,GetUnitX(dat.orb[i]),GetUnitY(dat.orb[i]),ORB_HEIGHT)
                            else
                                set dat.light[i] = AddLightningEx(LIGHTNING,true,GetUnitX(dat.orb[i]),GetUnitY(dat.orb[i]),ORB_HEIGHT,X,Y,GetUnitFlyHeight(dat.target[i])+50) 
                            endif
                            //****cooldown****
                            call PauseUnit(dat.orb[i],true)
                            set dat.delay[i] = GetOrbDrainCooldown(dat.lvl) + DRAIN_TIME
                        endif 
                    //****distance too huge so go nearer****
                    elseif not IsUnitPaused(dat.orb[i]) then
                        set a = AngleBetweenUnits(dat.orb[i],dat.target[i])
                        set X = GetUnitX(dat.orb[i]) + GetOrbSpeed(dat.lvl) * Cos(a)
                        set Y = GetUnitY(dat.orb[i]) + GetOrbSpeed(dat.lvl) * Sin(a) 
                        call SetUnitX(dat.orb[i],X)
                        call SetUnitY(dat.orb[i],Y) 
                    endif 
                else
                    //****new target****
                    set DATA = dat
                    set dat.target[i] = RandomEnemyUnit(dat.x,dat.y,GetOrbAreaSize(dat.lvl))     
                endif
            else 
            //****Time is up so return to caster****
                set a = AngleBetweenCoords(GetUnitX(dat.orb[i]),GetUnitY(dat.orb[i]),GetUnitX(dat.u),GetUnitY(dat.u))
                set X = GetUnitX(dat.orb[i]) + GetOrbSpeed(dat.lvl) * Cos(a)
                set Y = GetUnitY(dat.orb[i]) + GetOrbSpeed(dat.lvl) * Sin(a) 
                call SetUnitX(dat.orb[i],X)
                call SetUnitY(dat.orb[i],Y)
                //****caster reached so heal****
                if DistanceBetweenCoords(X,Y,GetUnitX(dat.u),GetUnitY(dat.u)) < 10 then
                    call RemoveUnit(dat.orb[i])
                    call DestroyEffect(AddSpecialEffect(EXPLODE_SFX,X,Y))
                    if AOE_HEAL then
                        set DATA = dat
                        set dat.temp = dat.drained[i]
                        call GroupEnumUnitsInRange(g,X,Y,GetHealRange(dat.lvl),Condition(function AlliesOnly))
                        call ForGroup(g, function AOEheal)
                    else
                        call SetWidgetLife(dat.u,GetWidgetLife(dat.u) + dat.drained[i] * GetHealPercentage(dat.lvl))
                        call DestroyEffect(AddSpecialEffectTarget(HEAL_SFX,dat.u,HEAL_SFX_TARGET))
                    endif
                    set dat.orbs = dat.orbs - 1
                    set dat.orb[i] = dat.orb[dat.orbs]
                    set i = i - 1
                endif  
            endif
            set i = i + 1 
        endloop
    endif
    //****no orbs left ==> end spell****
    if dat.orbs == 0 then
        call FlushChildHashtable(h,GetHandleId(dat.tim))
        call DestroyTimer(dat.tim)
        call dat.destroy()
    endif
endfunction

private function Cast takes nothing returns boolean
    local data dat
    local integer i = 0
    if GetSpellAbilityId() == SPELL_ID then
        set dat = data.create()
        loop
            exitwhen i == 50                                                                    
            set dat.phase[i] = 0
            set dat.drained[i] = 0
            set dat.delay[i] = 0
            set i = i + 1
        endloop
        set dat.x = GetSpellTargetX()
        set dat.y = GetSpellTargetY()
        set dat.u = GetTriggerUnit()
        set dat.lvl = GetUnitAbilityLevel(dat.u,SPELL_ID)
        call SaveInteger(h,GetHandleId(dat.tim),0,dat)
        call TimerStart(dat.tim,INTERVAL,true,function Execute)
    endif
    return false
endfunction

private function Init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerAddCondition(t,Condition(function Cast))
    call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT)
endfunction

endlibrary

Keywords:
light, beam, holy, heal, AOE, MUI, vjass, jass, camera, orb, drain, life
Contents

Touch of Light (Map)

Reviews
10:36, 3rd Feb 2010 TriggerHappy: Are all these BJ functions you created necessary? FirstOfGroup loops aren't efficient either. Also, you can reduce your usage of hashtables by using systems that do the attaching for you.

Moderator

M

Moderator

10:36, 3rd Feb 2010
TriggerHappy:

Are all these BJ functions you created necessary?
FirstOfGroup loops aren't efficient either.

Also, you can reduce your usage of hashtables by using systems that do the attaching for you.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
Use fairy dragon missile for orbs and use chain heal lightning instead of drain lightning to make it look really holy.

That looks far better imo.

EDIT:

Using AutoIndex or SetTimerData(TimerUtils) should allow you to get rid of that relativelly useless hashtable usage?
 
Use fairy dragon missile for orbs and use chain heal lightning instead of drain lightning to make it look really holy.

That looks far better imo.

EDIT:

Using AutoIndex or SetTimerData(TimerUtils) should allow you to get rid of that relativelly useless hashtable usage?

okay i use your effect proposals now and you're right! it looks much more holy :D

why should i include and require a whole system when i can do it with 2 lines of code with the same eficiency?
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
Well first of all people won't copy AutoIndex/TimerUtils per spell, you only need 1 for a varying number of things.

Also it was only a comment, i never said you have to do it.

offtopic:

Mortar-'s details:

#2 Top 20 Spells

Your's details:

Rank 2 in TOP 20 Spells^^

Seems a bit impossible, doesn't it?
 
Well first of all people won't copy AutoIndex/TimerUtils per spell, you only need 1 for a varying number of things.

Also it was only a comment, i never said you have to do it.
well my answer shouldn't offend or attack you :D sorry if it did!

offtopic:

Mortar-'s details:

#2 Top 20 Spells

Your's details:

Rank 2 in TOP 20 Spells^^

Seems a bit impossible, doesn't it?

WOO you're right :D didn't check it for a long time!!
But it seems that many people own my equipment system now :D:D
I will change it... DONE :D:D:D
 
Level 5
Joined
Dec 8, 2008
Messages
102
Good code
but idea seems for me like the pit lord ulti^^ where some units are flying around him draining life from enemies and after time they return healing him for the amount of dealt damage, but your special effects are indeed better than the other ability

anyway +rep for this nice resource
 
Level 6
Joined
Aug 29, 2008
Messages
134
Good code
but idea seems for me like the pit lord ulti^^ where some units are flying around him draining life from enemies and after time they return healing him for the amount of dealt damage, but your special effects are indeed better than the other ability

anyway +rep for this nice resource

That's not the Pit Lord :ugly:
That's the Crypt Lord :grin:
 
Problem.jpg

I don't know how to fix it, but this happened. I will say these things, so you know what I did.
1) I casted only once each full cycle and waited for it to finish before recasting.
2) They started sticking after the 1st cast. not all three from same cast.

Awesome Side Note
*I full on charged the enemy and was able to obliterate them after like 7 casts and no loses on my side, give I had to pull guys back here and there when low hp.
 
Level 6
Joined
Apr 15, 2012
Messages
205
I tried to modify the trigger, if I change anything, and I mean anything in the map, the trigger won't work. I am new here so there is probably something that I have done wrong. Please help me, I really like this spell.
 
Level 14
Joined
Jul 19, 2007
Messages
767
Very cool spell, gj man! 5/5 for the idea. Just one question. How should I set the JASS-trigger if I don't want the orbs to drain life from structues or spell immunity units? I want the orbs to only drain life from organic units. Pls tell me how I can fix that? I'm very noobish at JASS-triggering but at least I was able to make this spell work on my map but as the one above mentioned, if I change anything in the trigger, the trigger won't work so I always have to unable the trigger and open it with NewGen and then enable it again and save the changes again and then it will work again but I cannot save changes with normal World Editor.. :-/
 
Top