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

Dark Spells: Malice Orb and Torment v1.01

  • Like
Reactions: baassee and CeDiL
Requires Jass NewGen Pack with the most recent Jasshelper.
Spells should be MUI and leakless.

Malice Orb
Channels a dark orb at a target point which absorbs the surrounding negative energy until it explodes, dealing damage in a target area based on how long the spell has been channeled for.
Channels to a max of 6 seconds. Every second will increase the AoE by 5.
Level 1 - Initially deals 100 damage, increase damage by 15 every second.
Level 2 - Initially deals 125 damage, increase damage by 25 every second.
Level 3 - Initially deals 150 damage, increase damage by 35 every second.

Required Libraries:
- TimerUtils
- GroupUtils
- xe system (xebasic and xefx)

Problems/Issues:
- If you cast another Malice Orb while channeling one, the first one will not be interrupted; it will only stop when it reaches the ChannelDuration.
- Sometimes, the texttag will display a number different from what would be calculated. This could be due to life regeneration.
- Note that I set the Follow Through Time to 7 instead of 6 to make the animation flow better. However, this will only affect the unit.

JASS:
//====================================
//Malice Orb v1.01 by watermelon_1234
//====================================
//Required Libraries: TimerUtils, GroupUtils, and xe system (xebasic & xefx)
//====================================
//Copy Object Editor Data:
//-Malice Orb ability
//====================================

scope MaliceOrb  

    native UnitAlive takes unit id returns boolean //Remove this line if it's already implemented
    
//*********************************
//Settings
//*********************************
    globals
        private constant integer    SPELL_ID = 'A000' //The raw id of the Malice Orb ability
        private constant string     SPELL_ORDER = "darkritual" //The order string of the Malice Orb ability. Must be the Base Order Id, not Order String!
        private constant real       TIMER_LOOP = 0.1  //How many times the timer will loop. High values aren't recommended for this spell
        private constant string     ORB_SFX = "Abilities\\Spells\\Undead\\AntiMagicShell\\AntiMagicShell.mdl" //The SFX for the orb
        private constant real       ORB_HEIGHT = 200. //What height the orb should be at upon creation
        private constant string     BURST_SFX = "Objects\\Spawnmodels\\Undead\\UDeathSmall\\UDeathSmall.mdl" //The explosion SFX when the spell stops
        private constant real       BURST_HEIGHT = 150. //Height of the burst sfx
        private constant string     GROW_SFX = "Abilities\\Spells\\Undead\\Possession\\PossessionMissile.mdl" //A little SFX shown periodically to show the orb is growing
        private constant real       GROW_HEIGHT = 150. //Height of the grow sfx
        private constant real       GROW_SFX_INT = 1.5 //The interval when the GROWSFX will be played
        private constant attacktype ATK_TYPE = ATTACK_TYPE_NORMAL //Attack type of the damage
        private constant damagetype DMG_TYPE = DAMAGE_TYPE_UNIVERSAL //Damage type of the damage    
        private constant weapontype WPN_TYPE = null //Weapon type of the damage
        private constant boolean    DO_PRELOADING = true //Determine whether or not to preload the ghost unit and the special effects.
    endglobals
    
    //The Channel duration of the spell. Make sure it matches the duration put in the spell.
    private function ChannelDuration takes integer lvl returns real        
        return 6.
    endfunction

    //The initial size of the orb.
    private function InitScale takes integer lvl returns real        
        return 0.5+0.5*lvl
    endfunction
    
    //How much the orb should increase in size per second.
    private function ScaleIncrement takes integer lvl returns real        
        return .1
    endfunction   
    
    private function InitArea takes integer lvl returns real
        //The initial area of the spell
        return 100.+50*lvl
    endfunction  
    
    private function AreaIncrement takes integer lvl returns real 
        //How much the area will increase per second.
        return 5.
    endfunction 
               
    private function InitDamage takes integer lvl returns real
        //The amount of damage done without any consideration of DamageIncrement
        return 75. + 25*lvl
    endfunction
    
    private function DamageIncrement takes integer lvl returns real
        //How much the damage should increase by every second
        return 5. + 10*lvl
    endfunction    
    
    //Settings for the texttag that will display the total amount of damage dealt
    private function MakeTextTag takes unit cast,real dmg returns nothing        
        set bj_lastCreatedTextTag = CreateTextTag() //Used a global variable since I'm lazy. :P
        call SetTextTagPosUnit(bj_lastCreatedTextTag,cast,-15)            
        call SetTextTagText(bj_lastCreatedTextTag,"-"+I2S(R2I(dmg))+"!",0.0312)
        call SetTextTagColor(bj_lastCreatedTextTag,0,255,0,255)  
        call SetTextTagVelocity(bj_lastCreatedTextTag,0,0.04)
        call SetTextTagFadepoint(bj_lastCreatedTextTag,0.65)            
        call SetTextTagLifespan(bj_lastCreatedTextTag,1)
        call SetTextTagPermanent(bj_lastCreatedTextTag,false)
    endfunction
//***************************************************************************************************
//Actual coding of the spell below here
//*************************************************************************************************** 
    globals
        private boolexpr e
    endglobals
    
    private struct Data
        unit cast  
        xefx orb
        integer lvl
        real count = 0  //This variable counts how long the spell has been channeled
        real growCount = 0 //A separate variable is needed to know when to play the GROWSFX
        real dmg 
        real totalDmg = 0
        timer t
        private static thistype temp 
        
        static method create takes unit c, real x, real y returns thistype
            local thistype this = thistype.allocate()
            set .cast = c            
            set .lvl = GetUnitAbilityLevel(.cast,SPELL_ID)
            set .orb = xefx.create(x,y,0)
            set .orb.z = ORB_HEIGHT
            set .orb.fxpath = ORB_SFX            
            set .orb.scale = InitScale(.lvl)
            set .t = NewTimer()
            call SetTimerData(.t,this) 
            call TimerStart(.t,TIMER_LOOP,true,function thistype.onLoop)      
            return this
        endmethod 
        
        //Chooses the targets that will be affected and deals damage. Also adds the damage done
        static method filter takes nothing returns boolean
            local unit u = GetFilterUnit()
            local real life = GetWidgetLife(u)
            if UnitAlive(u) and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) and IsUnitEnemy(u,GetOwningPlayer(temp.cast)) then 
                call UnitDamageTarget(temp.cast,u,temp.dmg,false,true,ATK_TYPE,DMG_TYPE,WPN_TYPE)
                set temp.totalDmg = temp.totalDmg + life - GetWidgetLife(u)  
            endif
            set u = null
            return false
        endmethod
        
        static method onLoop takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            local xefx sfx  //Plays an sfx where the orb is. Has the same height as the orb.
            if GetUnitCurrentOrder(.cast) == OrderId(SPELL_ORDER) and .count <= ChannelDuration(.lvl) then            
                set .count = .count + TIMER_LOOP 
                set .growCount = .growCount + TIMER_LOOP 
                
                set .orb.scale = InitScale(.lvl) +ScaleIncrement(.lvl)*.count   
                
                if .growCount >= GROW_SFX_INT then //If the growCount is at least the GROW_SFX_INT, create a nice effect for the orb
                    set sfx = xefx.create(.orb.x,.orb.y,0)
                    set sfx.fxpath = GROW_SFX
                    set sfx.z = GROW_HEIGHT
                    set sfx.scale = InitScale(.lvl) +ScaleIncrement(.lvl)*.count  
                    call sfx.destroy()
                    set .growCount = 0 //set it back to 0 to restart counting
                endif                  
            else
            
                if .count > ChannelDuration(.lvl) then //This is just to prevent the spell from damaging higher than it should
                    set .count = ChannelDuration(.lvl) 
                endif  
                set sfx = xefx.create(.orb.x,.orb.y,0)
                set sfx.fxpath = BURST_SFX
                set sfx.z = BURST_HEIGHT
                set sfx.scale = InitScale(.lvl) +ScaleIncrement(.lvl)*.count 
                call sfx.destroy()                
                set .dmg = InitDamage(.lvl)+DamageIncrement(.lvl)*.count                
                set temp = this
                call GroupEnumUnitsInArea(ENUM_GROUP,.orb.x,.orb.y,InitArea(.lvl) + AreaIncrement(.lvl)*.count,e)
                if .totalDmg > 0 then //This deals with the creation of a texttag that displays the total damage done if greater than 0.
                    call MakeTextTag(.cast,.totalDmg)                
                endif
                call ReleaseTimer(.t)
                call .orb.destroy() //Destroy the orb effect.
                call .destroy()                
            endif
        endmethod
        
        static method spellActions takes nothing returns boolean
            if GetSpellAbilityId() == SPELL_ID then
                call thistype.create(GetTriggerUnit(),GetSpellTargetX(),GetSpellTargetY())
            endif
            return false
        endmethod
        
        static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT)
            call TriggerAddCondition(t,Condition(function thistype.spellActions))
            static if DO_PRELOADING then
                call Preload(ORB_SFX)
                call Preload(BURST_SFX)
                call Preload(GROW_SFX)
            endif
            set e = Filter(function thistype.filter)
        endmethod
    endstruct

endscope

Torment
Summons a ghost to haunt a target ground enemy unit, dealing damage every second dependent on the distance between the ghost and the target.
Level 1 - Deals 35 max damage per second within 100 range. Ghost has a movement speed of 180. Lasts 5 seconds.
Level 2 - Deals 45 max damage per second within 150 range. Ghost has a movement speed of 195. Lasts 6 seconds.
Level 3 - Deals 55 max damage per second within 200 range. Ghost has a movement speed of 210. Lasts 7 seconds.

Required Libraries:
- TimerUtils

Problems/Issues:
- The ghost's speed is limited by the max movement speed. However, it is advised for the ghost to have a lower speed than most units to balance this spell.
- The ghost will be created instantly if the target already has the buff Torment.

JASS:
//====================================
//Torment v1.01 by watermelon_1234
//====================================
//Required Libraries: TimerUtils
//====================================
//Copy Object Editor Data:
//*Torment ability
//*Torment buff
//*Torment unit
//====================================
scope Torment 

    native UnitAlive takes unit id returns boolean //Remove this line if it's already implemented
    
//*********************************
//Settings
//*********************************    
    globals
        private constant integer    SPELL_ID            = 'A001' //The raw ID for the Torment spell
        private constant integer    BUFF_ID             = 'B000' //The raw ID for the Torment buff
        private constant integer    GHOST_ID            = 'h000'  //The raw ID of the Torment (Dummy) unit that will follow the target      
        private constant string     BIRTH_SFX           = "Abilities\\Spells\\Undead\\AnimateDead\\AnimateDeadTarget.mdl" //Plays an SFX where the ghost will spawn
        private constant string     DAMAGE_SFX          = "Abilities\\Spells\\Undead\\DeathandDecay\\DeathandDecayDamage.mdl" //The effect that will play when the spell damages the target.
        private constant string     DAMAGE_SFX_ATTACH   = "head" //The attachment point for the DAMAGESFX
        private constant string     KILL_SFX            = "Objects\\Spawnmodels\\Undead\\UndeadDissipate\\UndeadDissipate.mdl" //The effect that plays when damage dealt from this spell causes the target to die.
        private constant real       BIRTH_OFFSET        = 75. //How far away the BIRTH_SFX should be played from the caster
        private constant real       TIMER_LOOP          = 0.25 //This refers to how many times the timer will run the function.
        private constant attacktype ATK_TYPE            = ATTACK_TYPE_NORMAL //Attack type of the damage
        private constant damagetype DMG_TYPE            = DAMAGE_TYPE_UNIVERSAL //Damage type of the damage
        private constant weapontype WPN_TYPE            = null //Weapon type of the damage
        private constant boolean    DO_PRELOADING       = true //Determine whether or not to preload the ghost unit and the special effects.
    endglobals
    
    //This function sets up how the ghost will look like and more importantly, its movement speed.
    private function GhostSetUp takes unit ghost, integer lvl returns nothing        
        call SetUnitVertexColor(ghost,255,255,255,100)
        call SetUnitMoveSpeed(ghost,165.+15*lvl)
    endfunction     
   
    //Max damage dealt to the target per second when the ghost is within MaxDamageDistance
    private constant function Damage takes integer lvl returns real         
        return 25.+10*lvl
    endfunction   
    
    //Duration of the buff.
    private constant function Duration takes integer lvl returns real        
        return 4.+1*lvl
    endfunction
    
    //The distance between the ghost and target unit when full damage will be dealt
    private constant function MaxDamageDistance takes integer lvl returns real        
        return 75.+25*lvl
    endfunction
   
    //The formula to calculate the damage with distance in mind
    private constant function DamageDistanceFormula takes integer lvl, real dist returns real     
        if dist <= MaxDamageDistance(lvl) then 
            return Damage(lvl) 
        endif
        return Damage(lvl)*MaxDamageDistance(lvl)/dist
    endfunction    
//***************************************************************************************************
//Actual coding of the spell below here. Don't touch unless you know what you're doing!
//***************************************************************************************************
    private struct Data         
        unit        caster
        unit        target
        unit        ghost = null //This unit will be created inside the struct. Null at first to show that it's not created yet.
        integer     level        
        boolean     buffed = false //The boolean will be used to detect when the target will receive the buff
        real        count = 0 //count will be used to prevent stacking               
        //The following two variables will be used to check if the target has changed it location
        real        targetx
        real        targety
        timer t //Timer used for looping...
        
        static method create takes unit c, unit targ returns thistype
            local thistype this = thistype.allocate()
            local real cx = GetUnitX(c)
            local real cy = GetUnitY(c) 
            local real angle = Atan2(GetUnitY(targ)- cy, GetUnitX(targ)- cx) 
            set .caster = c
            set .target = targ
            set .level = GetUnitAbilityLevel(c,SPELL_ID) 
            set .t = NewTimer()
            call SetTimerData(.t, this)
            call TimerStart(.t,TIMER_LOOP,true,function thistype.onLoop)        
            call DestroyEffect(AddSpecialEffect(BIRTH_SFX,cx + BIRTH_OFFSET * Cos(angle),cy + BIRTH_OFFSET * Sin(angle)))
            return this
        endmethod
        
        static method onLoop takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            local real x = GetUnitX(.target)
            local real y = GetUnitY(.target)
            local real dist //Only made a variable for this because it made the UnitDamageTarget line too long
            if GetUnitAbilityLevel(.target,BUFF_ID) > 0 and not .buffed then
                set .buffed = true
            endif
            if GetUnitAbilityLevel(.target,BUFF_ID) > 0 and .buffed then
                if .ghost == null then //If it hasn't been created yet, create it.
                    set .ghost = CreateUnit(Player(15),GHOST_ID,x,y,0)
                    call GhostSetUp(.ghost,.level)
                endif
                //Checks if the target's x or y coordinate has changed. If so, move to its new coordinates.
                if .targetx != x or .targety != y then 
                    call IssuePointOrder(.ghost,"move",x,y)                
                    set .targetx = x
                    set .targety = y
                endif
                call DestroyEffect(AddSpecialEffectTarget(DAMAGE_SFX,.target,DAMAGE_SFX_ATTACH))
                set dist = SquareRoot((GetUnitX(.ghost)-x)*(GetUnitX(.ghost)-x)+(GetUnitY(.ghost)-y)*(GetUnitY(.ghost)-y))
                call UnitDamageTarget(.caster,.target,DamageDistanceFormula(.level,dist)*TIMER_LOOP,false,true,ATK_TYPE,DMG_TYPE,WPN_TYPE)
                //Play an SFX if it succeeded in killing it
                if not UnitAlive(.target) then
                    call DestroyEffect(AddSpecialEffect(KILL_SFX,x,y))
                else
                    set .count = .count + TIMER_LOOP //We need to increase .count if the target is still alive. 
                endif                
            endif         
            if .buffed and (GetUnitAbilityLevel(.target, BUFF_ID) < 1 or .count >= Duration(.level)) then
                call KillUnit(.ghost)
                call ReleaseTimer(.t) 
                call .destroy()                       
            endif            
        endmethod
        
        static method spellActions takes nothing returns boolean
            if GetSpellAbilityId() == SPELL_ID then
                call thistype.create(GetTriggerUnit(),GetSpellTargetUnit())
            endif
            return false //Never need to run any trigger actions
        endmethod
        
        static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT)
            call TriggerAddCondition( t, Condition( function thistype.spellActions)) //Trigger conditions are faster than trigger actions   
            //Preloading to remove lag
            static if DO_PRELOADING then
                call RemoveUnit(CreateUnit(Player(15),GHOST_ID,0,0,0))
                call Preload(BIRTH_SFX)
                call Preload(KILL_SFX)
                call Preload(DAMAGE_SFX)
            endif
        endmethod
    endstruct
endscope

v1.00
Released.

v1.00a
Text Fix: Capitalized all constant global variables.
Minor Fix: Replaced bj_RADTODEG with the actual numbers.

v1.00b
Minor Fix: Removed the unnecessary onDestroy method in Torment.
Minor Revamp: Initialized three variables of the structs that were used in Malice Orb and Torment: counter, growcount, and hasbuff. As far as I can tell, this shouldn't really change anything.

v1.00c
No change to Torment.
Updated GroupUtils. Malice Orb no longer requires GroupEnumUnitsInArea library.

v1.00d
Nulled handle variables in both spells before the struct would be destroyed.

v1.00e
Fixed an error in Torment's code with AddSpecialEffect. Didn't change Malice Orb.

v1.01
Recoded both spells to mainly have the spell code in the struct. Renamed global constants to have "_" as spaces. Added some new constants. Hoped to clarify some of the comments.
Malice Orb now requires xefx.


Credits would be nice but not required.
Credits to:
~ Vexorian for developing vJass, xebasic, and the dummy model
~ RisingDusk for GroupUtils
~ moyack for his "How to develop spells with effects over time" tutorial
~ TriggerHappy for telling me that FirstOfGroup loops are slow

Please give any kind of feedback on how I could improve the spell.

Keywords:
ghost,torment,orb,malice,explosion
Contents

Malice Orb and Torment v1.00d (Map)

Reviews
12:43, 20th Dec 2009 TriggerHappy: Review for Dark Spells Please name all your constants in capital letters, it's a standard naming convention. Otherwise, the coding was fine. Status Feel free to message me here if you have...

Moderator

M

Moderator

12:43, 20th Dec 2009
TriggerHappy:

Review for Dark Spells

Please name all your constants in capital letters, it's a standard
naming convention. Otherwise, the coding was fine.

Status

Feel free to message me here if you have any issues with
my review or if you have updated your resource and want it reviewed again.

Approved
 
Level 9
Joined
Apr 4, 2004
Messages
519
Doesn't seem like Torment functions anymore. I get an error when I try to save my map with the Torment trigger Enabled.

Line 846: Too many arguments given to function : AddSpecialEffect

Any way you could possibly fix this?
 
Level 22
Joined
Nov 14, 2008
Messages
3,256
I guess this:

- If you cast another Malice Orb while channeling one, the first one will not be interrupted; it will only stop when it reaches the ChannelDuration.

can be fixed by adding a enough long cooldown'

anyways I will use them

one question, dunno if it really matters but does it make any difference setting wpn type to null instead of wpn type whoknows. I know the weapon type is only used for the sound so I guess it wont matter but asking it anyways
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
I guess this:

- If you cast another Malice Orb while channeling one, the first one will not be interrupted; it will only stop when it reaches the ChannelDuration.

can be fixed by adding a enough long cooldown'
Yes, it can be fixed with having a cooldown longer than ChannelDuration. I just thought I should mention that bug since that's not the way how blizzard spells normally work. I could fix it if you want to have a shorter cooldown than the channel duration.

one question, dunno if it really matters but does it make any difference setting wpn type to null instead of wpn type whoknows. I know the weapon type is only used for the sound so I guess it wont matter but asking it anyways
I don't know where I read it, but I saw someone say that WEAPON_TYPE_WHOKNOWS is pretty much the same as null so I just decided to go with null instead.

Glad you liked the spell. =D
 
Level 22
Joined
Nov 14, 2008
Messages
3,256
Yes, it can be fixed with having a cooldown longer than ChannelDuration. I just thought I should mention that bug since that's not the way how blizzard spells normally work. I could fix it if you want to have a shorter cooldown than the channel duration.


I don't know where I read it, but I saw someone say that WEAPON_TYPE_WHOKNOWS is pretty much the same as null so I just decided to go with null instead.

Glad you liked the spell. =D

nah it's alright :)

oh I got it, you're welcome :D
 
Top