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

Warlike Spirit v1.1

Spell Description

Call a force of spirit that fly around the caster,
spirit attacks the enemy in 1000 range and also steal 6/7/8/9% of
their hit points and return it to the caster.
But if the enemy die after spirit has stealed their hit points
a Warlike Spirit will be created. Spirit lasts 5.5/6.5/7.5/8.5 seconds.

Warlike Spirit
JASS:
//***************************************************************************
//***************************************************************************
//***************************************************************************
//
//              *          *            *   *    *   *
//            *   *      *   *           * *     *   *
//          *      *   *       *          *      * . *
//                                        Warlike Spirit
//                 *****                 ----------------
//                                           By: Hara
//
//          - Sepll Description: 
//  `                       - Call a force of spirit that fly around the caster, 
//                            spirit attacks the enemy in 1000 range and also steal 6/7/8/9% of 
//                            their hit points and return it to the caster. 
//                            But if the enemy die after spirit has stealed their hit points
//                            a Warlike Spirit will be created. Spirit lasts 5.5/6.5/7.5/8.5 seconds.
//
//          - Installation:
//                                - Import/copy Warlike Spirit code and Custom Units/Abilities/Buffs to your map
//                                - Change the SPELL_ID, WARLIKE_SPIRIT_ID, WS_STUN_ABI, DUMMY_ID if needed
//                                - You may view the raw ID of the objects by pressing CTRL+D in the object editor
//                                - You may play with the configurables below
//
//
//***************************************************************************
//***************************************************************************
//***************************************************************************

//! zinc
library WarlikeSpirit{

    //------------ CONFIGURATION ------------//
    
    //Spirit count called per cast
    private constant integer    SPIRIT_COUNT        = 10;
    //Dummy rawcode, change if needed
    private constant integer    DUMMY_ID            = 'e000';
    //Warlike spirit rawcode, change if needed
    private constant integer    WARLIKE_SPIRIT_ID   = 'e001';
    //Spell rawcode, change if needed
    private constant integer    SPELL_ID            = 'A000';
    //Warlke Spirit's stun rawcode, change if needed
    private constant integer    WS_STUN_ABI         = 'A001';
    //An effect when spirit hits enemy
    private constant string     HIT_EFFECT          = "Abilities\\Spells\\Human\\StormBolt\\StormBoltMissile.mdl";
    //Enemy's effect attachment
    private constant string     HIT_ATTACHMENT      = "chest" ;
    //An effect when spirit return hit points to the caster
    private constant string     HIT_CASTER          = "Abilities\\Spells\\Human\\HolyBolt\\HolyBoltSpecialArt.mdl";
    //Caster's effect attachment
    private constant string     CASTER_ATTACHMENT   = "origin" ;
    //Distance between the caster and the force of spirit
    private constant real       DISTANCE            = 250;
    //Hit distance between spirit and enemy
    private constant real       HIT_DISTANCE        = 3;
    //Spirit missle speed/PERIODIC when it attacks enemy
    private constant real       TARGET_SPEED        = 25;
    //Spirit swivel speed/PERIODIC
    private constant real       SPIRIT_SPEED        = 5;
    //Spirit's enemy detection range
    private constant real       RANGE               = 1000;
    //Time before fire spirit to the enemy
    private constant real       SHOOT_TIME          = 0.5;
    //Default scale value of warlike spirit
    private constant real       WS_DEFAULT_SCALE    = 1.5;
    //Timer period
    private constant real       PERIODIC            = .0312500;

    private constant attacktype ATTACK_TYPE         =   ATTACK_TYPE_MAGIC;
    private constant damagetype DAMAGE_TYPE         =   DAMAGE_TYPE_COLD;
    private constant weapontype WEAPON_TYPE         =   WEAPON_TYPE_METAL_HEAVY_BASH;
    
    //***************************************//
    
    //------------ NON-CONFIGURATION --------//
    
    private constant timer      TIMER            = CreateTimer();
    
    private constant real       inBetween        = 6.283188 / SPIRIT_COUNT ; //6.283188 = bj_PI*2
    
    private constant group      G                = CreateGroup();
    
    private constant location  locZ              = Location(0,0);
    
    private          integer    MUI                = -1;
    private          integer    MUI2[];
    
    private          unit       Caster;
    private          player     CasterOwner;
    
    
    //***************************************//
    
    //IsUnitAlive func
    function UnitAlive (unit filterUnit)                -> boolean{
        return (!IsUnitType(filterUnit,UNIT_TYPE_DEAD) && GetUnitTypeId(filterUnit)!=0);
    }
    
    //Spell damage
    function getDamagePercentage (integer abiLevel)     -> real{
        return (abiLevel + 5.);
    }
    
    //Spell duration
    function getDuration (integer abiLevel)             -> real {
        return (abiLevel + 4.5);
    }
    
    //Summon warlike spirit duration
    function getWarlikeDuration (integer abiLevel)      -> real {
        return (abiLevel + 1.5);
    }
    
    //--------------------------------
    
    function DistanceBetween (unit A, unit B)           -> real {
        real dx = GetUnitX(B) - GetUnitX(A);
        real dy = GetUnitY(B) - GetUnitY(A);
        return SquareRoot(dx * dx + dy * dy);
    }
    
    function filterFunc()                               -> boolean{
        return UnitAlive(GetFilterUnit());
    }
    
    function GroupPickEnum (){
    
        unit    tempU   =   GetEnumUnit();
    
        if(UnitAlive(tempU) && IsUnitEnemy(tempU,CasterOwner)) {
        
            bj_groupRandomConsidered = bj_groupRandomConsidered + 1;
        
            if (GetRandomInt(1,bj_groupRandomConsidered) == 1)
                bj_groupRandomCurrentPick = tempU;
        
        }
        
        tempU   =   null;
        
    }

    function RandomUnit (group whichGroup) -> unit{
    
        bj_groupRandomConsidered = 0;
        bj_groupRandomCurrentPick = null;
        ForGroup(whichGroup, function GroupPickEnum);
        
        return bj_groupRandomCurrentPick;
        
    }
    
    //-----------------------------------
    
    private struct WarlikeSpiritPlugin{
    
        unit caster;
        unit dummy;
        unit target;
        unit warlike_spirit=null;
        
        real angle;
        real height = 0.;
        real time   = 0.;
        real maxDis;
        real maxHei;
        real dmg;
        real ws_dur;
        
        integer abiLevel;
        
        player casterOwner;
        
        boolean fire  = false;
        boolean back  = false;
        boolean pause = false;
        
        static method release( integer tempInt ){
        
            //Recycle
            MUI2[tempInt] = MUI2[MUI];
            MUI2[MUI]     = -1;
            MUI           = MUI - 1;
            
            if( MUI == -1 )
                PauseTimer(TIMER);
        
        }

        method getHeight(boolean b,real tempX,real tempY) -> real{
        
            real tempA;
        
            if( b ){
                            
                tempA               = GetUnitFlyHeight(target) + 1.;
                height              = tempA - (tempA * ( (DistanceBetween(target , dummy)/maxDis) ));
                return                bj_RADTODEG*Atan2(GetUnitY(target)-tempY,GetUnitX(target)-tempX ) * bj_DEGTORAD;
                                
            }else{
            
                tempA               = GetUnitFlyHeight(dummy) + 1.;
                height              = maxHei * ( (DistanceBetween(caster,dummy)/maxDis) );
                return                bj_RADTODEG*Atan2(GetUnitY(caster)-tempY,GetUnitX(caster)-tempX ) * bj_DEGTORAD;
                
            }
        
        }
        
        method shootBack(){
        
            maxHei          = GetUnitFlyHeight(dummy);
            maxDis          = DistanceBetween(caster , dummy);
            back            = true;
            target          = null;
        
        }
        
        static method onPeriodic(){
        
            thistype    this;
        
            integer     tempInt     =       0;
            integer     tempInt2;
            
            real        tempX;
            real        tempY;
            real        tempA;
            
            unit        tempU       =       null;
            
            while(tempInt <= MUI){
            
                this                = MUI2[tempInt];
                
                //
                
                if( UnitAlive(dummy) && UnitAlive(caster) ){
                
                    if(  fire  ) {
                    
                        if( !pause ){
                        
                            tempX                   = GetUnitX(dummy);
                            tempY                   = GetUnitY(dummy);
                            
                            tempA                   = getHeight(target != null,tempX,tempY);
                                                      
                            tempX                   = tempX + TARGET_SPEED * Cos(tempA);
                            tempY                   = tempY + TARGET_SPEED * Sin(tempA);
                            
                            if( !back ){

                                if( UnitAlive(target) ){
                                
                                    if( IsUnitInRangeXY(target,tempX,tempY,HIT_DISTANCE) ){
                                
                                        DestroyEffect(AddSpecialEffectTarget(HIT_EFFECT,target,HIT_ATTACHMENT));
                                        
                                        dmg=(GetUnitState(target,UNIT_STATE_MAX_LIFE)*dmg)/100.;
                                        
                                        UnitDamageTarget(caster,target,dmg,true,false,ATTACK_TYPE,DAMAGE_TYPE,WEAPON_TYPE);
                                        
                                        //Check if the enemy is alive
                                        
                                        if(!UnitAlive(target)){
                                        
                                            //Create warlike spirit
                                            warlike_spirit  =   CreateUnit(casterOwner,WARLIKE_SPIRIT_ID,tempX,tempY,0.);
                                            SetUnitAbilityLevel(warlike_spirit,WS_STUN_ABI,abiLevel);
                                            UnitApplyTimedLife(warlike_spirit,0x0,ws_dur);
                                            SetUnitScale(dummy,0.,0.,0.);
                                            pause           =   true;
                                        
                                        }else
                                            shootBack();
                                        
                                    
                                    }
                                
                                }else
                                    KillUnit(dummy);
                                
                            
                            }else{
                            
                                if( IsUnitInRangeXY(caster,tempX,tempY,HIT_DISTANCE) ){
                                
                                    DestroyEffect(AddSpecialEffectTarget(HIT_CASTER,caster,CASTER_ATTACHMENT));
                                    
                                    if( warlike_spirit != null ){
                                        dmg            = dmg/2;
                                        warlike_spirit = null;
                                    }
                                    
                                    SetWidgetLife(caster,GetWidgetLife(caster)+dmg);
                                    
                                    RemoveUnit(dummy);
                                    
                                    dummy       = null;
                                    caster      = null;
                                    casterOwner = null;
                                    
                                    this.destroy();
                                    release(tempInt);
                                        
                                }

                            }
                        
                        }else if ( !UnitAlive( warlike_spirit ) ){
                            pause           = false;
                            
                            SetUnitX(dummy,GetUnitX(warlike_spirit));
                            SetUnitY(dummy,GetUnitY(warlike_spirit));
                            
                            SetUnitScale(dummy,WS_DEFAULT_SCALE,WS_DEFAULT_SCALE,0.);
                            
                            shootBack();
                        }
                    
                    }else {
                    
                        //
                        
                        angle               = angle + SPIRIT_SPEED;
                        tempA               = angle *  bj_DEGTORAD;
                        
                        tempX               = GetUnitX(caster) + DISTANCE * Cos(tempA);
                        tempY               = GetUnitY(caster) + DISTANCE * Sin(tempA);
                        
                        //

                        if( target == null ){
                        
                            CasterOwner         = casterOwner;
                            GroupEnumUnitsInRange(G,tempX,tempY,RANGE,function filterFunc);
                            target              = RandomUnit(G);
                            GroupClear(G);
                        
                        }else{
                        
                            if( time <= 0. ){
                            
                                fire                = true;
                                maxDis              = DistanceBetween(target , dummy);
                                
                                UnitPauseTimedLife(dummy,true);
                            
                            }else
                                time         = time - PERIODIC;
                        
                        }
                        
                    }
                    
                    if( !pause ){
                    
                        MoveLocation(locZ,tempX,tempY);
                        SetUnitX(dummy,tempX);
                        SetUnitY(dummy,tempY);
                        SetUnitFlyHeight(dummy,GetLocationZ(locZ)+height,99999.);
                    
                    }
                
                }else{
                
                    this.destroy();
                    release(tempInt);
                }

                //
                
                tempInt              = tempInt + 1;
            
            }
            
        
        }
    
        static method new_WarlikeWpirit(unit whichDummy , real whichAngle, real shootTime){
        
            thistype this       = allocate();
            
            MUI                   = MUI + 1;
            MUI2[MUI]               = this;
        
            caster              = Caster;
            dummy               = whichDummy;
            
            abiLevel            = GetUnitAbilityLevel(caster,SPELL_ID);
            
            angle               = whichAngle;
            
            casterOwner         = CasterOwner;
            
            time                = shootTime;
            
            dmg                 = getDamagePercentage(abiLevel);
            ws_dur              = getWarlikeDuration(abiLevel);
            
            UnitApplyTimedLife(dummy,0x0,getDuration(abiLevel));
            
            if(MUI == 0){
        
                TimerStart(TIMER,PERIODIC,true,function thistype.onPeriodic);
        
            }
        
        }
    
    }
    
    //
    
    function onCast() -> boolean{

        real     angle    = 360./SPIRIT_COUNT;
        real     x;
        real     y;
        real     tempX;
        real     tempY;
        real     tempA;
        real     tempT    = 0.;
        
        integer  temp     = 1;
        
        Caster            = GetTriggerUnit();
        CasterOwner       = GetTriggerPlayer();
        
        tempX             = GetUnitX(Caster);
        tempY             = GetUnitY(Caster);
        
        while(temp <= SPIRIT_COUNT){
        
            tempA         = temp  * inBetween;
            
            x             = tempX + DISTANCE * Cos(tempA);
            y             = tempY + DISTANCE * Sin(tempA);
            
            //
            tempT         = tempT + SHOOT_TIME;
            WarlikeSpiritPlugin.new_WarlikeWpirit( CreateUnit(CasterOwner,DUMMY_ID,x,y,0.) , bj_RADTODEG*Atan2(y-tempY,x-tempX ) , tempT );
            
            //
        
            temp          = temp + 1;
        
        }

        return false;
        
    }
    
    function onInit(){
        
        integer i               = 0;
        trigger trig            = CreateTrigger();
        
        while(i<bj_MAX_PLAYERS){
        
            TriggerRegisterPlayerUnitEvent(trig,Player(i),EVENT_PLAYER_UNIT_SPELL_EFFECT,null);
            i = i + 1;
            
        }
        
        TriggerAddCondition(trig,function onCast);
    
    }

}
//! endzinc

Changelog
v1.0: First release version.
v1.1: Minor bugs fixed.
Keywords:
warlike,spirit
Contents

Just another Warcraft III map (Map)

Reviews
Submission: Warlike Spirit v1.1 Date: 18.05.2015 Status: Approved Rating: 4/5 Link Post Moderator: IcemanBo
Okay, let's analyze this spell properly.

Visual:
Volchachka told me his opinion about visuals. I'm not very good at rating these since I love every flashy eyecandy. ^^

Effects are 10/10. Swift execution, nice idea, good harmony, no effect overloading and stable FPS rate on them.

Coding:
  • In Configuration section of your code there are following things which can be improved:
    • PERIODIC = .0312500 is better to be typed without unnecessary zeros, just like: .03125.
    • constant attacktype ATTACK_TYPE, constant damagetype DAMAGE_TYPE and constant weapontype WEAPON_TYPE should definetly be private. Else your spell may be incompatible with something that somebody implemented in the map. For example, in his own damage system he wants something names ATTACK_TYPE to be public.
    • private constant group G is not a good name for global variable because it's not too understandable for end user (and no matter that it's non-configurable).
    • private constant real inBetween = 360. / SPIRIT_COUNT * .0174533;, if it's constant, must be simplified as much as its possible. For the compiler, a/b*c is same with a*c/b (to make it as a/(b*c) for compiler you'd placed brackets. So you can calculate the value of 360*0.0174533 = 360*PI/180 = 2*PI which is equal to 6.283185. With the way I have shown, in your case, you can get much more precise result than just reapproximating the approximated. The direct calculation'd given you 6.283188.
    • You do not need the GetUnitTypeId(filterUnit) != 0 comparison because only units which do not exist in the map have the unit type 0. I did a test on a blank 32x32 map:
      JASS:
      function CheckUnitWithZeroType takes nothing returns nothing
          local unit  testUnit  = CreateUnit(Player(0),0,0,0,0)
          local group testGroup = CreateGroup()
          local unit  groupUnit
          
          if IsUnitType(testUnit,UNIT_TYPE_DEAD) then
              call BJDebugMsg("The unit with type 0 is dead.")
          else
              call BJDebugMsg("The unit with type 0 is not dead.")
          endif
          
          call GroupEnumUnitsInRange(testGroup,0.,0.,200.,null)
          
          loop
              set groupUnit = FirstOfGroup(testGroup)
              exitwhen groupUnit == null
              call BJDebugMsg("Unit detected! Type: "+I2S(GetUnitTypeId(groupUnit)))
              call GroupRemoveUnit(testGroup,groupUnit)
          endloop
          
          call DestroyGroup(testGroup)
          
          set testGroup = null
          set testUnit  = null
      endfunction
      This resulted the following debug: truly, the unit with a type 0 considered as not dead, but such a unit wasn't included in group (no debug from group loop at all => 0 units). So you gotta remove this condition as unnecessary one. Units with a type 0 will be correctly interpreted by Blizzard's functions, this should not be a point of worry to filter it out. It turns out as unnecessary code line.
  • In Spell section of your code there are following things which can be improved:
    • As endless times before this one, I'm pointing out the same mistake: please use GetWidgetX and GetWidgetY instead of GetUnitX and GetUnitY because first ones are slightly faster than second ones because of less type inherits.
    • Why struct WarlikeSpiritPlugin is public? This is used only by your spell, so how about making it private as all other things which are inside? This increases the compatibility of your spell with other spells in some map.
    • Regarding dmg=(GetUnitState(target,UNIT_STATE_MAX_LIFE)*dmg)/100.;... Actually *.01 is much faster than /100.
    • Why you need to declare player p = GetTriggerPlayer(); when you already have CasterOwner = GetTriggerPlayer();? Besides of that, that player wasn't nulled properly by you => it produced a memory leak.
    • I fear that it's pretty much non-MUI because of globals like Caster and CasterOwner which will overwrite if a different unit will cast the spell. Please reorganize that part so the spell becomes full MUI.

    Cannot give it more than 4/5 mostly because of that non-MUI'ness. Fix it up and I will update my review. :)

    4/5: Recommended

    How that helped me to become better coder:
    • Very smart usage of IsUnitInRangeXY function. After I did a couple of benchmark, comparing the speed of SquareRoot method and IsUnitInRangeXY, second one executed 81 times more than the SquareRoot-based method. That's pretty impressive finding!
 
I had the honour to read 2 zinc spells today in spell section. \o/
  • Please deindex properly if caster was removed from game. That's necessarily needed.
  • Your speed setting is actually influenced by interval. Better would be to adapt it in config to normal WC3 movement speed/second.
    Or at least you should mention that it's /interval.
  • 6.283188 -> bj_pi*2.
    You can stay with it actually, but with "pi" it's more intuitive and easier to understand/read.
  • bj_RADTODEG*Atan2(GetUnitY(target)-tempY,GetUnitX(target)-tempX ) * .0174533;
    why multiply with bj_RADTODEG, then with .0174533? Also .0174533 is harder to understand, it's not good to work with such values directly in code.
  • Names like "M" and "MD" are not descritive.

I fear that it's pretty much non-MUI because of globals like Caster and CasterOwner which will overwrite if a different unit will cast the spell. Please reorganize that part so the spell becomes full MUI.
nhocklanhox6 has not commented it, but as I could see he uses them as temporary container.

Good and solid spell. But daheck, for me personaly you use too much free space sometimes in variable assignment or just between some code. :D ... ^_°
 
for me personaly you use too much free space sometimes in variable assignment or just between some code. :D

:D, I just split types of variable assignment like: real with real, integer with integer, I think it will increase readable things. And the code, if the code length is not fit like: AAAAAAA with AAAAAAAAAAAAAAAA, I will split them by those spacings ^_<..

Btw, thanks for the review :)..
 
Last edited:
I just notice it now, because you're using the constants now,
but if you use DEGTORAD and then RADTODEG again, it's redundant.

MUI and MUI2 is also not descriptive. "MUI" actually describes a compatibility.

Is it intended that you still see the spirits for a few more seconds after death/removal?

= 6.283188 / SPIRIT_COUNT ; //6.283188 = bj_PI*2
^I don't understand why you still prefer to use the number over the constant. :/

Edit:

But spell is coded well and works fine all in all.

Approved
 
Last edited:
Top