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

Wrath v1.02b

  • Like
Reactions: X-OMG-X
Requires Jass NewGen Pack with the most recent Jasshelper.
Spell should be MUI and leakless.

Wrath
Conjures a terrible spirit of wrath and anger to deal periodic damage for 2.5 seconds after being summoned and then major damage to nearby enemy units. Its strength depends on how long the caster spent time summoning it.
The caster must spend 3 seconds summoning the spirit to draw out its full strength.
Level 1 - Deals 20 periodic damage and 175 delayed damage at max strength.
Level 2 - Deals 30 periodic damage and 250 delayed damage at max strength.
Level 3 - Deals 40 periodic damage and 325 delayed damage at max strength.

JASS:
//==========================================================================================
// Wrath v1.02b by watermelon_1234
//******************************************************************************************
// Libraries required: (Libraries with * are optional)
//  - New Table         http://www.hiveworkshop.com/forums/jass-functions-413/snippet-new-table-188084/
//  - TimerUtils        http://www.wc3c.net/showthread.php?t=101322                    
//  - xe system         http://www.wc3c.net/showthread.php?t=101150
//  * BoundSentinel     http://www.wc3c.net/showthread.php?t=102576
//  * GroupUtils        http://www.wc3c.net/showthread.php?t=104464
//##########################################################################################
// Importing:
//  1. Copy the ability, Wrath
//  2. Copy the unit, Wrath (Dummy)
//  3. Implement the required libraries
//  4. Copy this trigger
//  5. Configure the spell
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Notes:
//  - N/A
//==========================================================================================

scope Wrath

//==========================================================================================
//                              CONSTANTS
//==========================================================================================

    globals

//------------------------------------------------------------------------------------------ 
//      General spell settings
//------------------------------------------------------------------------------------------ 

        private constant integer    SPELL_ID            = 'A000'    // Raw id of the Wish ability
        private constant integer    DUMMY_ID            = 'h000'    // Raw id of the Wrath dummy unit        
        private constant real       TIMER_LOOP          = 0.03      // How often the timer loops, affects the smoothness of the spell
        
//------------------------------------------------------------------------------------------ 
//      Wrath-specific settings
//------------------------------------------------------------------------------------------

        private constant boolean    RECYCLE_DUMMY        = true      // Determines whether or not to recylce the wrath unit; good idea if the spell is used frequently
        private constant integer    ANIM_SUMMON         = 2         // Animation index the wrath plays while being summoned
        private constant string     ANIM_REPEAT         = "attack"  // Animation the wrath plays every ANIM_REPEAT_INTERVAL until MISSILE_TIME seconds has elapsed        
        private constant real       ANIM_REPEAT_INT     = 0.5       // Determines how often the wrath plays ANIM_REPEAT and when DAMAGE_TIME_SFX will be played
        private constant string     ANIM_FIN            = "spell"   // Animation the wrath plays when the spell deals the delayed damage
        private constant real       WRATH_EXPIRE        = 1.4       // Determines how long the wrath will remain after playing ANIM_FIN

//------------------------------------------------------------------------------------------ 
//      Missile-specfic settings
//------------------------------------------------------------------------------------------
        
        private constant string     MISSILE_SFX         = "Abilities\\Weapons\\ZigguratMissile\\ZigguratMissile.mdl" // SFX path for the missile
        private constant real       MISSILE_ROTATE      = bj_PI/2   // Determines how many radians the missiles will orbit per second
        private constant integer    MAX_MISSILES        = 8         // The maximum number of missiles that would be created

//------------------------------------------------------------------------------------------    
//      Shared Settings for the Wrath and Missiles
//------------------------------------------------------------------------------------------

    // Values with an INIT and FIN are affected by how long the spell was channeled for
    
        // Vertex Colors
        private constant integer    RED                 = 255       // Red value
        private constant integer    GREEN               = 255       // Green value
        private constant integer    BLUE                = 255       // Blue value 
        private constant integer    ALPHA_INIT          = 50        // Initial alpha
        private constant integer    ALPHA_FIN           = 150       // Final alpha
        // Scaling
        private constant real       SCALE_INIT          = 0.65      // Initial scaling
        private constant real       SCALE_FIN           = 1.35      // Final scaling
        // Height
        private constant real       HEIGHT_INIT         = 50.       // Initial height
        private constant real       HEIGHT_FIN          = 200.      // Final height

//------------------------------------------------------------------------------------------        
//      Other SFX settings
//------------------------------------------------------------------------------------------
        
        private constant boolean    PRELOAD             = true      // Determines if the spell should preload the sfxs and Wrath dummy unit
        private constant string     DMG_SFX             = "Abilities\\Spells\\Undead\\Possession\\PossessionMissile.mdl" //The sfx that will be played on units damaged by the Delayed damage of the spell
        private constant string     DMG_ATTACH          = "chest"   // Attachment point for DMG_SFX
        private constant string     DMG_TIME_SFX        = "Abilities\\Spells\\Undead\\DeathandDecay\\DeathandDecayDamage.mdl" //The sfx that will play on units affected by the DoT filter. Note that this sfx will be played as often as MISSILE_TIME_LOOP
        private constant string     DMG_TIME_ATTACH     = "origin"  //Attachment point for DMG_TIME_SFX
        private constant string     POINT_SFX           = "Objects\\Spawnmodels\\Undead\\UndeadDissipate\\UndeadDissipate.mdl" //The sfx that will be played on the wrath unit
        
//------------------------------------------------------------------------------------------        
//      Damage Settings
//------------------------------------------------------------------------------------------

        private constant attacktype ATK_TYPE            = ATTACK_TYPE_NORMAL
        private constant damagetype DMG_TYPE            = DAMAGE_TYPE_UNIVERSAL
        private constant weapontype WPN_TYPE            = null
        
//------------------------------------------------------------------------------------------        
//      Texttag settings
//------------------------------------------------------------------------------------------    
    
        private constant real       FONT_SIZE           = .038      // Font size for the texttag
        private constant real       TEXT_VELOCITY       = 0.04      // Velocity for texttag
        private constant real       TEXT_FADE           = 1.5       // Fading age for texttag
        private constant real       TEXT_LIFE           = 2.        // Lifespan for texttag  
        // Vertex color
        private constant integer    TEXT_R              = 150       // Red value
        private constant integer    TEXT_G              = 0         // Green value
        private constant integer    TEXT_B              = 255       // Blue value
        private constant integer    TEXT_A              = 255       // Alpha value
    
    endglobals    
    
//==========================================================================================
//                              OTHER CONFIGURATION
//==========================================================================================
   
    // The target area of the spell
    private constant function Area takes integer lvl returns real 
        return 225. + 75*lvl
    endfunction 

    // Determines which units are damaged by the spell
    private function AffectedTargets takes unit target, player owner returns boolean
        return not IsUnitType(target, UNIT_TYPE_DEAD) and /*
        */ IsUnitEnemy(target, owner) and /*
        */ not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) and /*
        */ not IsUnitType(target, UNIT_TYPE_STRUCTURE)
    endfunction

    // The casting time for the spell
    private constant function CastTime takes integer lvl returns real 
        return 3.
    endfunction
    
    // The number of missiles that will orbit around the wrath; change MAX_MISSILES accordingly
    private constant function MissileNumber takes integer lvl returns integer
        return 8
    endfunction 
    
    // How long it should take for the missile to reach the wrath
    private constant function MissileTime takes integer lvl returns real 
        return 2.5
    endfunction
    
    // Damage done to units every second for MissileTime seconds
    private constant function PeriodicDamage takes integer lvl returns real 
        return 10. + 10*lvl
    endfunction
    
    // Damage done to enemy units MissileTime seconds  after the wrath has been summoned
    private constant function DelayedDamage takes integer lvl returns real
        return 100. + 75*lvl
    endfunction  
    
//==========================================================================================
//                              END OF CONFIGURATION
//==========================================================================================
    
    globals
        private Table info          // Passes info for spell channeling purposes
    endglobals

    private struct Data 
        unit cast                   // Caster of the spell
        unit wrath                  // Wrath dummy unit
        integer lvl                 // The level of the ability of the caster when it was casted
        integer missileNum          // Number of missiles created    
        real x                      // The x of the spell target point
        real y                      // The y of the spell target point    
        real area                   // spell target area
        real missileTime            // How long the missiles will take to ascend
        real percent                // Stores ratio of how long the wrath was summoned to CastTime
        real count = 0              // Count duration for the spell
        real animCount = 0          // A more specific count that determines when the wrath should play ANIM_REPEAT
        real ang = 0                // Angle for the missiles to rotate around the wrath
        timer t                     // Main timer used by the spell to carry out actions
        xefx array fx[MAX_MISSILES] // Stores missiles created
        
        static thistype temp        // Passes information from a struct to the filter method
        static real Total           // Stores total amount of damage done by the delayed damage for the texttag

        // Cleans up the final parts of the spell
        static method timedDestroy takes nothing returns nothing 
            local thistype this = GetTimerData(GetExpiredTimer())
            static if RECYCLE_DUMMY then
                call ShowUnit(.wrath, false)
            else
                call RemoveUnit(.wrath)
            endif
            call ReleaseTimer(.t)        
            call .destroy()      
        endmethod
        
        // Damages units by the delayed portion of the spell
        static method dealDelayedDmg takes nothing returns boolean
            local unit u = GetFilterUnit()
            local real life 
            if AffectedTargets(u, GetOwningPlayer(temp.cast)) then
                set life = GetWidgetLife(u)
                call DestroyEffect(AddSpecialEffectTarget(DMG_SFX, u, DMG_ATTACH))
                call UnitDamageTarget(temp.cast, u, DelayedDamage(temp.lvl)*temp.percent, false, true, ATK_TYPE, DMG_TYPE, WPN_TYPE)
                set Total = Total + life - GetWidgetLife(u)  
            endif
            set u = null
            return false
        endmethod
        
        // Damages units in the periodic portion of the spell
        static method dealPeriodicDamage takes nothing returns boolean 
            local unit u = GetFilterUnit()
            if AffectedTargets(u, GetOwningPlayer(temp.cast)) then 
                // Play sfx about the same time the wrath plays its animation
                if temp.animCount == 0 then
                    call DestroyEffect(AddSpecialEffectTarget(DMG_TIME_SFX, u, DMG_TIME_ATTACH))
                endif
                call UnitDamageTarget(temp.cast, u, PeriodicDamage(temp.lvl)*temp.percent*TIMER_LOOP, false, true, ATK_TYPE, DMG_TYPE, WPN_TYPE)
            endif
            set u = null
            return false
        endmethod
        
        // Takes care of most of the spell's bulk:
        //  - playing the wrath's animation
        //  - setting the missiles
        //  - dealing damage over time
        //  - dealing delayed damage
        static method onEffectLoop takes nothing returns nothing 
            local thistype this = GetTimerData(GetExpiredTimer())
            local integer i = 0 
            local real offset
            local texttag t
            
            set temp = this // For group enumeration
            
            if .count + TIMER_LOOP < .missileTime then
                
                set .count = .count + TIMER_LOOP
                static if LIBRARY_GroupUtils then
                    call GroupEnumUnitsInArea(ENUM_GROUP, .x, .y, .area, Condition(function thistype.dealPeriodicDamage))
                else
                    call GroupEnumUnitsInRange(bj_lastCreatedGroup, .x, .y, .area, Condition(function thistype.dealPeriodicDamage))
                endif                
                
                // Done this way so wrath will play its animation immediately after being summoned
                if .animCount == 0 then 
                    call SetUnitAnimation(.wrath,ANIM_REPEAT)
                endif                
                set .animCount = .animCount + TIMER_LOOP
                if .animCount >= ANIM_REPEAT_INT then 
                    set .animCount = 0
                endif
                
                // Missile movement
                set .ang = .ang + MISSILE_ROTATE * TIMER_LOOP
                set offset = .area - .area / .missileTime * .count
                loop        
                    set .fx[i].x = .x + offset * Cos(.ang + bj_PI * 2 / .missileNum * i)
                    set .fx[i].y = .y + offset * Sin(.ang + bj_PI * 2 / .missileNum * i)
                    set .fx[i].z = (HEIGHT_INIT + (HEIGHT_FIN - HEIGHT_INIT) * .percent) * .count / .missileTime 
                    set i = i + 1
                    exitwhen i == MissileNumber(.lvl)
                endloop
                
            else
                
                call DestroyEffect(AddSpecialEffect(POINT_SFX, .x, .y))

                set Total = 0     
                static if LIBRARY_GroupUtils then
                    call GroupEnumUnitsInArea(ENUM_GROUP, .x, .y, .area, Condition(function thistype.dealDelayedDmg))
                else
                    call GroupEnumUnitsInRange(bj_lastCreatedGroup, .x, .y, .area, Condition(function thistype.dealDelayedDmg))
                endif
                
                // Create a texttag if damage was dealt
                if Total > 0 then                     
                    set t = CreateTextTag()
                    call SetTextTagPos(t, .x, .y, HEIGHT_INIT + (HEIGHT_FIN - HEIGHT_INIT) * .percent)            
                    call SetTextTagText(t, "-"+I2S(R2I(Total))+"!", FONT_SIZE)
                    call SetTextTagColor(t, TEXT_R, TEXT_G, TEXT_B, TEXT_A)  
                    call SetTextTagVelocity(t, 0, TEXT_VELOCITY)
                    call SetTextTagFadepoint(t, TEXT_FADE)        
                    call SetTextTagLifespan(t, TEXT_LIFE)
                    call SetTextTagPermanent(t, false)
                    set t = null
                endif
                
                // Clean up the missiles
                loop
                    call .fx[i].destroy()
                    set i = i + 1
                    exitwhen i == .missileNum
                endloop
                
                // Allow wrath to play ANIM_FIN before getting removed
                call SetUnitAnimation(.wrath, ANIM_FIN)
                call TimerStart(.t, WRATH_EXPIRE, false, function thistype.timedDestroy)
                
            endif            
        endmethod
        
        // Takes effect when the caster stops channeling the spell for any reason and then starts the effects
        static method spellStop takes nothing returns boolean 
            local thistype this
            local integer i = 0
            local integer alpha
            local real mx
            local real my
            local real scale
            
            if GetSpellAbilityId() == SPELL_ID then
            
                set this = info[GetHandleId(GetTriggerUnit())]
                
                set .percent = .count / CastTime(.lvl)
                // For precision, since Jass is stupid
                if .percent == 1. then
                    set .percent = 1 
                endif
                
                set alpha = R2I(ALPHA_INIT + (ALPHA_FIN-ALPHA_INIT)*.percent)
                set scale = SCALE_INIT + (SCALE_FIN-SCALE_INIT) * .percent
                
                loop
                    set mx = .x + .area*Cos(bj_PI * 2 / missileNum*i)
                    set my = .y + .area*Sin(bj_PI * 2 / missileNum*i)
                    
                    set .fx[i] = xefx.create(mx, my, 0)
                    set .fx[i].fxpath = MISSILE_SFX
                    call .fx[i].recolor(RED, GREEN, BLUE, alpha)
                    set .fx[i].scale = scale
                    
                    set i = i + 1
                    exitwhen i == .missileNum
                endloop
                
                set .count = 0
                call TimerStart(.t, TIMER_LOOP, true, function thistype.onEffectLoop)
                
                call info.remove(GetHandleId(.cast)) // No longer need to store this info since spell channeling is done
            
            endif
            return false
        endmethod
        
        // Sets the visuals of the wrath as it's being summoned
        static method onChannel takes nothing returns nothing 
            local thistype this = thistype(GetTimerData(GetExpiredTimer()))
            set .count = .count + TIMER_LOOP
            set .percent = .count / CastTime(.lvl)
            call SetUnitScale(.wrath, SCALE_INIT +(SCALE_FIN-SCALE_INIT) * .percent, 0, 0)
            call SetUnitVertexColor(.wrath, RED, GREEN, BLUE, R2I(ALPHA_INIT + (ALPHA_FIN-ALPHA_INIT) * .percent))
            call SetUnitFlyHeight(.wrath, HEIGHT_INIT + (HEIGHT_FIN-HEIGHT_INIT) * .percent, 0)              
        endmethod
        
        static method startSpell takes unit c, real x, real y returns nothing
            local thistype this = thistype.allocate()
            local real ang
            set .cast = c            
            set .x = x
            set .y = y
            set .lvl = GetUnitAbilityLevel(.cast, SPELL_ID)
            
            // Variables that are set as they're used a lot in calculations
            set .area = Area(.lvl)
            set .missileNum = MissileNumber(.lvl)
            set .missileTime = MissileTime(.lvl)
            
            // Ensures that the ghost will have a proper facing angle if the spell is cast directly on the caster
            if y - GetUnitY(c) > 0 and x - GetUnitX(c) > 0 then     
                set ang = bj_RADTODEG*Atan2(y - GetUnitY(c), x - GetUnitX(c))                    
            else
                set ang = GetUnitFacing(c)
            endif
            
            // Wrath settings
            static if RECYCLE_DUMMY then
                if .wrath == null then
                    set .wrath = CreateUnit(Player(15), DUMMY_ID, x, y, ang)
                else                           
                    call SetUnitX(.wrath, x)
                    call SetUnitY(.wrath, y)
                    call SetUnitFacing(.wrath, ang) 
                    
                    call ShowUnit(.wrath, true) 
                    call UnitRemoveAbility(.wrath, 'Aloc')
                    call UnitAddAbility(.wrath, 'Aloc')
                endif
            else
                set .wrath = CreateUnit(Player(15), DUMMY_ID, x, y, ang)
            endif
            call SetUnitScale(.wrath, SCALE_INIT, 0, 0)
            call SetUnitVertexColor(.wrath, RED, GREEN, BLUE, ALPHA_INIT)
            call SetUnitFlyHeight(.wrath, HEIGHT_INIT, 0)
            call SetUnitAnimationByIndex(.wrath, ANIM_SUMMON)
            
            set info[GetHandleId(.cast)] = this
            
            set .t = NewTimer()
            call SetTimerData(.t, this)
            call TimerStart(.t, TIMER_LOOP, true, function thistype.onChannel) 
        endmethod 
        
        static method spellActions takes nothing returns boolean
            if GetSpellAbilityId() == SPELL_ID then  
                call thistype.startSpell(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))
            
            set t = CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_ENDCAST)
            call TriggerAddCondition(t, Condition(function thistype.spellStop))
            
            set info = Table.create()
            
            // Preloading
            static if PRELOAD then
                call RemoveUnit(CreateUnit(Player(15),DUMMY_ID,0,0,0))
                call Preload(MISSILE_SFX)
                call Preload(DMG_SFX)
                call Preload(DMG_TIME_SFX)
                call Preload(POINT_SFX)
            endif       
            
            set t = null
        endmethod
    endstruct 
endscope

Changelog:
v1.02b
Provided links to libraries
Changed create method to a static method that returns nothing

v1.02a
Uses bj_lastCreatedGroup instead of declaring own group variable
Uses local variable for texttag
Uses SetUnitFacing instead of SetUnitFacingTimed
Remove GetUnitTypeId(target) != 0 from AffectedTargets
No longer uses XE_ANIMATION_PERIOD; defined own timer periodic constant

v1.02
Uses New Table by Bribe
Made GroupUtils optional
Tweaked general look of code
Fixed areas of code that were leaking
Made most of the missiles' settings correspond to the wrath's
Fixed calculations related to ratios for how long the wrath was summoned for
No longer uses UnitAlive

v1.01a
Made the numbers used in calculations more precise.

v1.01
Removed an unnecessary trigger and changed the way D.percent becomes precise

v1.00a
Minor Text Fixes

v1.00
Released


Please credit me if you use this spell in your map.
Credits to:
~ Bribe for New Table
~ RisingDusk for GroupUtils
~ Vexorian for BoundSentinel, TimerUtils, and xesystem
~ dataangel for UnitCircleUnit function whose math I used to make the missiles orbit

Any kind of feedback on how I could improve the spell is welcomed.

Keywords:
ghost, wrath, spirit, summon, anger, revenge, vengeance
Contents

Wrath (Map)

Reviews
24 Sep 2011 Bribe: The spell looks good, it's well documented and easy to follow along with your work. The coding is pretty decent as well. >Approved

Moderator

M

Moderator

24 Sep 2011
Bribe: The spell looks good, it's well documented and easy to follow along with your work. The coding is pretty decent as well.

>Approved
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Please provide links to required libraries.

You don't need to return 'this' from the create method as you never use its return value, so you could use a different method name that returns nothing.

^ I don't really care about this one.

We also should get some up-to-date benchmarks on whether FirstOfGroup is faster or slower than enumerating and starting new threads.
 
Top