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

[vJASS] Chain Lightning System; Currently Learning Advanced Structs

Status
Not open for further replies.
Level 3
Joined
Aug 12, 2010
Messages
29
So this is my first time really getting used to writing something mainly using structs and as of now, it works how it's intended to(Create a chain, allowing healing or damage and executing a function if one is set). My question is: How could I go about betting optimizing or improving what I created? I looked at quite a bit of coding while writing this for pointers, but I could never really find an advance tutorial on structs. I'm pretty positive it's MUI.

Any feedback, criticism and comments are very much welcomed!

The entire system is written with Zinc.

JASS:
//! zinc
library ChainSys requires GroupUtils,AIDS,Damage,TimerUtils{

   private{
       //Height of the chain
       constant real OFFSET = 65.0;
       //How often the chain will move
       constant real INTERVAL = 0.15;
   }

   private{
       location SourceZ=Location(0,0);
       location TargetZ=Location(0,0);
   }

   public{
       constant string LIGHTNING_CHAIN_LIGHTNING_PRIMARY   = "CLPB";
       constant string LIGHTNING_CHAIN_LIGHTNING_SECONDARY = "CLSB";
       constant string LIGHTNING_DRAIN                     = "DRAB";
       constant string LIGHTNING_DRAIN_LIFE                = "DRAL";
       constant string LIGHTNING_DRAIN_MANA                = "DRAM";
       constant string LIGHTNING_FINGER_OF_DEATH           = "AFOD";
       constant string LIGHTNING_FORKED_LIGHTNING          = "FORK";
       constant string LIGHTNING_HEALING_WAVE_PRIMARY      = "HWPB";
       constant string LIGHTNING_HEALING_WAVE_SECONDARY    = "HWSB";
       constant string LIGHTNING_LIGHTNING_ATTACK          = "CHIM";
       constant string LIGHTNING_MAGIC_LEASH               = "LEAS";
       constant string LIGHTNING_MANA_BURN                 = "MBUR";
       constant string LIGHTNING_MANA_FLARE                = "MFPB";
       constant string LIGHTNING_SPIRIT_LINK               = "SPLK";
   }
   
   public{
       unit Chain_LastCasterUnit;
       unit Chain_LastHitUnit;
   }

   struct ChainTime{
       lightning light;

       method destroy(){
           DestroyLightning(this.light);
           this.light=null;
       }
   }

   function onTimer(){
       timer t=GetExpiredTimer();
       ChainTime this=GetTimerData(t);
       this.destroy();
   }

   function AddLightningTimed(string art,unit whichUnit,unit target,real duration,integer red,integer green,integer blue){
       ChainTime this=ChainTime.create();
       timer t=NewTimer();
       real sourceX=GetUnitX(whichUnit),sourceY=GetUnitY(whichUnit),sourceH=GetUnitFlyHeight(whichUnit);
       real targetX=GetUnitX(target),targetY=GetUnitY(target),targetH=GetUnitFlyHeight(target);
       MoveLocation(SourceZ,sourceX,sourceY);
       MoveLocation(TargetZ,targetX,targetY);
       this.light=AddLightningEx(art,true,
       sourceX,sourceY,GetLocationZ(SourceZ)+OFFSET+sourceH,
       targetX,targetY,GetLocationZ(TargetZ)+OFFSET+targetH);
       SetLightningColor(this.light,red,green,blue,1.0);
       SetTimerData(t,integer(this));
       TimerStart(t,duration,false,function onTimer);
       t=null;
   }

   public struct Chain{
       unit caster,source,target;
       player owner;
       real radiusX,radiusY;
       
       string primaryChain;
       string secondaryChain;
       string impact;
       string point;
       
       integer maxBounce;
       integer red=1;
       integer green=1;
       integer blue=1;

       real radius;
       real damage;
       real heal;
       real modify;

       boolean hitFriendly=false;
       boolean hitEnemy=false;
       boolean hitStructure=false;

       attacktype attackType;
       damagetype damageType;
   
       static code onHit;

       group chainGroup;
       integer tick=-1;

       method destroy(){
           ReleaseGroup(this.chainGroup);
           this.caster=null;
           this.source=null;
           this.target=null;
           this.owner=null;
           this.chainGroup=null;
       }

       static method onTimer(){
           timer t=GetExpiredTimer();
           trigger e;
           thistype this=GetTimerData(t);
           real sourceX=GetUnitX(this.source),sourceY=GetUnitY(this.source),sourceH=GetUnitFlyHeight(this.source);
           real targetX=GetUnitX(this.target),targetY=GetUnitY(this.source),targetH=GetUnitFlyHeight(this.target);
           group g=NewGroup();
           MoveLocation(SourceZ,sourceX,sourceY);
           MoveLocation(TargetZ,targetX,targetY);
           GroupAddUnit(this.chainGroup,this.target);
           if (this.target!=null){
               //Primary creation
               if (this.tick==-1){
                   AddLightningTimed(this.primaryChain,this.source,this.target,0.45,this.red,this.green,this.blue);
               //Secondary creation
               }else if (this.tick>=0&&this.secondaryChain!=null){
                   AddLightningTimed(this.secondaryChain,this.source,this.target,0.45,this.red,this.green,this.blue);
               }
               if (this.damage>0){
                   UnitDamageTargetEx(this.caster,this.target,this.damage,false,false,this.attackType,this.damageType,WEAPON_TYPE_WHOKNOWS);
                   this.damage-=this.damage*modify;
               }else if (this.heal>0){
                   SetWidgetLife(this.target,GetWidgetLife(this.target)+this.heal);
                   this.heal-=this.heal*modify;
               }
               Chain_LastHitUnit=this.target;
               Chain_LastCasterUnit=this.caster;
               e=CreateTrigger();
               TriggerAddAction(e,this.onHit);
               TriggerExecute(e);
               DestroyTrigger(e);
               DestroyEffect(AddSpecialEffectTarget(this.impact,this.target,this.point));       
               this.tick+=1;
               this.source=this.target;
               GroupEnumUnitsInRange(g,radiusX,radiusY,this.radius,null);
               this.target=FirstOfGroup(g);
               while (this.target!=null){
                   if (!IsUnitType(this.target,UNIT_TYPE_DEAD)&&GetUnitTypeId(this.target)>0&&!IsUnitInGroup(this.target,this.chainGroup)){
                       if (this.hitStructure||!IsUnitType(this.target,UNIT_TYPE_STRUCTURE)){
                           if (this.hitEnemy&&IsUnitEnemy(this.target,this.owner)){
                               break;
                           }else if (this.hitFriendly&&IsUnitAlly(this.target,this.owner)){
                               break;
                           }
                       }
                   }
                   GroupRemoveUnit(g,this.target);
                   this.target=FirstOfGroup(g);
               }
           }else{
               ReleaseTimer(t);
               this.destroy();
           }
           if (this.tick>=this.maxBounce){
               ReleaseTimer(t);
               this.destroy();
           }
           ReleaseGroup(g);
           g=null;
           e=null;
           t=null;
       }

       static method launch(thistype this){
           timer t=NewTimer();
           this.source=this.caster;
           this.radiusX=GetUnitX(this.target);
           this.radiusY=GetUnitY(this.target);
           this.chainGroup=NewGroup();
           if (this.secondaryChain==null){
               this.secondaryChain=this.primaryChain;
           }
           SetTimerData(t,this);
           TimerStart(t,INTERVAL,true,function thistype.onTimer);
           t=null;
       }
           
       static method initialize(unit whichCaster)->thistype{
           Chain this=GetUnitId(whichCaster);
           this.caster=whichCaster;
           this.owner=GetOwningPlayer(this.caster);
           this.tick=-1;
           return this;
       }

       method operator primaryArt=(string art){
           this.primaryChain=art;
       }

       method operator secondaryArt=(string art){
           this.secondaryChain=art;
       }

       method operator impactArt=(string art){
           this.impact=art;
       }

       method operator attachment=(string where){
           this.point=where;
       }

       method operator onStrike=(code whatEvent){
           this.onHit=whatEvent;
       }

       method operator bounce=(integer value){
           this.maxBounce=value;
       }
       
       method operator colorRed=(integer value){
           this.red=value;
       }
       
       method operator colorBlue=(integer value){
           this.blue=value;
       }
       
       method operator colorGreen=(integer value){
           this.green=value;
       }

       method operator area=(real value){
           this.radius=value;
       }

       method operator targetDamage=(real value){
           this.damage=value;
       }

       method operator targetHeal=(real value){
           this.heal=value;
       }

       method operator modifyValue=(real value){
           this.modify=value;
       }

       method operator attacktype=(attacktype value){
           this.attackType=value;
       }
       
       method operator damagetype=(damagetype value){
           this.damageType=value;
       }

       method operator filterFriendly=(boolean flag){
           this.hitFriendly=flag;
       }

       method operator filterEnemy=(boolean flag){
           this.hitEnemy=flag;
       }
       
       method operator filterStructure=(boolean flag){
           this.hitStructure=flag;
       }   
   }
}
//! endzinc
 
Last edited:
overall looks really good! Couple of comments:

  • If you want to customize what happens when a struct is destroyed, you should use method onDestroy instead of method destroy. The latter overrides the actual destroy that structs implement by default (which is in charge of marking instances as "free" so that they can be reused later). Alternatively, you can add a deallocate call, which is basically what ZINC/vJass does under the hood:
    JASS:
    method destroy() {
        DestroyLightning(this.light);
        this.light = null;
        this.deallocate();
    }
  • You don't really need to null variables in the destroy method. Struct members are global arrays, so they don't suffer the same leak that local variables do.
  • I noticed you have a variable, static code onHit. The reason why code has to be static is because code arrays aren't supported. For what you're trying to do, a lot of people take in either a condition or trigger. For example, onStrike could instead be set up like this:
    JASS:
    method operator onStrike=(code whatEvent) {
        this.onHit = Condition(function whatEvent)
    }
    Where onHit would be a condition onHit.
  • In general, method operators are nice when you want to do some side-effects when someone sets a variable, or if you want to support a particular API but you don't want to give users access to your variables. For example, in the ^onStrike above, we want to allow the user to pass in a code var, but we are storing it as a condition internally. However, in your case, all your method operators are just assigning a variable that is the same type, so I would personally rename those variables and remove the operators. e.g. this:
    JASS:
    struct Example {
        effect fx;
    
        method operator specialEffect=(effect sfx) {
            this.fx = sfx;
        }
    }
    
    // ... could be 
    
    struct Example {
        effect specialEffect;
    }

    It'll remove a lot of bloat.
  • Some repeated code here:
    JASS:
    }else{
        ReleaseTimer(t);
        this.destroy();
    }
    
    if (this.tick>=this.maxBounce){
        ReleaseTimer(t);
        this.destroy();
    }
    
    // ... could be
    
    if (this.target == null || this.tick >= this.maxBounce) {
        ReleaseTimer(t);
        this.destroy();
    }
  • These lines don't seem to be used in onTimer:
    JASS:
               MoveLocation(SourceZ,sourceX,sourceY);
               MoveLocation(TargetZ,targetX,targetY);
  • 0.45 (the lightning duration) should probably be a constant in the configuration section
  • red, green, and blue should be real instead of integers. Since the JASS natives take a number from 0-1 instead of 0-255.
  • I wouldn't use an indexing system for this. It places an unnecessary constraint where you can have only one Chain going on at a time per caster. I would instead just use the normal struct allocation instead of using GetUnitId.
i also have a couple of comments about the code architecture/readability--most of these are opinions though:
  • Convention is usually to have spaces for clarity. e.g. SetWidgetLife(u, life) instead of SetWidgetLife(u,life). And if (this.hitStructure || !IsUnitType...) instead of if (this.hitStructure||!IsUnitType...). And timer t = GetExpiredTimer() instead of timer t=GetExpiredTimer(). Just for readability. :)
  • Might be worth moving the Z related stuff into a separate function:
    JASS:
    private {
        location LocationZ = Location(0, 0);
    }
    
    function GetZ(real x, real y) -> real {
        call MoveLocation(LocationZ, x, y)
        return GetLocationZ(LocationZ)
    }
    It'll make AddLightningTimed a little cleaner to read
but overall, it looks awesome! Great job!
 
Level 3
Joined
Aug 12, 2010
Messages
29
Thank you @PurgeandFire for the comment! I greatly appreciate it. I took what you wrote and essentially rewrote the entire thing.

JASS:
//! zinc
library ChainSys requires TimerUtils, optional Damage,optional GroupUtils{

   private{
       //Height of the chain
       constant real CHAIN_OFFSET   = 65.0;
       //How often the chain will move.
       constant real CHAIN_INTERVAL = 0.15;
       //How long the chain effect will last.
       constant real CHAIN_DURATION = 0.55;
   }

   private{
       location LocationZ=Location(0,0);
   }

   public{
       constant string LIGHTNING_CHAIN_LIGHTNING_PRIMARY   = "CLPB";
       constant string LIGHTNING_CHAIN_LIGHTNING_SECONDARY = "CLSB";
       constant string LIGHTNING_DRAIN                     = "DRAB";
       constant string LIGHTNING_DRAIN_LIFE                = "DRAL";
       constant string LIGHTNING_DRAIN_MANA                = "DRAM";
       constant string LIGHTNING_FINGER_OF_DEATH           = "AFOD";
       constant string LIGHTNING_FORKED_LIGHTNING          = "FORK";
       constant string LIGHTNING_HEALING_WAVE_PRIMARY      = "HWPB";
       constant string LIGHTNING_HEALING_WAVE_SECONDARY    = "HWSB";
       constant string LIGHTNING_LIGHTNING_ATTACK          = "CHIM";
       constant string LIGHTNING_MAGIC_LEASH               = "LEAS";
       constant string LIGHTNING_MANA_BURN                 = "MBUR";
       constant string LIGHTNING_MANA_FLARE                = "MFPB";
       constant string LIGHTNING_SPIRIT_LINK               = "SPLK";
   }
 
   public{
       unit Chain_LastCasterUnit;
       unit Chain_LastHitUnit;
   }

   function GetLocZ(real x,real y)->real{
       MoveLocation(LocationZ,x,y);
       return GetLocationZ(LocationZ);
   }

   struct ChainEffect{
       lightning Chain_Lightning;
   }

   function onTimer(){
       timer t = GetExpiredTimer();
       ChainEffect this = GetTimerData(t);
       DestroyLightning(this.Chain_Lightning);
       this.destroy();
   }

   function AddLightningTimed(string effectId,unit source,unit target,real duration,real red,real green,real blue){
       ChainEffect this = ChainEffect.create();
       timer t = NewTimer();
       real sourceX = GetUnitX(source),sourceY = GetUnitY(source),sourceH = GetUnitFlyHeight(source),sourceZ = GetLocZ(sourceX,sourceY);
       real targetX = GetUnitX(target),targetY = GetUnitY(target),targetH = GetUnitFlyHeight(target),targetZ = GetLocZ(targetX,targetY);
       this.Chain_Lightning=AddLightningEx(effectId,true,
       sourceX,sourceY,sourceZ+CHAIN_OFFSET+sourceH,
       targetX,targetY,targetZ+CHAIN_OFFSET+targetH);
       SetLightningColor(this.Chain_Lightning,red,green,blue,1.0);
       SetTimerData(t,this);
       TimerStart(t,duration,false,function onTimer);
       t = null;
   }

   public struct Chain{
       unit Chain_Caster,Chain_Source,Chain_Target;
     
       player Chain_Owner;

       string Chain_Primary_Effect;
       string Chain_Secondary_Effect;
       string Chain_Impact_Effect;
       string Chain_Impact_Attachment;

       integer Chain_Max_Bounce;

       real Chain_Color_Red   = 1.0;
       real Chain_Color_Green = 1.0;
       real Chain_Color_Blue  = 1.0;
       real Chain_Radius;
       real Chain_Interval    = CHAIN_INTERVAL;
       real Chain_Duration    = CHAIN_DURATION;
       real Chain_Damage;
       real Chain_Heal;
       real Chain_Modify_Value;
       real Chain_RadiusX,Chain_RadiusY;

       boolean Chain_Hits_Friendly  = false;
       boolean Chain_Hits_Enemy     = false;
       boolean Chain_Hits_Structure = false;

       attacktype Chain_Attacktype = ATTACK_TYPE_MAGIC;
       damagetype Chain_Damagetype = DAMAGE_TYPE_NORMAL;

       static code Chain_OnHit = null;

       group Chain_Hit_Group;
       integer Chain_Tick = -1;

       method destroy(){
           static if (LIBRARY_GroupUtils){
               ReleaseGroup(this.Chain_Hit_Group);
           }else{
               RemoveGroup(this.Chain_Hit_Group);
           }
           this.Chain_Tick=-1;
           this.deallocate();
       }

       static method onTimer(){
           timer t = GetExpiredTimer();
           thistype this = GetTimerData(t);
           group g;
           boolean b = false;
         
           static if (LIBRARY_GroupUtils){
               g = NewGroup();
           }else{
               g = CreateGroup();
           }
           GroupAddUnit(this.Chain_Hit_Group,this.Chain_Target);
           this.Chain_Tick+=1;
           if (this.Chain_Target==null
           || this.Chain_Tick>this.Chain_Max_Bounce){
               ReleaseTimer(t);
               this.destroy();
           }else{
               if (this.Chain_Tick==-1){
                   AddLightningTimed(this.Chain_Primary_Effect,this.Chain_Source,this.Chain_Target,this.Chain_Interval,this.Chain_Color_Red,this.Chain_Color_Green,this.Chain_Color_Blue);
               }else{
                   AddLightningTimed(this.Chain_Secondary_Effect,this.Chain_Source,this.Chain_Target,this.Chain_Interval,this.Chain_Color_Red,this.Chain_Color_Green,this.Chain_Color_Blue);
               }
               if (this.Chain_Damage>0.0){
                   static if (LIBRARY_Damage){
                       UnitDamageTargetEx(this.Chain_Caster,this.Chain_Target,this.Chain_Damage,false,false,this.Chain_Attacktype,this.Chain_Damagetype,WEAPON_TYPE_WHOKNOWS);
                   }else{
                       UnitDamageTarget(this.Chain_Caster,this.Chain_Target,this.Chain_Damage,false,false,this.Chain_Attacktype,this.Chain_Damagetype,WEAPON_TYPE_WHOKNOWS); 
                   }
                   this.Chain_Damage+=this.Chain_Damage*this.Chain_Modify_Value;
               }
               if (this.Chain_Heal>0.0){
                   SetWidgetLife(this.Chain_Target,GetWidgetLife(this.Chain_Target)+this.Chain_Heal);
                   this.Chain_Heal+=this.Chain_Heal*this.Chain_Modify_Value;
               }
               if (this.Chain_OnHit!=null){
                   Chain_LastCasterUnit = this.Chain_Caster;
                   Chain_LastHitUnit    = this.Chain_Target;
                   Filter(this.Chain_OnHit);
                   BJDebugMsg("OnHit fired?");
               }
               DestroyEffect(AddSpecialEffectTarget(this.Chain_Impact_Effect,this.Chain_Target,this.Chain_Impact_Attachment));
               this.Chain_Source=this.Chain_Target;
               GroupEnumUnitsInRange(g,this.Chain_RadiusX,this.Chain_RadiusY,this.Chain_Radius,null);
               this.Chain_Target=FirstOfGroup(g);
               while (this.Chain_Target!=null){
                   if (!IsUnitType(this.Chain_Target,UNIT_TYPE_DEAD)
                   && GetUnitTypeId(this.Chain_Target)>0
                   && !IsUnitInGroup(this.Chain_Target,this.Chain_Hit_Group)){
                       if (this.Chain_Hits_Structure
                       || !IsUnitType(this.Chain_Target,UNIT_TYPE_STRUCTURE)){
                           if (this.Chain_Hits_Enemy
                           && IsUnitEnemy(this.Chain_Target,this.Chain_Owner)){
                               break;
                           }else if (this.Chain_Hits_Friendly==TRUE
                           && IsUnitAlly(this.Chain_Target,this.Chain_Owner)){
                               break;
                           }
                       }
                   }
                   GroupRemoveUnit(g,this.Chain_Target);
                   this.Chain_Target=FirstOfGroup(g);
               } 
           }
           static if (LIBRARY_GroupUtils){
               ReleaseGroup(g);
           }else{
               RemoveGroup(g);
           }
           t = null;
           g = null;
       }

       static method LaunchChain(thistype this){
           timer t = NewTimer();
           this.Chain_Source = this.Chain_Caster;
           this.Chain_RadiusX = GetUnitX(this.Chain_Target);
           this.Chain_RadiusY = GetUnitY(this.Chain_Target);
           static if (LIBRARY_GroupUtils){
               this.Chain_Hit_Group = NewGroup();
           }else{
               this.Chain_Hit_Group = CreateGroup();
           }
           if (this.Chain_Secondary_Effect==null
           && this.Chain_Primary_Effect!=null){
               this.Chain_Secondary_Effect=this.Chain_Primary_Effect;
           }
           SetTimerData(t,this);
           TimerStart(t,this.Chain_Interval,true,function thistype.onTimer);
           t = null;
       }
     
       static method create()->thistype{
           Chain this=Chain.allocate();
           return this;
       }
   }
}
//! endzinc

The only thing I'm running into is the part where you'd set the OnHit function. When I test it, nothing happens at all. I'm not entirely sure what you exactly meant by the usage you wrote.

Also another thing I'm running into is that the variables are carrying over into the cast of the ability.
For example, this.Chain_Tick when used on the BJDebugMsg will continue from the last value from the previous ability, causing me to have to "reset" it on the destroy method.
 
Last edited:
Level 13
Joined
Jun 23, 2009
Messages
297
Looks neat, but I'm honestly not a big fan of how you're handling conditions for getting new targets for the chain as it's pretty limited, what if someone wants its own chain spell not to hit magic-immune units, for instance? I'd use what you have right now as a basic condition setter and give the user the option to provide its own check as a function in case they want to do something more complex with it.
Also, still on magic-immune units, I may be wrong but since you're using Magic attack type doesn't that mean that magic-immune units will be counted as valid targets for the chain but then be unaffected by it, potentially making the chain spell worse than it should be by skipping over some other target that could actually be affected?

You might also code the check in such a way that "g", the group you're using for the "Enum", never gets populated, thus having a *very* slight performance boost, but that's just a nitpick and probably not even worth bothering with it.

As far as your actual problems go, I'm so rusty with code atm that I won't bother trying and failing to help and just leave it to Purge.

EDIT: Shouldn't those out-of-struct functions be private? I'm not too familiar with Zinc so I don't know how it handles those.
 
Last edited:
Level 3
Joined
Aug 12, 2010
Messages
29
To my understanding, they're private by default. I did some more to it, like giving a setting to allow a user to ignore or allow the chain to target a immune. The damage type can be modified when setting up.

I'm defiently going to create a Filter setting so players can determine more of their own settings, or I'll just add more to the premade filters I already have.

(like ignore ancient, or heroes, etc)

JASS:
function onAction(){
       Chain this = Chain.create();
       unit u = GetTriggerUnit(),t = GetSpellTargetUnit();
       player p = GetOwningPlayer(u);
       this.Chain_Caster = u;
       this.Chain_Target = t;
       this.Chain_Owner = p;
       //Skipped chain effects, for an example that they're not needed to function.
       this.Chain_Impact_Effect = "Abilities\\Spells\\Other\\Incinerate\\FireLordDeathExplode.mdl";
       this.Chain_Impact_Attachment = "origin";
       this.Chain_Max_Bounce = 4;
       this.Chain_Radius = 600.0;
       this.Chain_Damage = 120.0;
       this.Chain_Attacktype = ATTACK_TYPE_NORMAL;
       this.Chain_Damagetype = DAMAGE_TYPE_FIRE;
       this.Chain_Interval = 0.33;
       this.Chain_Hits_Enemy = true;
       this.Chain_Hits_Immune = true;
       this.Chain_OnHit = function onImpact;
       this.LaunchChain(this);
   }
 
Level 13
Joined
Jun 23, 2009
Messages
297
The damage type can be modified when setting up.
Oh, right, struct...
I'm defiently going to create a Filter setting so players can determine more of their own settings, or I'll just add more to the premade filters I already have.

(like ignore ancient, or heroes, etc)
I still suggest going for the former rather than the latter, less work for you, more flexibility for the user.
 
Level 3
Joined
Aug 12, 2010
Messages
29
I could probably add a setting something like boolexpr Chain_Target_Filter and during the new unit check, I'd just leave in the basic enemy or friendly and give them control of any other filters they need.

Edit*
JASS:
               GroupEnumUnitsInRange(g,this.Chain_RadiusX,this.Chain_RadiusY,this.Chain_Radius,this.Chain_Target_Filter);
               this.Chain_Target=FirstOfGroup(g);
               while (this.Chain_Target!=null){
                   if (!IsUnitType(this.Chain_Target,UNIT_TYPE_DEAD)
                   && GetUnitTypeId(this.Chain_Target)>0
                   && !IsUnitInGroup(this.Chain_Target,this.Chain_Hit_Group)){
                       if (this.Chain_Hits_Enemy
                       && IsUnitEnemy(this.Chain_Target,this.Chain_Owner)){
                           break;
                       }else if (this.Chain_Hits_Friendly
                       && IsUnitAlly(this.Chain_Target,this.Chain_Owner)){
                           break;
                       }
                   }
                   GroupRemoveUnit(g,this.Chain_Target);
                   this.Chain_Target=FirstOfGroup(g);
               }
How the unit check looks now.

and here's an example on how you would use it. This example ability will not target heroic units, magic immune, flying, mechanical or structures.

JASS:
/*
//All usable variables that can be used.

this.Chain_Caster <- Casting unit of the chain
this.Chain_Target <- Current target of a chain.

this.Chain_Owner <- Owning player of the Chain_Caster.

//Damage effects
//Using "None" in place of an effect will not spawn a chain or display an effect.
this.Chain_Damage_Primary_Effect    <- The lightning effect that is displayed during the first cast of a chain.
this.Chain_Damage_Secondary_Effect  <- The lightning effect that is displayed during the bounces of a chain.
this.Chain_Damage_Impact_Effect     <- The specialeffect that is displayed when a unit is hit by a chain.
this.Chain_Damage_Impact_Attachment <- The attachment point of the Damage_Impact_Effect.
//Healing effects
//Does the same as above, but triggers during healing.
this.Chain_Heal_Primary_Effect
this.Chain_Damage_Secondary_Effect
this.Chain_Damage_Impact_Effect   
this.Chain_Damage_Impact_Attachment

this.Chain_Max_Bounce <- How many bounces the chain will do before ending.

//Colors
this.Chain_Color_Red   <-Determine what RGB color the chain will be.
this.Chain_Color_Green
this.Chain_Color_Blue

this.Chain_Radius         <- The search area for locating a new target for the chain.
this.Chain_Interval       <- How often the chain will search for a new target.
this.Chain_Duration       <- How long a chain effect will last before being destroyed.
this.Chain_Damage         <- How much damage is dealt when a unit is hit by a chain.
this.Chain_Heal           <- How much life is healed when a unit is hit by a chain.
this.Chain_Modify_Damage  <- Determine if damage is increased or decreased on every bounce. A negative value will decrease and a positive will increase.
this.Chain_Modify_Heal    <- Same as Modify_Damage.

this.Chain_Hits_Friendly <- Determine if the chain can target a friendly unit.
this.Chain_Hits_Enemy    <- Determine if the chain can target an enemy unit.

this.Chain_Target_Filter <- Further add additional filtering for the new target search.

this.Chain_Attacktype <- Modify what type of attack type the chain will deal.
this.Chain_Damagetype <- Modify what type of damage type the chain will be.

this.Chain_OnHit <- Fire a function whenever a chain hits a unit.

Chain_LastCasterUnit <- Return the caster unit of the last triggering chain.
Chain_LastHitUnit    <- Return the last hit unit of a chain.
Chain_LastDamage     <- Return the last damage dealt during a chain.
Chain_LastHeal       <- Return the last healing received during a chain.
*/
//! zinc
library ChainSys requires TimerUtils, optional Damage,optional GroupUtils{

   private{
       //Determines the height of the Chain.
       constant real CHAIN_OFFSET   = 65.0;
       //A default setting on how fast the chain will search for a new target. This can be modified with Chain_Interval.
       constant real CHAIN_INTERVAL = 0.15;
       //A default setting on how long a chain effect will last before being removed.
       constant real CHAIN_DURATION = 0.55;
   }

   private{
       location LocationZ=Location(0,0);
   }

   public{
       //All available lightning effects within Warcraft3.
       constant string LIGHTNING_CHAIN_LIGHTNING_PRIMARY   = "CLPB";
       constant string LIGHTNING_CHAIN_LIGHTNING_SECONDARY = "CLSB";
       constant string LIGHTNING_DRAIN                     = "DRAB";
       constant string LIGHTNING_DRAIN_LIFE                = "DRAL";
       constant string LIGHTNING_DRAIN_MANA                = "DRAM";
       constant string LIGHTNING_FINGER_OF_DEATH           = "AFOD";
       constant string LIGHTNING_FORKED_LIGHTNING          = "FORK";
       constant string LIGHTNING_HEALING_WAVE_PRIMARY      = "HWPB";
       constant string LIGHTNING_HEALING_WAVE_SECONDARY    = "HWSB";
       constant string LIGHTNING_LIGHTNING_ATTACK          = "CHIM";
       constant string LIGHTNING_MAGIC_LEASH               = "LEAS";
       constant string LIGHTNING_MANA_BURN                 = "MBUR";
       constant string LIGHTNING_MANA_FLARE                = "MFPB";
       constant string LIGHTNING_SPIRIT_LINK               = "SPLK";
   }
   
   public{
       //Returns the last unit that casted a chain.
       unit Chain_LastCasterUnit;
       //Returns the last unit hit by a chain.
       unit Chain_LastHitUnit;

       //Returns the last damage dealt by a chain.
       real Chain_LastDamage;
       //Returns the last heal received by a chain.
       real Chain_LastHeal;
   }

   function GetLocZ(real x,real y)->real{
       MoveLocation(LocationZ,x,y);
       return GetLocationZ(LocationZ);
   }

   struct ChainEffect{
       lightning Chain_Lightning;
   }

   function onTimer(){
       timer t = GetExpiredTimer();
       ChainEffect this = GetTimerData(t);
       DestroyLightning(this.Chain_Lightning);
       this.destroy();
   }

   function AddLightningTimed(string effectId,unit source,unit target,real duration,real red,real green,real blue){
       ChainEffect this = ChainEffect.create();
       timer t = NewTimer();
       real sourceX = GetUnitX(source),sourceY = GetUnitY(source),sourceH = GetUnitFlyHeight(source),sourceZ = GetLocZ(sourceX,sourceY);
       real targetX = GetUnitX(target),targetY = GetUnitY(target),targetH = GetUnitFlyHeight(target),targetZ = GetLocZ(targetX,targetY);
       this.Chain_Lightning=AddLightningEx(effectId,true,
       sourceX,sourceY,sourceZ+CHAIN_OFFSET+sourceH,
       targetX,targetY,targetZ+CHAIN_OFFSET+targetH);
       SetLightningColor(this.Chain_Lightning,red,green,blue,1.0);
       SetTimerData(t,this);
       TimerStart(t,duration,false,function onTimer);
       t = null;
   }

   public struct Chain{
       unit Chain_Caster,Chain_Source,Chain_Target;

       player Chain_Owner;

       //Primary effects
       string Chain_Damage_Primary_Effect;
       string Chain_Heal_Primary_Effect;
       //Secondary effects
       string Chain_Damage_Secondary_Effect;
       string Chain_Heal_Secondary_Effect;
       //Impact effects
       string Chain_Damage_Impact_Effect;
       string Chain_Heal_Impact_Effect;
       //Attachment effects
       string Chain_Damage_Impact_Attachment;
       string Chain_Heal_Impact_Attachment;

       integer Chain_Max_Bounce;

       real Chain_Color_Red   = 1.0;
       real Chain_Color_Green = 1.0;
       real Chain_Color_Blue  = 1.0;
       real Chain_Radius;
       real Chain_Interval    = CHAIN_INTERVAL;
       real Chain_Duration    = CHAIN_DURATION;
       real Chain_Damage;
       real Chain_Heal;
       real Chain_Modify_Value;
       private real Chain_RadiusX,Chain_RadiusY;

       boolean Chain_Hits_Friendly  = false;
       boolean Chain_Hits_Enemy     = false;
       
       boolexpr Chain_Target_Filter = null;

       attacktype Chain_Attacktype = ATTACK_TYPE_MAGIC;
       damagetype Chain_Damagetype = DAMAGE_TYPE_NORMAL;

       static code Chain_OnHit;
       
       private{
           group Chain_Hit_Group;
           integer Chain_Tick = -1;
       }

       method destroy(){
           static if (LIBRARY_GroupUtils){
               ReleaseGroup(this.Chain_Hit_Group);
           }else{
               RemoveGroup(this.Chain_Hit_Group);
           }
           DestroyBoolExpr(this.Chain_Target_Filter);
           this.Chain_Tick = -1;
           this.deallocate();
       }

       static method onTimer(){
           timer t = GetExpiredTimer();
           thistype this = GetTimerData(t);
           group g;
           trigger e;
           string s;
           
           static if (LIBRARY_GroupUtils){
               g = NewGroup();
           }else{
               g = CreateGroup();
           }
           SetTimerData(t,this);
           TimerStart(t,this.Chain_Interval,true,function thistype.onTimer);
           GroupAddUnit(this.Chain_Hit_Group,this.Chain_Target);
           this.Chain_Tick+=1;
           if (this.Chain_Target==null
           || this.Chain_Tick>this.Chain_Max_Bounce){
               ReleaseTimer(t);
               this.destroy();
           }else{
               if (this.Chain_Tick==0){
                   if (this.Chain_Damage_Primary_Effect!="none"||this.Chain_Heal_Primary_Effect!="none"){
                       if (!IsUnitAlly(this.Chain_Target,this.Chain_Owner)){
                           s = this.Chain_Damage_Primary_Effect;
                       }else{
                           s = this.Chain_Heal_Primary_Effect;
                       }
                       AddLightningTimed(s,this.Chain_Source,this.Chain_Target,this.Chain_Interval,this.Chain_Color_Red,this.Chain_Color_Green,this.Chain_Color_Blue);
                   }
               }else{
                   if (!IsUnitAlly(this.Chain_Target,this.Chain_Owner)){
                       s = this.Chain_Damage_Secondary_Effect;
                   }else{
                       s = this.Chain_Heal_Secondary_Effect;
                   }
                   if (this.Chain_Damage_Secondary_Effect!="none"||this.Chain_Heal_Secondary_Effect!="none"){
                       AddLightningTimed(s,this.Chain_Source,this.Chain_Target,this.Chain_Interval,this.Chain_Color_Red,this.Chain_Color_Green,this.Chain_Color_Blue);
                   }
               }
               if (this.Chain_Damage>0.0){
                   if (!IsUnitAlly(this.Chain_Target,this.Chain_Owner)){
                       static if (LIBRARY_Damage){
                           UnitDamageTargetEx(this.Chain_Caster,this.Chain_Target,this.Chain_Damage,false,false,this.Chain_Attacktype,this.Chain_Damagetype,WEAPON_TYPE_WHOKNOWS);
                       }else{
                           UnitDamageTarget(this.Chain_Caster,this.Chain_Target,this.Chain_Damage,false,false,this.Chain_Attacktype,this.Chain_Damagetype,WEAPON_TYPE_WHOKNOWS);   
                       }
                       Chain_LastDamage = this.Chain_Damage;
                       this.Chain_Damage+=this.Chain_Damage*this.Chain_Modify_Value;
                       DestroyEffect(AddSpecialEffectTarget(this.Chain_Damage_Impact_Effect,this.Chain_Target,this.Chain_Damage_Impact_Attachment));
                   }
               }
               if (this.Chain_Heal>0.0){
                   if (!IsUnitEnemy(this.Chain_Target,this.Chain_Owner)){
                       SetWidgetLife(this.Chain_Target,GetWidgetLife(this.Chain_Target)+this.Chain_Heal);
                       Chain_LastHeal = this.Chain_Heal;
                       this.Chain_Heal+=this.Chain_Heal*this.Chain_Modify_Value;
                       DestroyEffect(AddSpecialEffectTarget(this.Chain_Heal_Impact_Effect,this.Chain_Target,this.Chain_Heal_Impact_Attachment));
                   }
               }
               if (this.Chain_OnHit!=null){
                   Chain_LastCasterUnit = this.Chain_Caster;
                   Chain_LastHitUnit    = this.Chain_Target;
                   e = CreateTrigger();
                   TriggerAddCondition(e,Condition(this.Chain_OnHit));
                   TriggerEvaluate(e);
               }
               this.Chain_Source=this.Chain_Target;
               GroupEnumUnitsInRange(g,this.Chain_RadiusX,this.Chain_RadiusY,this.Chain_Radius,this.Chain_Target_Filter);
               this.Chain_Target=FirstOfGroup(g);
               while (this.Chain_Target!=null){
                   if (!IsUnitType(this.Chain_Target,UNIT_TYPE_DEAD)
                   && GetUnitTypeId(this.Chain_Target)>0
                   && !IsUnitInGroup(this.Chain_Target,this.Chain_Hit_Group)){
                       if (this.Chain_Hits_Enemy
                       && IsUnitEnemy(this.Chain_Target,this.Chain_Owner)){
                           break;
                       }else if (this.Chain_Hits_Friendly
                       && IsUnitAlly(this.Chain_Target,this.Chain_Owner)){
                           break;
                       }
                   }
                   GroupRemoveUnit(g,this.Chain_Target);
                   this.Chain_Target=FirstOfGroup(g);
               }   
           }
           static if (LIBRARY_GroupUtils){
               ReleaseGroup(g);
           }else{
               RemoveGroup(g);
           }
           DestroyTrigger(e);
           t = null;
           g = null;
           e = null;
       }

       static method LaunchChain(thistype this){
           timer t = NewTimer();
           this.Chain_Source = this.Chain_Caster;
           this.Chain_RadiusX = GetUnitX(this.Chain_Target);
           this.Chain_RadiusY = GetUnitY(this.Chain_Target);
           static if (LIBRARY_GroupUtils){
               this.Chain_Hit_Group = NewGroup();
           }else{
               this.Chain_Hit_Group = CreateGroup();
           }
           SetTimerData(t,this);
           TimerStart(t,0.01,true,function thistype.onTimer);
           t = null;
       }
       
       static method create()->thistype{
           Chain this=Chain.allocate();
           return this;
       }
   }
}
//! endzinc

This is the current iteration of the system.
 
Last edited:
Looking a lot better! And yeah, the way you use TriggerEvaluate in post #7 is how I meant for you to implement the onHit. :)

Few more things:
  • I would use null instead of "none" to signify that there should be no effect. And to avoid any weird bugs with that, I would make sure you initialize those strings to null, like so:
    JASS:
    string Chain_Damage_Primary_Effect = null;
    string Chain_Heal_Primary_Effect = null;
    
    // etc..
    Why? Struct instances are really just an integer (like "5") that act as an index to an array. And when you destroy that instance, the struct's variables aren't automatically nulled. So if that "5" index gets reused later, you might still have some old data stored in your struct variables. Setting some things to null by default will
  • Make sure you release the timer in function onTimer()
  • The onHit looks good, but if you want to be able to specify different onHits for different chain spells, I would do it like this:
    JASS:
    // variable in the Chain struct
    private condition onHitCondition;
    
    // ... this operator allows you to keep the API as "Chain.onHit = function blahblah"
    
    method operator onHit= takes code c returns nothing 
        set this.onHitCondition = Condition(c)
    endmethod
    
    // ...
    
    e = CreateTrigger();
    TriggerAddCondition(e, onHitCondition);
    TriggerEvaluate(e);
  • I also think this part is a little hard to read:
    JASS:
                   if (this.Chain_Tick==0){
                       if (this.Chain_Damage_Primary_Effect!="none"||this.Chain_Heal_Primary_Effect!="none"){
                           if (!IsUnitAlly(this.Chain_Target,this.Chain_Owner)){
                               s = this.Chain_Damage_Primary_Effect;
                           }else{
                               s = this.Chain_Heal_Primary_Effect;
                           }
                           AddLightningTimed(s,this.Chain_Source,this.Chain_Target,this.Chain_Interval,this.Chain_Color_Red,this.Chain_Color_Green,this.Chain_Color_Blue);
                       }
                   }else{
                       if (!IsUnitAlly(this.Chain_Target,this.Chain_Owner)){
                           s = this.Chain_Damage_Secondary_Effect;
                       }else{
                           s = this.Chain_Heal_Secondary_Effect;
                       }
                       if (this.Chain_Damage_Secondary_Effect!="none"||this.Chain_Heal_Secondary_Effect!="none"){
                           AddLightningTimed(s,this.Chain_Source,this.Chain_Target,this.Chain_Interval,this.Chain_Color_Red,this.Chain_Color_Green,this.Chain_Color_Blue);
                       }
                   }
    Here is how I would do it:
    JASS:
     // add this at the top of AddLightningTimed
    if (s == null) {
        return;
    }
    
    // ... in Chain struct
    private method effectString takes nothing returns string
        if (IsUnitAlly(this.Chain_Target, this.Chain_Owner)) {
            if (this.Chain_Tick == 0) {
                return this.Chain_Heal_Primary_Effect;
            } else {
                return this.Chain_Heal_Secondary_Effect;
            }
        }
    
        if (this.Chain_Tick == 0) {
            return this.Chain_Damage_Primary_Effect;
        }
    
        return this.Chain_Damage_Secondary_Effect;
    endmethod
    
    // ... in static method onTimer()
    s = effectString()
    AddLightningTimed(s, ...)

    sorry, I wrote the effect string method a little ugly but it is nice to separate that logic into a separate method so that your onTimer method is more readable. But totally optional. :) A lot of people don't do this because there is some overhead to calling separate functions in JASS (especially on a repeating timer), so it is okay if you do it all inline. I like putting things into clear functions though
 
Level 3
Joined
Aug 12, 2010
Messages
29
What i noticed mainly with the chain_tick is that if I use a bjdebugmsg to display what the tick is at, the second cast will be what the previous cast ended at, for example the tick ended at 5, it'll start counting from 5 on the next cast.

Another thing, if I use condition as a variable condition onHit it will synax error as unknown.
 
Level 13
Joined
Jun 23, 2009
Messages
297
What i noticed mainly with the chain_tick is that if I use a bjdebugmsg to display what the tick is at, the second cast will be what the previous cast ended at, for example the tick ended at 5, it'll start counting from 5 on the next cast.
Might be because you're resetting it (and destroying it soon after) way after you've attached the value to a timer (and in another function) so new timer instances still point to the old value due to TimerUtils's recycling, that would be my guess. Even if my guess is off setting it to -1 in static method LaunchChain instead should be way more reliable (and that way you could even leave it unspecified at the start).

...or you could pass the timer to the destroy method before releasing it and setting the chain_tick from both the timer's data and the struct there, but that's stupid. :D
 
Last edited:
What i noticed mainly with the chain_tick is that if I use a bjdebugmsg to display what the tick is at, the second cast will be what the previous cast ended at, for example the tick ended at 5, it'll start counting from 5 on the next cast.

Another thing, if I use condition as a variable condition onHit it will synax error as unknown.

for the first part, I also agree with michael peppers. Try setting the tick to -1 in launch chain.

for the second part, that's my fault. apparently its called conditionfunc, so try using that instead
 
Status
Not open for further replies.
Top