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

Haunt - v1.6.0

**** Haunt **** -- v1.6.0 - MUI, MPI + all that jazz.
** Inspired by the infamous WoW ability: http://www.wowhead.com/?spell=48181#. But with several modifications.

Tooltip:
Sends ghostly souls to travel around the caster which hit any target that comes within range for 15 damage. It will then return to heal the caster for the total damage dealt. Lasts 5 seconds or until all the souls are gone.

The ghostly souls will purposely spawn for 1.05 seconds and then being to attack souls. This was done so that you can actually see the souls (the black revolving effects) before the souls are actually taken. The spell loses a lot of eye-candy and fun without it, but it is modifiable.

REQUIRED:
http://www.wc3c.net/showthread.php?t=90999 - JassNewGen Pack
http://www.wc3c.net/showthread.php?t=88142 - JassHelper 0.A.2.B

SYSTEMS USED:
http://www.thehelper.net/forums/showthread.php?t=132538 -- Timer32 by Jesus4Lyf

Also uses the dummy model by whoever made it.

I could switch to use TimerUtils, but it is annoying to "properly" download JassHelper for the static ifs to compile. Besides, I like T32 better anyway. =P

Screenshots:
haunt.jpg

hauntsoul.jpg


The screenshots don't do a great amount of justice though.

Anyway, without further ado, the code:
JASS:
scope Haunt initializer Init
//REQUIRES JASS NEWGEN PACK AND JASSHELPER
//http://www.wc3c.net/showthread.php?t=90999         *JassNewGen Pack Download
//http://www.wc3c.net/showthread.php?t=88142         *JassHelper
//************************************H***********************************HAUNT
//*************************************A**********************************HAUNT
//**************************************U*********************************HAUNT
//***************************************N********************************HAUNT
//****************************************T*******************************HAUNT
//*    SPELL - HAUNT v1.6.0
//*                      By Purgeandfire - Give credits if you can.
//*    Description: Sends Sends ghostly souls to travel around the caster which hit any target that comes within
//*                 range for 10*AbilityLevl+5 damage. It will then return to heal the caster for the total damage dealt.
//* ****Systems Used: Timer32 -- Jesus4Lyf
//* ****Models Used: Dummy.mdx

//***** Documentation *****
//*    Required Object Editor Data: "Haunt" Spell, "Dummy" unit
//*    Implementation: Copy the Haunt spell, the dummy unit, T32 trigger and the haunt trigger.
//*    Required Alterations:
//*             - RAWCODE: Change this to the RAWCODE of your haunt spell.
//*             - DUMCODE: Change this to the RAWCODE of your dummy unit.
//*    Cautions:
//*             - RADIUS: Don't set this too high. It *might* cause a crash if it is too large.
//*             - SOULRAD: This will look ugly if set too high. It has an emergency execute which will
//*                        execute the timer drain functions if the soul takes too long to get to the unit.
//*                        This was made to prevent possible infinite timer loops. (Which would cause extreme lag and crash)
//*             - DSPAWNS: This can make your circle look a lot more circular. However, more units results in
//*                        more attached effects, so it can cause a bit more frame rate drop for the duration of the spell.
//*             - waitBeforeEnum: When the spell is cast, black souls spawn and revolve around the caster. This enumeration wait
//*                               was made so that you can actually see the black souls spawn before they go and steal life from a unit.
//*                               You can shorten/lengthen this.
//*             - angINCFACTOR: This modifies how fast the circles revolve around the caster. Note that this
//*                             takes RADIANS not degrees. This can go extremely fast if you set it to anything
//*                             higher than 1. About 0.08 or so is a moderate pace. 0.3 is a fast pace, but makes
//*                             a good circular effect.
//*             - TRANSPARENCY: Modifies the transparency of the ghost that returns to the caster. This is from
//*                             0-255, not 0-100. 0 is completely transparent, and 255 is visible.
//*             - SOULVELOCITY: This modifies the movement speed of the souls. The only thing to beware of is that
//*                             a HIGHER input results in a lower speed and vice versa.
//*             - GHOSTVELOCITY: Modifies the movement speed of the ghosts that return. Same thing as above, beware
//*                              that a HIGHER input results in a lower speed and vice versa. Note that setting this
//*                              too high can result the ghost actually being slower than the ranger. There is now an
//*                              emergency destroy and an automatic accelerator to have the ghost eventually reach the target.
//*                              Don't set this too high or else you'll hardly see it, or it will have to rely each time on the
//*                              emergency destroy to prevent the spell from crashing the game. Be sure that if this is increased,
//*                              that you set the exittimer_distance global mentioned next.
//*             - EXITTIMER_DISTANCE: This modifies the distance the soul/ghost needs to be before it is destroyed.
//*                              Be careful though, this is distance*distance, not just normal distance. So 35*35 = 1225
//*                              which will mean that they will be destroyed once they get within a distance of 35.
//*             - emergencyExits: These are new which allow you to modify when the soul/ghost timers will perform an emergency exit.
//*                              This is for those rare occasions where the spell might bug out. This ensures that the timer will
//*                              be paused eventually, preventing the chances of infinite loops which would most likely lag spike or 
//*                              eventually crash the game.
//***** End Documentation *****
//* Default Values:
//* globals
//*     private constant integer RAWCODE = 'A000'
//*     private constant integer DUMCODE = 'h003'
//*     private constant string DUMMYSFX = "Abilities\\Weapons\\AvengerMissile\\AvengerMissile.mdl"
//*     private constant string ONHITSFX = "Abilities\\Weapons\\DemonHunterMissile\\DemonHunterMissile.mdl"
//*     private constant string DRAINSFX = "Abilities\\Weapons\\IllidanMissile\\IllidanMissile.mdl"
//*     private constant real RADIUS = 300
//*     private constant real SOULRAD = 175
//*     private constant integer DSPAWNS = 13
//*     private constant real waitBeforeEnum = 1.05
//*     private constant real angINCFACTOR = 0.3
//*     private constant integer SOULRED = 0
//*     private constant integer SOULGREEN = 255
//*     private constant integer SOULBLUE = 225
//*     private constant integer TRANSPARENCY = 100
//*     private constant real SOULVELOCITY = 10
//*     private constant real GHOSTVELOCITY = 12
//*     private group HauntG = CreateGroup()
//*     private timer t = CreateTimer()
//* endglobals


globals
    private constant integer RAWCODE = 'A000' 
//RAWCODE of the Haunt spell.
    private constant integer DUMCODE = 'h003'
//RAWCODE of the dummy unit.
    private constant real DURATION = 5.005
//Modifies the duration before the effects automatically destroy themselves. (Seconds)
//For most accuracy, be sure that 0.035 is a factor of the value. (It works fine if it isn't though)
    private constant string DUMMYSFX = "Abilities\\Weapons\\AvengerMissile\\AvengerMissile.mdl"
//String path of the effects that revolve around the caster.
    private constant string ONHITSFX = "Abilities\\Weapons\\DemonHunterMissile\\DemonHunterMissile.mdl" 
//String path of the effects that occur when a target unit is hit.
    private constant string DRAINSFX = "Abilities\\Weapons\\IllidanMissile\\IllidanMissile.mdl"
//String path of the effect that occurs when the soul returns to the caster.
    private constant real RADIUS = 300
//Modifies the radius of the circle that the effects follow.
    private constant real SOULRAD = 175
//Modifies the radius in which the soul will jump.
    private constant integer DSPAWNS = 13
//Number of souls to create. More units equals more circular motion. However, more units also equals more frame drop.
    private constant real waitBeforeEnum = 1.05
//Modifies the time to wait before the souls will start snatching nearby units. (Seconds)
    private constant real angINCFACTOR = 0.3
//This is the angle increase factor every 0.035 seconds. It takes radians, not degrees. This can go pretty fast if too high.
    private constant integer SOULRED = 0
    private constant integer SOULGREEN = 255
    private constant integer SOULBLUE = 225
    private constant integer TRANSPARENCY = 100
//Modifies the vertex coloring of the soul that returns to the caster. Values are from 0-255 for all.
    private constant real SOULVELOCITY = 10
//Modifies the velocity of the souls that rush toward the enemies.
//This isn't very noticeable though, unless the enemy is moving fast.
//LOWER VELOCITY SPECIFIED RESULTS IN A HIGHER SPEED, higher velocity results in a slower speed.
    private constant real GHOSTVELOCITY = 12
//Modifies the speed of the ghosts that return to the caster.
//LOWER VELOCITY SPECIFIED RESULTS IN A HIGHER SPEED, higher velocity results in a slower speed.
    private constant real EXITTIMER_DISTANCE = 1225
//Modifies the distance the soul/ghost has to be to the target before stopping the timer.
//This distance is actually 35. Use your input distance^2 (your input to the power of 2) and input that value for it to work.
    private constant integer SOULHEIGHT = 75
//Modifies the elevation of the black souls.
    private constant integer GHOSTHEIGHT = 15
//Modifies the elevation of the ghosts.
    private constant attacktype ATTACKTYPE = ATTACK_TYPE_NORMAL
    private constant damagetype DAMAGETYPE = DAMAGE_TYPE_ACID
    private constant weapontype WEAPONTYPE = WEAPON_TYPE_WHOKNOWS
//Modifies the attacktype, damagetype, and weapontype of the damaging souls. Why did I use acid? No clue.
    private constant real emergencyExitSoul = 1.25
//Modifies the duration (seconds) before the souls will be forced to reach they're destination. (To prevent infinite loops)
    private constant real emergencyExitGhost = 4
//Modifies the duration (seconds) before the ghosts will be forced to reach they're destination. (To prevent infinite loops)

//Do not change these below.
    private group HauntG = CreateGroup()
    private timer t = CreateTimer()
endglobals

private constant function HAUNTDAMAGE takes integer LVL returns real
    return 10.*LVL+5
//Modify this to modify the damage dealt by the spell per soul to unit.
//10 * Ability_Level + 5
//Rank 1: 15 damage, Rank 2: 25 damage, Rank 3: 35 damage.
endfunction
native UnitAlive takes unit u returns boolean
private function HSCond takes nothing returns boolean
    return UnitAlive(GetFilterUnit()) and (not IsUnitType(GetFilterUnit(),UNIT_TYPE_STRUCTURE)) and (not IsUnitType(GetFilterUnit(),UNIT_TYPE_ETHEREAL))
endfunction


//*SPELL CODE*
private struct HauntSReturn 
    unit a 
    unit b
    unit c
    real damage
    real mD
    effect e
    real speedMultiplier
    
    method onDestroy takes nothing returns nothing
        set .a = null
        set .b = null
        set .c = null
        set .damage = 0
        set .mD = 0
        set .speedMultiplier = 0
    endmethod
    
    private method periodic takes nothing returns nothing
        local real x = GetUnitX(.c)
        local real y = GetUnitY(.c)
        local real dx = GetUnitX(.a)
        local real dy = GetUnitY(.a)
        local real distX = dx-x
        local real distY = dy-y
        local real ang = Atan2(distY,distX)
        local real dist = distX*distX+distY*distY
        local real newX = x+(.mD/GHOSTVELOCITY)*Cos(ang)*.speedMultiplier
        local real newY = y+(.mD/GHOSTVELOCITY)*Sin(ang)*.speedMultiplier
        call SetUnitX(.c,newX)
        call SetUnitY(.c,newY)
        if dist<EXITTIMER_DISTANCE or .speedMultiplier >= ((emergencyExitGhost/0.03125)*.1) then
            call SetUnitX(.c,dx)
            call SetUnitY(.c,dy)
            call DestroyEffect(AddSpecialEffect(ONHITSFX,dx,dy))
            call DestroyEffect(.e)
            call RemoveUnit(.c)
            call SetWidgetLife(.a,GetWidgetLife(.a)+.damage)
            call this.stopPeriodic()
            call this.destroy()
        endif
        set .speedMultiplier = .speedMultiplier + .1
    endmethod
    implement T32x
    
    static method create takes unit caster, unit target, real damageTar returns HauntSReturn
        local thistype this = thistype.allocate()
        local real x = GetUnitX(target)-GetUnitX(caster)
        local real y = GetUnitY(target)-GetUnitY(caster)
        local real f = (bj_RADTODEG*Atan2(y,x))+180
        set .a = caster
        set .b = target
        call SetUnitPathing(target,false)
        set .c = CreateUnit(Player(15),GetUnitTypeId(.b),GetUnitX(.b),GetUnitY(.b),f)
        set .damage = damageTar
        set .mD = SquareRoot(x*x+y*y)
        set .e = AddSpecialEffectTarget(DRAINSFX,.c,"origin")
        set .speedMultiplier = 1
        call SetUnitColor(.c,GetPlayerColor(GetOwningPlayer(.b)))
        call UnitAddAbility(.c,'Amrf')
        call SetUnitPathing(.c,false)
        call UnitAddAbility(.c,'Aloc')
        call UnitAddAbility(.c,'Avul')
        call SetUnitFlyHeight(.c,GHOSTHEIGHT,0)
        call PauseUnit(.c,true)
        call SetUnitVertexColor(.c,SOULRED,SOULGREEN,SOULBLUE,TRANSPARENCY)
        call SetUnitPathing(target,true)
        return this
    endmethod
endstruct
private struct HauntSGmove
    unit a
    unit b
    effect c
    unit d
    real mD
    integer SCount
    
    method onDestroy takes nothing returns nothing
        set .a = null
        set .b = null
        set .c = null
        set .d = null
        set .mD = 0
    endmethod
    
    private method periodic takes nothing returns nothing
        local real x = GetUnitX(.a)
        local real y = GetUnitY(.a)
        local real tx = GetUnitX(.b)
        local real ty = GetUnitY(.b)
        local real distX = tx-x
        local real distY = ty-y
        local real ang = Atan2(distY,distX)
        local real dist = distX*distX+distY*distY
        local real newX = x+(.mD/SOULVELOCITY)*Cos(ang)
        local real newY = y+(.mD/SOULVELOCITY)*Sin(ang)
        call SetUnitX(.a,newX)
        call SetUnitY(.a,newY)
        if dist<EXITTIMER_DISTANCE or .SCount >= (emergencyExitSoul/0.03125)  then
            call SetUnitX(.a,tx)
            call SetUnitY(.a,ty)
            call DestroyEffect(AddSpecialEffect(ONHITSFX,tx,ty))
            call UnitDamageTarget(.a,.b,HAUNTDAMAGE(GetUnitAbilityLevel(.d,RAWCODE)),true,false,ATTACKTYPE,DAMAGETYPE,WEAPONTYPE)
            call DestroyEffect(.c)
            call UnitRemoveAbility(.a,'Aloc')
            call ShowUnit(.a,false)
            call HauntSReturn.create(.d,.b,HAUNTDAMAGE(GetUnitAbilityLevel(.d,RAWCODE))).startPeriodic()
            call this.stopPeriodic()
            call this.destroy()
        endif
        set .SCount = .SCount + 1
    endmethod
    implement T32x
    
    static method create takes unit u, unit tar, effect E, unit caster returns HauntSGmove
        local thistype this = thistype.allocate()
        local real x = GetUnitX(tar)-GetUnitX(u)
        local real y = GetUnitY(tar)-GetUnitY(u)
        set .a = u
        set .b = tar
        set .c = E
        set .d = caster
        set .mD = SquareRoot(x*x+y*y)
        return this
    endmethod
endstruct
private struct HauntS
    unit u
    real x
    real y
    unit array dum[DSPAWNS]
    effect array sfx[DSPAWNS]
    real DURATIONx
    integer ENUMERATE
    boolean array b[DSPAWNS]
    
    method onDestroy takes nothing returns nothing
        local integer i = 0 
        loop
            exitwhen i == (DSPAWNS)
            if (not IsUnitHidden(.dum[i])) then
                call DestroyEffect(AddSpecialEffect(DUMMYSFX,GetUnitX(.dum[i]),GetUnitY(.dum[i])))
            endif
            call DestroyEffect(.sfx[i])
            call UnitRemoveAbility(.dum[i],'Aloc')
            call ShowUnit(.dum[i],false)
            set .sfx[i] = null
            set i = i + 1
        endloop
        set .x = 0
        set .y = 0
        set .DURATIONx = 0
        set .ENUMERATE = 0
    endmethod
    
    private method periodic takes nothing returns nothing
        local integer i = 0
        local real dumX
        local real dumY
        local real x = GetUnitX(.u)
        local real y = GetUnitY(.u)
        local real ang = 0
        local unit FoG
        set .ENUMERATE = .ENUMERATE + 1
        loop
            exitwhen i == (DSPAWNS)
            if .b[i] == true then
                set ang = Atan2(GetUnitY(.dum[i])-y,GetUnitX(.dum[i])-x)+angINCFACTOR
                set dumX = x+RADIUS*Cos(ang)
                set dumY = y+RADIUS*Sin(ang)
                call SetUnitX(.dum[i],dumX)
                call SetUnitY(.dum[i],dumY)
                if .ENUMERATE >= (waitBeforeEnum/0.035) then
                    call GroupEnumUnitsInRange(HauntG,dumX,dumY,SOULRAD,Condition(function HSCond))
                    set FoG = FirstOfGroup(HauntG)
                    if  FoG != null and (not IsUnitAlly(FoG,GetOwningPlayer(.u))) and (not IsUnitHidden(.dum[i])) then
                        call HauntSGmove.create(.dum[i],FoG,.sfx[i],.u).startPeriodic()
                        set .b[i] = false
                    endif
                endif
            endif
            set i = i + 1
        endloop
        set .DURATIONx = .DURATIONx - 0.035 
        if .DURATIONx <= 0 then
            call this.stopPeriodic()
            call this.destroy()
        endif
        set FoG = null
    endmethod
    implement T32x
    
    static method create takes unit a returns HauntS
        local thistype this = thistype.allocate()
        local integer i = 0
        local real angle = 360/DSPAWNS
        local real dumX
        local real dumY
        set .u = a
        set .x = GetUnitX(a)
        set .y = GetUnitY(a)
        set .DURATIONx = DURATION
        set .ENUMERATE = 0
        loop
            exitwhen i >= (DSPAWNS)
            set dumX = .x+RADIUS*Cos((i*angle)*bj_DEGTORAD)
            set dumY = .y+RADIUS*Sin((i*angle)*bj_DEGTORAD)
            set .b[i] =  true
            if (not IsUnitHidden(.dum[i])) or .dum[i] == null then
                set .dum[i] = CreateUnit(Player(15),DUMCODE,dumX,dumY,270)
            else
                call ShowUnit(.dum[i],true)
                call SetUnitX(.dum[i],dumX)
                call SetUnitY(.dum[i],dumY)
            endif
            set .sfx[i] = AddSpecialEffectTarget(DUMMYSFX,.dum[i],"origin")
            call UnitAddAbility(.dum[i],'Aloc')
            call UnitAddAbility(.dum[i],'Amrf')
            call SetUnitFlyHeight(.dum[i],SOULHEIGHT,0)
            set i = i + 1
        endloop
        return this
    endmethod
endstruct

private function Cond takes nothing returns boolean
    if GetSpellAbilityId() == RAWCODE then
        call HauntS.create(GetTriggerUnit()).startPeriodic()
    endif
    return false
endfunction

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

Have fun. Please give feedback/report bugs if you can, especially on the code since I haven't really coded a spell in a long time. A lot has changed, so don't blame me if I have over 100 mistakes/inefficiencies.

Thanks to Watermelon_1234 for improving the code and suggesting more configurables! =D
Also thanks to TriggerHappy for code suggestions.


  • Spell Created
  • Minor code modifications
  • Fixed a hotkey issue where the spell was overwriting the hotkey of hold position
  • Added dummy recycling
  • Fixed an issue where the dummy would remain without locust after first spell cast
  • Added emergency checks to prevent infinite looping
  • Added more configurables and documentation
  • Created two more spells to display the simpleness in modifying the spell
  • Fixed an error where one less dummy was created.

I did not add full dummy recycling though, since the haunt return dummy has a constantly changing rawcode, it would be redundant and odd to recycle then. The rest of the dummies are recycled and function fully.

~Purge =D

Keywords:
Haunt, shadow, World of Warcraft, black, drain, life, death
Contents

Haunt (Map)

Reviews
17:04, 13th Mar 2010 TriggerHappy: Updated your code to fit my last review.

Moderator

M

Moderator

17:04, 13th Mar 2010
TriggerHappy:

Updated your code to fit my last review.
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
Quick Review on Coding:
Don't need the I2R in HAUNTDAMAGE. Just put a decimal point after 10.
(Could also make the function constant but not really necessary.)

JASS:
call SetUnitState(.a,UNIT_STATE_LIFE,GetWidgetLife(.a)+.damage)
to
JASS:
call SetWidgetLife(.a,GetWidgetLife(.a)+.damage)
I'm not sure about this, but I don't think you need to use GroupClear on HauntG.

Shouldn't 1225 in dist<1225 (method is periodic) be configurable?
Attacktype, damagetype, and possibly weapontype should be configurable.

Comments on coding style:
You should name struct members to be more specific about what they are. (Makes it easier for people to review)

This is optional, but you could capitalize all global variables that are constant.

In-game test:
Visual effects were pretty good.

For the dummy units used for special effects, you may want to consider making them the owner of Player(15) as they add to the player's unit count and food. You can change the unit's color with a trigger after unit creation.

Other opinions:
Allow the souls to have a height. (They look too close to the ground)

Good documentation. I felt that you should have more spacing between sentences.

Will edit this post once I test in-game.
 
Thanks for the reply. =D

I'm about to update it, I fixed everything you said (I think) except I didn't rename my struct members since I got lazy. =P

I'll update it in a second.

I would rep you but I need to spread some, I guess I'll just wait a while.

you can also just use 10 without decimal and it would still work...
 
It causes a syntax error since it is returning an integer, not a real. Since it is int*int+int it considers the output as an integer. Making the 10 a real by adding the decimal makes it real*int+int, which results in a real output. =)

it works for me (at least after I updated to the latest jass helper)... hehe... anyway nice spell.
 
Update!

I've added dummy recycling for all but the soul returns. (Reasons listed in the main post)

v1.6.0, I made a lot of changes. I decided that it was too meager to be a simple 0.1 up to the version for all the changes, so I decided to make it 1.6.0 for my hard work. =P

Please note any bugs. I've added two custom spells to it that are all based off haunt, just with different configuration. It is just meant to show the simplicity of modifying the code.
 
Please learn to indent properly.
Also, T32 is biting with the systems around here.

But it's a good spell and well coded.

Thanks. =) But how I am I not indenting properly? (Well, I guess I don't indent things within a scope, but those are just wrappers so I don't really indent because of them =P)

T32 is biting with the systems around here? Eh, I'm not sure what you mean by that. Yeah, I guess it isn't the most popular/used since it is only posted on TH, but it is super-duper fast for high frequencies (and extremely light, it is barely 50 lines [just a linked list], and even less if you remove the debugs [and only include T32x]) so it shouldn't be very detrimental to include. =D

However, thanks for the comment. Also, thanks TriggerHappy for approving it. =)
 
Top