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

XE 0.4 and vJASS spell

Status
Not open for further replies.
Level 13
Joined
Jul 26, 2008
Messages
1,009
I'm using XE Colliders for this because my math for making visual effects is useless. I grabbed the example spell and did some editing with my best understanding of vJASS. Of course the spell has some major issues, as it became a bigger undertaking than I expected any help would be appreciated and hopefully lead me to greater knowledge of vJASS :)

For Earth I used Rising Dusk's knockback, with slight mod, and for Frost I used EarthFury's bonusmod. Wind uses XEcast.

The tooltip:
Fire 5/7/9 arrows randomly imbued with elemental attributes.

Fire - Splash damage.
Earth - Knockback
Wind - Cyclone
Frost - Slow

Damage is determined by current damage bonus and main attribute.

JASS:
scope elementalVolley initializer init

    globals
        private constant real   DISTANCE        = 50. //distance between each two fire balls.
        private unit            TEMP
    endglobals
    
private struct elementalEarth extends xecollider
    
    unit owner

    method onUnitHit takes unit target returns nothing
        local real a = 57.29582 * Atan2(GetUnitY(target) - GetUnitY(this.owner), GetUnitX(target) - GetUnitX(this.owner))
        local real dam = SpellStat(this.owner,true)/2 + GetUnitBonus(this.owner, BONUS_DAMAGE)
        // Don't explode if the owner is "hit", often when creating the missile
        // it would think it hit the unit that created it...
        if (this.owner != target) and IsUnitType(target, UNIT_TYPE_STRUCTURE) == false and IsUnitEnemy(target, GetOwningPlayer(this.owner)) then

            // perform the damage stuff, credit the owner.
            call UnitDamageTarget(this.owner, target, dam , true, false, ATTACK_TYPE_PIERCE, DAMAGE_TYPE_NORMAL, wtype)
            call KnockbackTarget(this.owner, target, a, 250., 400, true, true, false, "MDX//KnockbackDust.mdx")
            call this.terminate()
        endif
    endmethod

endstruct

private struct elementalWind extends xecollider
    
    unit owner

    method onUnitHit takes unit target returns nothing
        local real a = 57.29582 * Atan2(GetUnitY(target) - GetUnitY(this.owner), GetUnitX(target) - GetUnitX(this.owner))
        local real dam = SpellStat(this.owner,true)/2 + GetUnitBonus(this.owner, BONUS_DAMAGE)

        if (this.owner != target) and IsUnitType(target, UNIT_TYPE_STRUCTURE) == false and IsUnitEnemy(target, GetOwningPlayer(this.owner)) then
            call UnitDamageTarget( this.owner, target, dam , true, false, ATTACK_TYPE_PIERCE, DAMAGE_TYPE_NORMAL, wtype)
            call CasterCastAbility(GetOwningPlayer(this.owner), 'A009', "cyclone", target, true)
            call this.terminate()
        endif
    endmethod

endstruct
    
private struct elementalFire extends xecollider

    unit owner

    static method explosion takes nothing returns boolean
     local real dam = SpellStat(TEMP,true)/4 + GetUnitBonus(TEMP, BONUS_DAMAGE)/2
        if IsUnitEnemy(GetFilterUnit(), GetOwningPlayer(TEMP)) and IsUnitType(GetFilterUnit(), UNIT_TYPE_DEAD) == false then
            call UnitDamageTarget( TEMP, GetFilterUnit(), dam , true, false, ATTACK_TYPE_PIERCE, DAMAGE_TYPE_MAGIC, wtype)
            return true
        endif
     return false
    endmethod

    method onUnitHit takes unit target returns nothing
        local real dam = SpellStat(this.owner,true)/2 + GetUnitBonus(this.owner, BONUS_DAMAGE)
        local group g = NewGroup()
        if (this.owner != target) and IsUnitType(target, UNIT_TYPE_STRUCTURE) == false and IsUnitEnemy(target, GetOwningPlayer(this.owner)) then
            call UnitDamageTarget( this.owner, target, dam , true, false, ATTACK_TYPE_PIERCE, DAMAGE_TYPE_NORMAL, wtype)
            set TEMP = this.owner
            call GroupEnumUnitsInRange(g, GetUnitX(this.owner), GetUnitY(this.owner), 300, Filter(function elementalFire.explosion))
            call ReleaseGroup(g)
            call this.terminate()
        endif
    endmethod
    
endstruct

    private struct elementalFrost extends xecollider
    
        unit owner
        unit t
        
        static method onLoop takes nothing returns nothing
         local timer tim = GetExpiredTimer()
         local elementalFrost d = elementalFrost(GetTimerData(tim))
                call AddUnitBonus(d.t, BONUS_ATTACK_SPEED, 25)
                call AddUnitBonus(d.t, BONUS_MOVEMENT_SPEED, 50)
                call d.terminate()
                call ReleaseTimer(tim)
        endmethod
        
        static method freeze takes unit target returns elementalFrost
         local elementalFrost d = elementalFrost.allocate(GetUnitX(target),GetUnitY(target),300)
         local timer tim = NewTimer()
         set d.t = target
            call AddUnitBonus(target, BONUS_ATTACK_SPEED, -25)
            call AddUnitBonus(target, BONUS_MOVEMENT_SPEED, -50)
            call SetTimerData(tim, d)
            call TimerStart(tim, 1.5, false, function elementalFrost.onLoop)
         return d
        endmethod
        
    method onUnitHit takes unit target returns nothing
     local real dam = SpellStat(this.owner,true)/2 + GetUnitBonus(this.owner, BONUS_DAMAGE)
        if (this.owner != target) and IsUnitType(target, UNIT_TYPE_STRUCTURE) == false and IsUnitEnemy(target, GetOwningPlayer(this.owner)) then
            call UnitDamageTarget( this.owner, target, dam , true, false, ATTACK_TYPE_PIERCE, DAMAGE_TYPE_NORMAL, wtype)
            call elementalFrost.freeze(target)
            call this.terminate()
        endif
    endmethod
        
    endstruct

    private function onSpellCast takes nothing returns nothing
     local unit     u   = GetTriggerUnit()
     local location loc = GetSpellTargetLoc()

     local real     x   = GetUnitX(u)
     local real     y   = GetUnitY(u)

     local real     ang = Atan2( GetLocationY(loc) - y, GetLocationX(loc) - x)

     local real     ang2
     local integer  i
     local integer  i2

     local real     mx
     local real     my

     local integer FIRE_BALL_COUNT = 3 + 2 * GetUnitAbilityLevel(u, 'ElVo')

     local elementalEarth xc //notice our variable uses the greenMissile type

        // ang2 would be perpendicular to the other angle, we are using radians so to get
        // ang2 we add PI/2 which is equivalent to 90 degrees.
        set ang2 = ang + bj_PI/2
        set i=0             //
        loop                // we are creating two missiles but that's too much hassle and repetitive
            exitwhen (i==FIRE_BALL_COUNT) // code, just use a loop and one trick for the angle you shall see bellow
            if ( (i==FIRE_BALL_COUNT-1) and ( ModuloInteger(FIRE_BALL_COUNT,2)==1) ) then
                set mx = x // When FIRE_BALL_COUNT is odd, create the last fire ball
                set my = y // right in the middle.
            else
                set mx = x + (DISTANCE*(i/2+1))*Cos(ang2) //Getting the position for the missile using a polar 
                set my = y + (DISTANCE*(i/2+1))*Sin(ang2) //projections. Increase distance by 75 every 2 steps.
            endif
            
            set i2 = GetRandomInt(1,4)
            if i2 == 1 then
                set xc = elementalEarth.create(mx, my, ang)
                call xc.flash( "Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl" )
            elseif i2 == 2 then
                set xc = elementalFrost.create(mx, my, ang)
                call xc.flash( "Abilities\\Weapons\\LichMissile\\LichMissile.mdl" )
            elseif i2 == 3 then
                set xc = elementalFire.create(mx, my, ang)
                call xc.flash( "Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl" )
            elseif i2 == 4 then
                set xc = elementalWind.create(mx, my, ang)
                call xc.flash( "Abilities\\Spells\\Human\\Thunderclap\\ThunderclapTarget.mdl" )
            endif
            set xc.fxpath = "Abilities\\Weapons\\Arrow\\ArrowMissile.mdl"
            
            set xc.speed   = 1000.0 // The missile starts with a speed of 0, acceleration of 1000
            set xc.expirationTime =    1.25 //20000.0 // it expires after 1 seconds.
                                            // So, try visualizing those things

            set xc.z              =   50.0 // some height, since missiles often fly...

            set xc.owner = u // important stuff, see how our collider is using the greenMissile struct
                             // so we can assign the owner, as you saw above the owner determines
                             // what can hit the missile and who to credit the damage for.


            set ang2= bj_PI + ang2 //this is the trick, the second angle will be the opposite to the
                                   // current one.
            set i=i+1
        endloop

        call RemoveLocation(loc) //We need to clean the point.

     set u=null
     set loc=null // good idea to null handle local variables.
    endfunction

    private function spellIdMatch takes nothing returns boolean
      return (GetSpellAbilityId()=='ElVo')
    endfunction


    private function init takes nothing returns nothing
     local trigger t=CreateTrigger()
        call TriggerAddCondition(t, Condition( function spellIdMatch) )
        call TriggerAddAction(t,    function onSpellCast)
        call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_SPELL_EFFECT )

     set t=null
    endfunction

endscope
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
The problem that I see is that you're using four structs but only one variable, xc, to refer to all of them. (I'm talking about onSpellCast) This should give you a syntax error.
The only fix I can think of for that is to use only one struct and have all of the effects in that struct. You probably would need another variable (like an integer) to identify what the effect is supposed to be.

Other code fixes:

  • JASS:
    (this.owner != target)
    is mostly a useless condition since you check if the target is an enemy unit. (Unless the caster switches owner after the spell is cast)
    BTW, it would probably make more sense if you named the variable caster since it's not a player variable.
  • In the structs elementalWind and elementalFrost, it might be better to check if the target is still alive after the damage is done before applying effects on it.
  • In elementalFire, you don't need the group variable g. You could use ENUM_GROUP instead (provided by GroupUtils). In the filter explosion, return true is not needed.
    BTW, you could also use GroupEnumUnitsInArea instead of GroupEnumUnitsInRange since it's more accurate. (Comes with GroupUtils)
  • Use GetSpellTargetX and GetSpellTargetY instead of GetSpellTargetLoc
 
Level 13
Joined
Jul 26, 2008
Messages
1,009
Thanks! I followed your advice and it works now. I just have to figure out how to attach effects to the missiles (Shouldn't be hard, just read more xefx) and how to make it so the arrows fire in a cone \|/ instead of straight out in a line |||.

Here's my updated script:

JASS:
//**********************************************
//* Double gem of green fire!
//* --------------------------
//* A quick sample for xecollider
//*
//*

//----------------------------------------------
scope elementalVolley initializer init

    globals
        private constant real   DISTANCE        = 50. //distance between each two fire balls.
        private unit            TEMP
    endglobals
    
private struct elementalArrow extends xecollider
    
    unit caster
    unit t
    integer i2

    static method explosion takes nothing returns boolean
     local real dam = SpellStat(TEMP,true)/4 + GetUnitBonus(TEMP, BONUS_DAMAGE)/2
        if IsUnitEnemy(GetFilterUnit(), GetOwningPlayer(TEMP)) and IsUnitType(GetFilterUnit(), UNIT_TYPE_DEAD) == false then
            call DestroyEffect(AddSpecialEffectTarget("Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl", GetFilterUnit(), "origin"))
            call UnitDamageTarget( TEMP, GetFilterUnit(), dam , true, false, ATTACK_TYPE_PIERCE, DAMAGE_TYPE_MAGIC, wtype)
        endif
     return false
    endmethod
        
    static method onLoop takes nothing returns nothing
     local timer tim = GetExpiredTimer()
     local elementalArrow d = elementalArrow(GetTimerData(tim))
            call AddUnitBonus(d.t, BONUS_ATTACK_SPEED, 25)
            call AddUnitBonus(d.t, BONUS_MOVEMENT_SPEED, 50)
            call d.terminate()
            call ReleaseTimer(tim)
    endmethod
        
    static method freeze takes unit target returns elementalArrow
     local elementalArrow d = elementalArrow.allocate(GetUnitX(target),GetUnitY(target),300)
     local timer tim = NewTimer()
     set d.t = target
        call AddUnitBonus(target, BONUS_ATTACK_SPEED, -25)
        call AddUnitBonus(target, BONUS_MOVEMENT_SPEED, -50)
        call SetTimerData(tim, d)
        call TimerStart(tim, 1.5, false, function elementalArrow.onLoop)
     return d
    endmethod

    method onUnitHit takes unit target returns nothing
     local xecast   xc  = xecast.create()
     local real dam = SpellStat(this.caster,true)/2 + GetUnitBonus(this.caster, BONUS_DAMAGE)
     local real a = 57.29582 * Atan2(GetUnitY(target) - GetUnitY(this.caster), GetUnitX(target) - GetUnitX(this.caster))
     set TEMP = this.caster
     
        if IsUnitType(target, UNIT_TYPE_STRUCTURE) == false and IsUnitEnemy(target, GetOwningPlayer(this.caster)) then
            call UnitDamageTarget( this.caster, target, dam, true, false, ATTACK_TYPE_PIERCE, DAMAGE_TYPE_NORMAL, wtype)
            if this.i2 == 1 then
                //Earth
                call KnockbackTarget(this.caster, target, a, 250., 400, true, false, false, "MDX\\KnockbackDust.mdx")
            elseif this.i2 == 2 and not(IsUnitType(target, UNIT_TYPE_DEAD)) then
                //Frost
                call UnitDamageTarget( this.caster, target, 1, true, false, ATTACK_TYPE_PIERCE, DAMAGE_TYPE_COLD, wtype)
                call DestroyEffect(AddSpecialEffectTarget("Abilities\\Weapons\\FrostWyrmMissile\\FrostWyrmMissile.mdl", target, "origin"))
            elseif this.i2 == 3 then
                //Fire
                call DestroyEffect(AddSpecialEffectTarget("Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl", target, "origin"))
                call GroupEnumUnitsInArea(ENUM_GROUP, GetUnitX(this.caster), GetUnitY(this.caster), 300, Filter(function elementalArrow.explosion))
            elseif this.i2 == 4 and not(IsUnitType(target, UNIT_TYPE_DEAD)) then
                //Wind
                set xc.abilityid    = 'A009' 
                set xc.orderstring  = "cyclone"
                set xc.owningplayer = GetOwningPlayer(caster)
                call xc.castOnTarget( target )
            endif
            //End
            call this.terminate()
            call xc.destroy()
        endif
    endmethod
        
endstruct

    private function onSpellCast takes unit u returns nothing
     local real     x2  = GetSpellTargetX()
     local real     y2  = GetSpellTargetY()

     local real     x   = GetUnitX(u)
     local real     y   = GetUnitY(u)

     local real     ang = Atan2( y2 - y, x2 - x)

     local real     ang2
     local integer  i
     local integer  i2

     local real     mx
     local real     my

     local integer ARROW_COUNT = 3 + 2 * GetUnitAbilityLevel(u, 'ElVo')

     local elementalArrow xc //notice our variable uses the elementalArrow type

        // ang2 would be perpendicular to the other angle, we are using radians so to get
        // ang2 we add PI/2 which is equivalent to 90 degrees.
        set ang2 = ang + bj_PI/2
        set i=0             //
        loop                // we are creating two missiles but that's too much hassle and repetitive
            exitwhen (i==ARROW_COUNT) // code, just use a loop and one trick for the angle you shall see bellow
            if ( (i==ARROW_COUNT-1) and ( ModuloInteger(ARROW_COUNT,2)==1) ) then
                set mx = x // When ARROW_COUNT is odd, create the last fire ball
                set my = y // right in the middle.
            else
                set mx = x + (DISTANCE*(i/2+1))*Cos(ang2) //Getting the position for the missile using a polar 
                set my = y + (DISTANCE*(i/2+1))*Sin(ang2) //projections. Increase distance by 75 every 2 steps.
            endif
            
            set xc = elementalArrow.create(mx, my, ang)
            set xc.fxpath = "Abilities\\Weapons\\Arrow\\ArrowMissile.mdl"
            
            set i2 = GetRandomInt(1,4)
            if i2 == 1 then
                call xc.flash( "Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl" )
            elseif i2 == 2 then
                call xc.flash( "Abilities\\Weapons\\LichMissile\\LichMissile.mdl" )
            elseif i2 == 3 then
                call xc.flash( "Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl" )
            elseif i2 == 4 then
                call xc.flash( "Abilities\\Spells\\Human\\Thunderclap\\ThunderclapTarget.mdl" )
            endif
            
            set xc.speed   = 1000.0 // The missile starts with a speed of 0, acceleration of 1000
            set xc.expirationTime =    1.25 //20000.0 // it expires after 1 seconds.
                                            // So, try visualizing those things

            set xc.z              =   50.0 // some height, since missiles often fly...
            set xc.i2     = i2
            set xc.caster = u // important stuff, see how our collider is using the greenMissile struct
                             // so we can assign the caster, as you saw above the caster determines
                             // what can hit the missile and who to credit the damage for.


            set ang2= bj_PI + ang2 //this is the trick, the second angle will be the opposite to the
                                   // current one.
            set i=i+1
        endloop

    endfunction

    private function spellIdMatch takes nothing returns boolean
        if (GetSpellAbilityId()=='ElVo') then
            call onSpellCast(GetTriggerUnit())
        endif
     return false
    endfunction


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

endscope
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
There are probably better ways to make your arrows travel in a cone but this is what I did.

JASS:
    //The following function provides a different angle for the missile in order to make a "cone" shape. 
    //Multiply the value returned by this function by MISSILE_DEVIATE_ANGLE which determines the cone's angle. Basically, the cone would have an angle of MISSILE_DEVIATE_ANGLE*2.
    private constant function MissileAngleDeviate takes real n, real tot returns real
        //n is the "index" of the missile. n should always begin at 0
        //tot is the total amount of missiles created.        
        if n == 0 and tot == 1 then
        //Return 0 for straight path. Pretty much useless to use this function if this happens.
            return 0.
        else
            return -1. + (n*2.)/(tot-1.)
        endif
    endfunction
Example code: (I'm not using xecollider though so you probably only need to change the angle)
JASS:
set .missile[i].x = .missile[i].x + SPEED*TIMER_LOOP * Cos(.ang+bj_DEGTORAD*MISSILE_DEVIATE_ANGLE*MissileAngleDeviate(I2R(i),I2R(.num)))
MISSILE_DEVIATE_ANGLE needs to be in radians.
.ang was the angle between the spell target coordinates and the caster.
i is an integer used for looping.
.num is the total amount of missiles fired

Edit: On your code:
You should set xc to xecast.create() in the conditions of the wind effect. You should also use xecast.createA() instead so you don't need to worry about destroying it later.

Did you forget to apply the slowing effects for Frost?
 
Status
Not open for further replies.
Top