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

Water Spirits v1.01

  • Like
Reactions: D4RK_G4ND4LF
Water Spirits

Unleashes water spirits that rotate around the target ally, and attack nearby enemy units, dealing damage, then return to the target ally, healing for 50% damage that dealt by them.

JASS:
library WaterSpirits requires TimerUtils, GetNearest, SimError, xefx, xedamage optional BoundSentinel
//-----------------------------------------------------------------------------------------------
// Water Spirits v1.01 by scorpion182
// requires:
//          - TimerUtils, SimError, xe by Vexorian
//          - GroupUtils (required by GetNearest) by Rising_Dusk
//          - GetNearest by grim001
//
//
//
//
//-----------------------------------------------------------------------------------------------
//----------------CALIBRATION SECTION------------------------------------------------------------
    globals
        private keyword data //don't touch this
        private constant integer SPELL_ID='A000' //ability rawcode
        private constant integer MAX_MISSILE=50 //maximum missiles
        private constant string ORB_PATH="Abilities\\Weapons\\SpiritOfVengeanceMissile\\SpiritOfVengeanceMissile.mdl" //orb fx path
        private constant string DAMAGE_FX="" //on-damage effect
        private constant string DAMAGE_ATTCH="origin" //on-death effect attachment point
        private constant string HEAL_FX="" //0n-heal effect
        private constant string HEAL_ATTCH="origin" //on-heal effect attachment point
        private constant real SCALE=1.2 //orb scale
        private constant real ANGLE_SPEED=.15 //orb angle speed
        private constant real HEIGHT=100. //orb height
        private constant real MIN_DIST=80. //minimum distance to absorb, make sure the value higher than 80, if not it won't work correctly
        private constant real MAX_DIST=1000. //max distance to attack the target
        private constant string ERROR_MSG="Another instance is running for this unit." //error message
    endglobals
    
    private constant function GetDamage takes integer lvl returns real
        return 15.+lvl*0 //do damage amount of hp each orb 
    endfunction
    
    private constant function GetHealFactor takes integer lvl returns real
        return 0.50+lvl*0 //heals 50% of the damage dealt by it
    endfunction
    
    private constant function GetMissileCount takes integer lvl returns integer
        return 3+lvl*0 //orb count
    endfunction
    
    private constant function GetDuration takes integer lvl returns real
        return 60.+lvl*0 //spell duration
    endfunction
    
    private constant function GetDistance takes integer lvl returns real
        return 150.+lvl*0 //orb's rotation distance
    endfunction
    
    private constant function GetAoE takes integer lvl returns real
        return 500.+lvl*0 //aoe each orb to find a target 
    endfunction
    
    private constant function GetSpeed takes integer lvl returns real
        return 800.*XE_ANIMATION_PERIOD+lvl*0. //orb missile speed 
    endfunction
    
    private function DamageOptions takes xedamage spellDamage returns nothing
        set spellDamage.dtype=DAMAGE_TYPE_UNIVERSAL
        set spellDamage.atype=ATTACK_TYPE_NORMAL
        set spellDamage.exception=UNIT_TYPE_STRUCTURE
        set spellDamage.visibleOnly=true 
        set spellDamage.damageSelf=true
        set spellDamage.damageAllies=true //heals for amount of the damage dealt by orb
        set spellDamage.allyfactor=-1.0   //to the target ally.
    endfunction
    
    //filter the targets, should match the value from DamageOptions
    private function IsValidTarget takes unit u, data s returns boolean
        return not IsUnitType(u, UNIT_TYPE_DEAD) and GetUnitTypeId(u) != 0 and IsUnitType(u,UNIT_TYPE_STRUCTURE)==false and IsUnitEnemy(u,GetOwningPlayer(s.caster))==true and IsUnitVisible(u,GetOwningPlayer(s.caster))==true
    endfunction
//------------------END OF CALIBRATION-----------------------------------------------------------
    globals
        private constant real A=2*bj_PI
        private xedamage xed
        private group casters=CreateGroup()
    endglobals
    
    private struct data
        unit caster //target ally
        unit owner //caster
        unit array target[MAX_MISSILE] //target enemies
        xefx array fx[MAX_MISSILE] //effect
        boolean array IsAttack[MAX_MISSILE] //attacking status
        boolean array IsHome[MAX_MISSILE] //homing status
        boolean array IsFinish[MAX_MISSILE] //finish status
        real array angle[MAX_MISSILE] //current angle
        real array tx[MAX_MISSILE] //current x track
        real array ty[MAX_MISSILE] //current y track
        real duration //spell duration
        boolean array heal[MAX_MISSILE] //attacking succeed status
        timer t
        integer total //orbs total
        integer lvl //ability level
        private static thistype temp
        
        static method create takes unit c, unit o returns data
            local thistype this=data.allocate()
            local integer i=0
            local real x=GetUnitX(c)
            local real y=GetUnitY(c)
            local real a
            
            set .lvl=GetUnitAbilityLevel(c,SPELL_ID)
            set .total=GetMissileCount(.lvl)
            set .duration=GetDuration(.lvl)
            set .caster=c
            set .owner=o
            set .t=NewTimer() 
            
            loop
            exitwhen i==.total
                set a=i*A/.total
                set .fx[i]=xefx.create(x,y,a)
                set .fx[i].fxpath=ORB_PATH
                set .fx[i].scale=SCALE
                set .fx[i].z=HEIGHT
                set .angle[i]=a
                set i=i+1
            endloop
            
            call GroupAddUnit(casters,c)
            
            return this
        endmethod
        
        static method VictimFilter takes nothing returns boolean
            return IsValidTarget(GetFilterUnit(),temp)
        endmethod
        
        static method onLoop takes nothing returns nothing
            local thistype this=data(GetTimerData(GetExpiredTimer()))
            local real x=GetUnitX(.caster)
            local real y=GetUnitY(.caster)
            local integer i=0
            local real x1
            local real y1
            local real x2
            local real y2
            local real ang
            local real newx
            local real newy
            local real dx
            local real dy
            local real dist
            
            if .duration>0. and not IsUnitType(.caster, UNIT_TYPE_DEAD) then
                set .duration=.duration-XE_ANIMATION_PERIOD
                
                loop
                exitwhen i==.total
                    
                    set .angle[i]=.angle[i]+ANGLE_SPEED
                    set tx[i]=x+GetDistance(.lvl)*Cos(.angle[i])
                    set ty[i]=y+GetDistance(.lvl)*Sin(.angle[i])
                   
                    //if not attacking, order to rotate
                    if not .IsAttack[i] then
                    
                        set .fx[i].x=tx[i]
                        set .fx[i].y=ty[i]
                        
                        set temp=this
                        set .target[i]=GetNearestUnit(.fx[i].x,fx[i].y,GetAoE(.lvl),Condition(function data.VictimFilter))
                        
                        
                        if .target[i]!=null then
                            set .IsAttack[i]=true
                        endif

                    else
                        //if find a target, order to attack
                        if not .IsHome[i] then
                        
                            set x1=.fx[i].x
                            set y1=.fx[i].y
                            set x2=GetUnitX(.target[i])
                            set y2=GetUnitY(.target[i])
                            set dx=x2-x1
                            set dy=y2-y1
                            set ang=Atan2(dy,dx)
                            set newx=x1+GetSpeed(.lvl)*Cos(ang)
                            set newy=y1+GetSpeed(.lvl)*Sin(ang)
                            set dist=SquareRoot(dx*dx+dy*dy)
                        
                            if (dist>MIN_DIST) then
                                set .fx[i].x=newx
                                set .fx[i].y=newy
                                set .fx[i].xyangle=ang
                                
                                //if the target is too far away from the caster or dead, order it to come back
                                if dist>MAX_DIST or IsUnitType(.target[i], UNIT_TYPE_DEAD) then
                                    set .IsHome[i]=true
                                    set .target[i]=null
                                endif
                                
                            else
                                //damage the target
                                if not IsUnitType(.target[i], UNIT_TYPE_DEAD) then
                                    call xed.useSpecialEffect(DAMAGE_FX,DAMAGE_ATTCH)
                                    call xed.damageTarget(.owner,.target[i],GetDamage(.lvl))
                                    set .heal[i]=true
                                endif
                                set .IsHome[i]=true
                                set .target[i]=null
                            endif
                        else
                        //back to caster
                            if not .IsFinish[i] then
                                set x1=.fx[i].x
                                set y1=.fx[i].y
                                set x2=GetUnitX(.caster)
                                set y2=GetUnitY(.caster)
                                set dx=x2-x1
                                set dy=y2-y1
                                set ang=Atan2(dy,dx)
                                set newx=x1+GetSpeed(.lvl)*Cos(ang)
                                set newy=y1+GetSpeed(.lvl)*Sin(ang)
                                set dist=SquareRoot(dx*dx+dy*dy)
                            
                                if (dist>MIN_DIST) then
                                    set .fx[i].x=newx
                                    set .fx[i].y=newy
                                    set .fx[i].xyangle=ang
                                else
                                    //heal the caster
                                    if not IsUnitType(.caster, UNIT_TYPE_DEAD) and .heal[i] then
                                        call xed.useSpecialEffect(HEAL_FX,HEAL_ATTCH)
                                        call xed.damageTarget(.owner,.caster,GetDamage(.lvl)*GetHealFactor(.lvl))
                                    endif
                                    set .IsFinish[i]=true
                                endif
                            else
                                //back to its rotation point
                                set x1=.fx[i].x
                                set y1=.fx[i].y
                                set x2=tx[i]
                                set y2=ty[i]
                                set dx=x2-x1
                                set dy=y2-y1
                                set ang=Atan2(dy,dx)
                                set newx=x1+GetSpeed(.lvl)*Cos(ang)
                                set newy=y1+GetSpeed(.lvl)*Sin(ang)
                                set dist=SquareRoot(dx*dx+dy*dy)
                            
                                if (dist>MIN_DIST) then
                                    set .fx[i].x=newx
                                    set .fx[i].y=newy
                                    set .fx[i].xyangle=ang
                                else
                                    set .IsFinish[i]=false
                                    set .IsHome[i]=false
                                    set .IsAttack[i]=false
                                    set .heal[i]=false
                                    set .fx[i].x=tx[i]
                                    set .fx[i].y=ty[i]
                                endif
                            endif
                        endif
                        
                    endif
                    
                    set i=i+1
                
                endloop
            else
                call .destroy()
            endif
        endmethod
        
        private method onDestroy takes nothing returns nothing
            local integer i=0
            
            call ReleaseTimer(.t)
            call GroupRemoveUnit(casters,.caster)
            
            loop
            exitwhen i==.total
                call .fx[i].destroy()
                set i=i+1
            endloop
            
        endmethod
        
        static method SpellEffect takes nothing returns boolean
            local thistype this
            
            if (GetSpellAbilityId()==SPELL_ID) then
                set this=data.create(GetSpellTargetUnit(),GetTriggerUnit())
                call SetTimerData(.t,this)
                call TimerStart(.t,XE_ANIMATION_PERIOD,true,function data.onLoop)
            endif
            return false
        endmethod
        
        static method Stop takes nothing returns boolean
            local unit c=GetSpellTargetUnit()
            local unit u=GetTriggerUnit()
            
            if GetSpellAbilityId()==SPELL_ID and IsUnitInGroup(c,casters) then
                call SimError(GetOwningPlayer(u),ERROR_MSG)
                call PauseUnit(u,true)
                call IssueImmediateOrder(u,"stop")
                call PauseUnit(u,false)
            endif
            
            set c=null
            set u=null
            return false
        endmethod
        
        static method onInit takes nothing returns nothing
            local trigger t=CreateTrigger()
            
            call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_CAST)
            call TriggerAddCondition(t,Condition(function data.SpellEffect))
            
            set t=CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_CHANNEL)
            call TriggerAddCondition(t,Condition(function data.Stop))
            
            //init xedamage
            set xed=xedamage.create()
            call DamageOptions(xed)
        endmethod
    
    endstruct

endlibrary


Requires:
- TimerUtils by Vexorian
- xe by Vexorian
- BoundSentinel (Optional) by Vexorian
- GroupUtils by Rising_Dusk
- GetNearest by grim001
- SimError by Vexorian

History:
~ v1.00 First Release.
~ v1.01 Fixed the codes.

Keywords:
water, shield, spirits, unit, target, rotate, spiral, order, heal, damage, around, bounce
Contents

Water Spirits v1.01 (Map)

Reviews
09:46, 4th Feb 2010 TriggerHappy: Coding looks good, as well as the effect.

Moderator

M

Moderator

09:46, 4th Feb 2010
TriggerHappy:

Coding looks good, as well as the effect.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
You could add a "hit" effect.

Also instead of this:
JASS:
.duration<GetDuration(.lvl)

Couldn't you just use:
JASS:
set .duration = .duration - *interval*
if .duration > 0.00
?

Also i dunno why you use a method which calls upon a private functions:
JASS:
        static method VictimFilter takes nothing returns boolean
            local unit u=GetFilterUnit()
            
            return IsValidTarget(u,temp)
        endmethod

Why not use the function itself and cut out the middle man?

Why are you using a boolexpr variable? Why not use the Condition(function)?

No need for this:
JASS:
call DestroyBoolExpr(b)

Boolean expresions don't leak anymore.(they never did aparently).

Instead of:
IsUnitType(.target[i], UNIT_TYPE_DEAD)

You could use:
GetWidgetLife(.target[i]) > .405


I heard somewhere that you shouldn't use negative damage for healing, use SetWidgetLife() instead, since negative damage could bug some damage detection systems.

EDIT:

In creation and loop methods you could replace "data" with "thistype".
 
Dude. You should really learn to comment more. Indenting and spaces is also beh,...

JASS:
    private struct data
        unit caster
        unit owner
        unit array target[MAX_MISSILE]
        xefx array fx[MAX_MISSILE]
        boolean array IsAttack[MAX_MISSILE]
        boolean array IsHome[MAX_MISSILE]
        boolean array IsFinish[MAX_MISSILE]
        real array angle[MAX_MISSILE]
        real array tx[MAX_MISSILE]
        real array ty[MAX_MISSILE]
        real duration=0
        boolean array heal[MAX_MISSILE]
        timer t
        integer total
        integer lvl

Would'nt
JASS:
    private struct data
        unit        array       target      [MAX_MISSILE]   //: The targets
        xefx        array       fx          [MAX_MISSILE]   //: The effects
        boolean     array       IsAttack    [MAX_MISSILE]   //: Boolean whether they are attacked
        boolean     array       IsHome      [MAX_MISSILE]   //: Boolean whether they are at home
        boolean     array       IsFinish    [MAX_MISSILE]   //: Boolean whether they finished
        real        array       angle       [MAX_MISSILE]   //: An array of angles
        real        array       tx          [MAX_MISSILE]   //: An array of X-Coordinates
        real        array       ty          [MAX_MISSILE]   //: ^ same for y
        boolean     array       heal        [MAX_MISSILE]   //: The heal in an array

        timer                   t                           //: The current object timer
        integer                 total                       //: Total number
        integer                 lvl                         //: The level of the spell
        real                    duration    = 0             //: Value about the duration
        unit                    caster                      //: Caster unit.
        unit                    owner                       //: Owning unit
be better?

It doesn't matter that much actually, but imho, I think its a lot better to read (and understand)
 
Last edited:
Level 16
Joined
Jun 9, 2008
Messages
734
UPDATED v1.01

@Maker
added in-game.

@TriggerHappy
~VictimFilter leaks, you never null the unit reference.
fixed.

~What's the point of destroying a boolexpr? They are stored in a table and recycled afaik.
fixed.

@ Kingz

~You could add a "hit" effect.
added.


~Also instead of this:...
fixed.

~Also i dunno why you use a method which calls upon a private functions:
~Why not use the function itself and cut out the middle man?

err..it's to make the target configureable at the calibration section.

~Why are you using a boolexpr variable? Why not use the Condition(function)?
fixed.

~Instead of:
IsUnitType(.target, UNIT_TYPE_DEAD)
You could use:
GetWidgetLife(.target) > .405


GetWidgetLife(.target) > .405 sometimes work, sometimes don't. Also it can break if you increase the health of a unit after it has died.

~I heard somewhere that you shouldn't use negative damage for healing, use SetWidgetLife() instead, since negative damage could bug some damage detection systems.

it comes from xedamage, i cant do anything about that.

~In creation and loop methods you could replace "data" with "thistype".
fixed.

@ Anachron
don't ask my indenting :p
 
Top