HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Simple Ward Spell, opinion/suggestion for completed code

06-15-2009, 10:31 PM#1
Flame_Phoenix
Hi guys, I made a spell, a ward that raises dead bodies from the ground. Sure I could've used the Necro ability, but if I can code something and make it look better, with less object editor data, then why not ?

I am looking for suggestions on how to improve the code ... and the effect ! =P
This will be used for a caster, so I took special care with the efficiency (big battles -> mass wards).

Collapse JASS:
//===========================================================================
//Summons a ward that will summon a skelly from a corpse every 2 seconds. The ward
//can also carry an ability, which allows him to summon stronger skellies, depending
//on it's level. A ward can only summon 1 unit at a time.
//
//@Requires:
// - TimedLoop
// - TimedHandles
//
//@author Flame_Phoenix 
//
//@Credits:
// - Vexorian, for TimedLoop
// - TriggerHappy187, for TimedHandles
// - Spec, for helping a lot on the making of the spell
// - 0zyx0, for a cool suggestion on how to improve speed
//
//@version 1.5.1
//===========================================================================
scope SoulWard2 initializer Init
    private keyword skelies //needed for core, do not change
//===========================================================================
//=============================SETUP START===================================
//===========================================================================
    globals
        private constant integer WARD_ID = 'o00H'   //id of the ward
        private constant integer WARD_ABI = 'A01F'  //ability that tells us the level of the ward
        private constant string BOLT = "AFOD"   //lightning effect
        private constant string SUMMON_EFF = "Abilities\\Spells\\Undead\\RaiseSkeletonWarrior\\RaiseSkeleton.mdl"   //summon effect
        private constant real COOLDOWN = 2.    //cooldown between rays of summoning
    endglobals
    
    private constant function Radius takes integer level returns real
    //the radius of the ward
        return 450.
    endfunction
    
    private constant function SkelyDuration takes integer level returns real
    //amount of time each skely lasts
        return 30.
    endfunction
    
    private function SetSkelies takes nothing returns nothing
    //skelies that can be summoned by the ward. The number in the array is the
    //level they ar asociated with
        set skelies[1] = 'nsko' //skeletal orc
        set skelies[2] = 'nsoc' //skeletal orc champion
    endfunction
    
    private function PickVictims takes unit ward, unit victim returns boolean
    //the units the ward can affect
        return (GetWidgetLife(victim) < 0.405) and (IsUnitTypeDummy(victim)==false) and (IsUnitTypeWard(victim) == false) 
    endfunction
//===========================================================================
//=============================SETUP END=====================================
//===========================================================================
    globals
        private integer array skelies
        private group wards
        private boolexpr chooseWards
        private group dead
        private boolexpr chooseDead
        private unit tmpWard
    endglobals
//===========================================================================    
    private function PickDead takes nothing returns boolean
        return PickVictims(tmpWard, GetFilterUnit())
    endfunction
//===========================================================================
    private function PickWards takes nothing returns boolean
        return GetUnitTypeId(GetFilterUnit()) == WARD_ID
    endfunction
//===========================================================================
    private struct aSoulWard
        unit theWard
        real wardX
        real wardY
        real counter
        
        //method required by TimedLoop 
        private method onTimedLoop takes nothing returns boolean            
            local integer wardLevel
            local unit aVictim
            local real vicX
            local real vicY
            local unit skely
            
            //if the ward is dead, there is no need to go on, we just clean all mess
            //TimedLoop we call onDestroy for us!
            if GetWidgetLife(.theWard) <= 0.405 then
                return TimedLoop_STOP
            endif
            
            //If it is time to summon skelies, we summon them. If it is not time yet, we simply 
            //increase the counter!
            if .counter >= COOLDOWN then 
                //time to pick dead units near the current ward
                set wardLevel = GetUnitAbilityLevel(.theWard, WARD_ABI)
                set tmpWard = .theWard
                call GroupEnumUnitsInRange(dead, .wardX, .wardY, Radius(wardLevel), chooseDead)
                    
                //if the first unit of the group is null, then it means the group has 0 units, and 
                //it means we don't need to do anything at all xD
                if FirstOfGroup(dead) != null then
                    set aVictim = GroupPickRandomUnit(dead)
                    set vicX = GetUnitX(aVictim)
                    set vicY = GetUnitY(aVictim)
                        
                    //create the bolt and the skelie!
                    call DestroyLightningDelayed(AddLightningEx(BOLT, false, .wardX, .wardY, 50., vicX, vicY, -1.), 1.)
                    set skely = CreateUnit(GetOwningPlayer(.theWard), skelies[wardLevel], vicX, vicY, 0.)
                    call SetUnitAnimation(skely, "birth")
                    call UnitApplyTimedLife(skely, 'BTLF', SkelyDuration(wardLevel))
            
                    //a nice effect
                    call DestroyEffect(AddSpecialEffect(SUMMON_EFF, vicX, vicY))
                        
                    //remove the body of the victim
                    call ShowUnit(aVictim, false)
                    call RemoveUnit(aVictim)
                    
                    //cleaning up the group for the next ward =D
                    call GroupClear(dead)
                endif
               
                //now that we did our job in the correct time, we reset the timer, for the cooldown to start
                //again once more =D
                set .counter = 0
                
                //cleaning our skely
                set skely = null
            else
                set .counter = .counter + TimedLoop_PERIOD
            endif
            
            //if everything was Ok, we just keep going ...
            return TimedLoop_CONTINUE
        endmethod
        
        implement TimedLoop //This does the module magic

        static method create takes unit ward returns aSoulWard
            local aSoulWard sw = aSoulWard.allocate()
            
            set sw.theWard = ward
            set sw.wardX = GetUnitX(ward)
            set sw.wardY = GetUnitY(ward)
            set sw.counter = 0
            
            //The TimedLoop script works by
            // creating a startTimedLoop method that will
            // do all the dirty work and end up calling
            // .onTimedLoop...
            call sw.startTimedLoop()
            
            return sw
        endmethod
    endstruct
//===========================================================================
    private function Conditions takes nothing returns boolean
        local aSoulWard ward
        
        if GetUnitTypeId(GetSummonedUnit()) == WARD_ID then
           set ward = aSoulWard.create(GetSummonedUnit())
        endif
        
        return false
    endfunction
//===========================================================================
    private function Init takes nothing returns nothing
        local trigger SoulWardTrg = CreateTrigger(  )
        call TriggerRegisterAnyUnitEventBJ(SoulWardTrg, EVENT_PLAYER_UNIT_SUMMON )
        call TriggerAddCondition(SoulWardTrg, Condition(function Conditions))

        //setting globals
        set wards = CreateGroup()
        set chooseWards = Condition(function PickWards)
        set dead = CreateGroup()
        set chooseDead = Condition(function PickDead)
        
        call SetSkelies()
        
        //Preloading effects
        call Preload(SUMMON_EFF)
    endfunction
endscope

Btw, is there a way to remove a corpse without using RemoveUnit ???
06-16-2009, 06:59 AM#2
0zyx0
You don't use any TSA within the actions, so they could be conditions instead, to improve speed. You should also avoid using CountUnitsInGroup(). Since the group is not needed for a longer time, you can use FirstOfGroup(dead) != null.
06-16-2009, 08:11 AM#3
Spec
I think, the algorithm is not fully correct. Let's say, hero summoned two wards at 0.0 and 1.5 seconds of game time. They shouldn't trigger simultaneously (first ward should trigger at 2.0 - 4.0 - 6.0 sec, second ward should trigger at 3.5 - 5.5 - 7.5 sec), but, in your case, they will. I hope you got what I mean. How to fix?
1. Use single timer loop with tiny period.
2. Use new timer for each ward. (use, e.g., TimerUtils).

Now about code efficiency...
Using CountUnitsInGroup() smells a little bit lame, especially when you can count them in filter function:

Collapse JASS:
private function PickDead takes nothing returns boolean
  if PickVictims(tmpWard, GetFilterUnit()) then
    set temp_i = temp_i + 1
    return true
  endif
  return false
endfunction
// . . .
set temp_i = 0
call GroupEnumUnitsInRange(dead, GetUnitX(aWard), GetUnitY(aWard), Radius(wardLevel), chooseDead)
if temp_i > 0 then
// . . .
endif

You can go further and put all picking code into filter functions.

What's wrong with removing corpses with RemoveUnit()?
06-16-2009, 09:35 AM#4
Flame_Phoenix
Quote:
You don't use any TSA within the actions, so they could be conditions instead, to improve speed.
According to Cohadar, if you place big chunks of code inside a Condition to run, you will eventually get inefficiency problems.
Quote:
You should also avoid using CountUnitsInGroup(). Since the group is not needed for a longer time, you can use FirstOfGroup(dead) != null.
Good idea, I wonder I didn't think it xD
Quote:
I think, the algorithm is not fully correct. Let's say, hero summoned two wards at 0.0 and 1.5 seconds of game time. They shouldn't trigger simultaneously (first ward should trigger at 2.0 - 4.0 - 6.0 sec, second ward should trigger at 3.5 - 5.5 - 7.5 sec), but, in your case, they will. I hope you got what I mean.
I know what you mean. In fact I tested the map and I saw it happening. Having in mind this is for a ladder map I thought "no problem, it is Ok" but you are probably right, I wonder if I should change that.
Quote:
How to fix?
I know I can fix it using 1 timer per instance .... but in a ladder map, with 8 players, if each team has 10 casters (not a big number) and if each caster uses the spell 1 time (which is ridiculous, in battles with over 200 units, each caster will cast the ward 2-3 times...) I will have 80 timers ... which is a pain in the ass !
So my main idea would be to use a stack for the instances and 1 timer I guess .... however I had bad experiences with this method in the past, and I do not master it's use as I would like to =S

Quote:
Now about code efficiency...
Using CountUnitsInGroup() smells a little bit lame, especially when you can count them in filter function:
Anitarf once suggested me a similar idea, I never understood his intention .. until now. But I don't understand, won't that global variable make make code not MUI ?

Quote:
You can go further and put all picking code into filter functions.
Nah, I think it is easier to read this way xD
Quote:
What's wrong with removing corpses with RemoveUnit()?
Remove unit is known for giving trouble with some systems. I believe it's use was even banned from system making because the native in cause has many problems ...
So the main idea is to avoid it whenever possible.
I can create a thread about it, if you find necessary.

EDIT EDIT

Code updated to 1.4.3, now it applies 0zyx0 suggestion.
rep++ for 0zyx0 and Spec for their suggestions and for being so awesome =P
06-16-2009, 11:35 AM#5
fX_
Quote:
when a unit dies
so id just make an on-death trigger that:
detects wards in range of the dying unit (and determines which ward should raise the unit if this is an issue... small details...);
raises the corpse;

...instead of a regular corpse check + monitor all the wards + index them etc

something like:
Collapse JASS:
function filter returns boolaen
   return canUnitBeRaised
endfunction

function wardFilter returns boolean
   return isThisAWard and isItSuitable
endfunction
function raise
   local group wards = CreateGroup()
   call GroupEnumUnitsInRange(wards, *maxWardRange(cozThisMightChangePerLevelOfTheWardAbility), function filter)
   //pick which ward should raise the unit (the closest one? the oldest one? the highest-level one? etc)
   //raise the unit

   //flush
endfunction

function Init
local trigger t = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_DEATH)
call TriggerAddCondition(t, Condition(filter)
call TriggerAddAction(t, raise)
set t = null
endfunction
06-16-2009, 11:36 AM#6
Spec
Quote:
I know I can fix it using 1 timer per instance .... but in a ladder map, with 8 players, if each team has 10 casters (not a big number) and if each caster uses the spell 1 time (which is ridiculous, in battles with over 200 units, each caster will cast the ward 2-3 times...) I will have 80 timers ... which is a pain in the ass !
So my main idea would be to use a stack for the instances and 1 timer I guess .... however I had bad experiences with this method in the past, and I do not master it's use as I would like to =S

As far as I know, moderators require users' scripts to be maximally modular (and efficient too, of course). TimedLoop is probably the thing you should to use.

Quote:
Anitarf once suggested me a similar idea, I never understood his intention .. until now. But I don't understand, won't that global variable make make code not MUI ?
No, sir, you are wrong. =)
It is usual temporary variable application, and it doesn't affect MUI possibility.

Quote:
Remove unit is known for giving trouble with some systems.
I guess, the troubles are called "phantom handles"? I've found similar problem in Griffen's thread about group.
06-16-2009, 11:43 AM#7
Flame_Phoenix
Quote:
so id just make an on-death trigger that:
detects wards in range of the dying unit (and determines which ward should raise the unit if this is an issue... small details...);
raises the corpse;
I forgot to change the description. The idea is to select corpses nearby and to create skelies, not just when a unit dies.
Quote:
As far as I know, moderators require users' scripts to be maximally modular (and efficient too, of course). TimedLoop is probably the thing you should to use.
This is an awesome new !! I didn't know such a thing existed !
I will try using right away !
Quote:
I guess, the troubles are called "phantom handles"? I've found similar problem in Griffen's thread about group.
It is always good to avoid such things =P

I will try using TimedLoop. If I succeed, you are sure to have your name on Credits Spec =D
06-16-2009, 11:58 AM#8
fX_
to raise spawned skeles too?

also, where can i look up this "phantom handle"-group thing?
06-16-2009, 01:08 PM#9
Spec
Groups - Memory Leaks and Slowdowns - actually, not stated as phantom handle issue, but it is identical.

By the way, I use "phantom handle" definition for any handle, that is referenced to non-existing (or, maybe, recreated) object.

But hidden unit is normally removed from the game, when his decay animation is finished, isn't he?
06-16-2009, 01:50 PM#10
Flame_Phoenix
Quote:
also, where can i look up this "phantom handle"-group thing?
It is not complete yet but ... :
http://www.wc3c.net/showthread.php?p...47#post1089647
Quote:
But hidden unit is normally removed from the game, when his decay animation is finished, isn't he?
I make no idea what happens to units once they die ...

Anyway, the spell was updated to version 1.5.0, it now uses TimedLoop for super efficiency!
I am open to suggestions and comments !
Thx to Spec for suggesting the system =D
06-17-2009, 02:32 PM#11
Spec
Small precision tip:
Collapse JASS:
if GetWidgetLife(.theWard) < 0.405 then
    return TimedLoop_STOP
endif

I think PickVictims() function has a lot of unneeded stuff.
As far as I know, standard structures, flying units, heroes, mechanical units, summons doesn't create corpses after death.

Collapse JASS:
    private function PickVictims takes unit ward, unit victim returns boolean
    //the units the ward can affect
        return (GetWidgetLife(victim) < 0.405) and (IsUnitTypeDummy(victim)==false) and (IsUnitTypeWard(victim)==false)
    endfunction

Oh, and I don't see IsUnitTypeDummy and IsUnitTypeWard functions... I guess, you just haven't wrote them in your post.
06-17-2009, 06:31 PM#12
Flame_Phoenix
Quote:
Small precision tip:
Right.
Quote:
I think PickVictims() function has a lot of unneeded stuff.
Right again, I guess I forgot those details xD
Quote:
Oh, and I don't see IsUnitTypeDummy and IsUnitTypeWard functions... I guess, you just haven't wrote them in your post.
Those are functions from my project, they tell me if a unit is a Ward or a Dummy (don't want corpses from such units)... forgot to delete those functions xD

Thx you have been of great aid =D

I have, however, a small problem with TimedLoop ... you see, I have to wait .25 seconds to take care of the struct and stop it's effect. In this case it is not a problem .... but let's imagine the following case:
- I have a super channel spell with many effects
- The user is an idiot, and changes the time to 10 seconds, instead of .25

So, if my hero dies ... the effects of the spell with only die 10 seconds after!
Is there a way to fix this?
Btw, more info at:
http://www.wc3c.net/showthread.php?t=105515&page=4

I know I am spamming help, but having in mind you know a lot of TimedLoop .. =P
06-18-2009, 09:53 AM#13
Spec
Quote:
- The user is an idiot, and changes the time to 10 seconds, instead of .25
If the user is idiot, that changes period value to 10.0, then this idiot is user enough to notice something wrong and change value back to normal.

In other words - you shouldn't take care about that.
'nuff said.
06-18-2009, 12:17 PM#14
Flame_Phoenix
Quote:
'nuff said.
Amen =P