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

Hydra v4.2

Hydra


Summons a many-headed dragon, attacking nearby enemies.


216087-albums7785-picture91932.gif


Obligatory RequirementsOptional Requirements

Hydra

Code Template

JASS:
library Hydra /* v4.2
*************************************************************************************
*
*   Summons a many-headed dragon, attacking nearby enemies.
*  
*************************************************************************************
*      
*       To Vexorian
*       -----------------------
*
*           For TimerUtils, Hydra, cool vJass features and JassHelper
*
*       To Nestharus
*       -----------------------
*
*           For ErrorMessage, properly debugging data structures.
*   
*************************************************************************************
*
*   */ uses /*
*  
*       */ Missile               /* http://www.hiveworkshop.com/forums/jass-resources-412/missile-265370/
*       */ TimerUtils            /* http://www.wc3c.net/showthread.php?t=101322
*       */ optional ErrorMessage /* http://github.com/nestharus/JASS/tree/master/jass/Systems/ErrorMessage
*
************************************************************************************
*
*   1. Import instruction
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*       Copy the Hydra script and the required libraries into your map.
*       The FireSnake.mdx model is very recommended as Hydra Head model.
*
*   2. API
*   ¯¯¯¯¯¯
*       struct Hydra
*  
*       Creator/Destructor:
*
*           static method create takes unit whichUnit, integer unitId, real x, real y, real z, real face, spawnInterval returns Hydra
*           method destroy takes nothing returns nothing
*
*       Mandatory:
*
*           method operator attackAnimationTime= takes real value returns nothing
*               - duration of the attack animation for the hydra unit (object editor).
*               - Normally it's half of the animation time displayed in the object editor,
*                 as the second half is the backswing animation time.
*
*           method operator attackSpeed= takes real value returns nothing
*               - attack speed of one single hydra head.
*           
*           method operator duration= takes real time returns nothing
*               - sets the life time of this Hydra instance, once it is summoned.
*               - time values <= 0, will make the Hydra permanent, until you set
*                 a new duration or use th destructor method: this.destroy().
*  
*           method summon takes nothing returns nothing
*               - summons this instance 
*
*           real    attackPeriod       ( pause between two heads )
*           real    headHeight         ( is also missile z offset )
*           real    attackRange    
*           real    bodysize           ( by default 48.00 )
*           real    birthAnimationTime ( timeout until the head will start to shoot. )
*           integer heads              ( total amount of heads )
*           boolean hasArcOrCurve      ( MUST be set, otherwise the Missile fly incorrect! )  
*
*       Optional:
*
*           method addHead takes real x, real y, real z, real face returns boolean
*               - summons a new head to the Hydra.
*               - only works if the Hydra is at least one head already exists.
*               - does not reset the duration.
*
*           method operator scale= takes real value returns nothing
*               - sets the scale of each hydra units. Runs for the entire hydra head list.
*               - can be updated at any time.
*
*           method operator scale takes nothing returns real
*               - gets the current scaling of each head.
*
*           method operator offset= takes real value returns nothing
*               - by default this value is 0.
*               - Each missile can be create with an offset towards the shooting unit.
*               - offset is automatically affect by the scale operator. Ergo scaling, also scales the offset.
*
*           method optimize takes nothing returns nothing
*               - optimizes attack period and animation, based on the attack speed.
*
*       Fields:
*
*           readonly unit    source 
*           readonly player  owner 
*           readonly integer typeId 
*           readonly real    posX          
*           readonly real    posY
*           readonly real    posZ
*                 
*       Safety:
*           readonly boolean summoned
*
*       Extending structs available stub methods:
*   
*           public stub method onSummonHead takes unit head returns nothing
*               - runs each time a new head is summoned
*
*           public stub method onRemoveHead takes unit head returns nothing
*               - runs each time a head is removed.
*
*           public stub method onTargetFilter takes unit target, player owner returns boolean
*               - runs each time a target filter is applied
*               by default: return UnitAlive(target) and IsUnitEnemy(target, owner)
*               

*
*           public stub method onFire takes unit shooter, unit target, Missile missile returns nothing
*               - runs each time a new missile is created.
*               by default: call missile.terminate()
*
*/

// Hydra Code. Changes may causes errors.
    native UnitAlive takes unit id returns boolean

    // Toogles unit indexer systems in the best way possible.
    // Considers the previous library setup ( enabled or not )
    private function ToogleUnitIndexer takes boolean enable returns boolean 
        local boolean prevSetup = true
        static if LIBRARY_UnitIndexer then
            set prevSetup = UnitIndexer.enabled
            set UnitIndexer.enabled = enable
        elseif LIBRARY_UnitIndexerGUI then
            set prevSetup = udg_UnitIndexerEnabled
            set udg_UnitIndexerEnabled = enable
        elseif LIBRARY_UnitDex then
            set prevSetup = UnitDex.Enabled
            set UnitDex.Enabled = enable
        endif
        return prevSetup
    endfunction
    
    // Data structure; complexity: List
    private keyword HydraStructure
    private struct HydraHead extends array
        implement HydraStructure
        unit unit
        unit target 
        real x// Hydra position.
        real y
        real z//
        real unitZ
        real lastShot // Hydra uses a timer stamp, stores elapsed time on lastShot.
        integer number// Head number.
        
        real aimX
        real aimY
        real aimZ
        real aimAngle
    endstruct
    
    globals
        // Constants required in Hydra.
        private constant real    TWO_PI           = bj_PI*2
        private constant real    DEFAULT_BODYSIZE = 48.
        private constant integer LOCUST           = 'Aloc'
        private constant integer TIMED_LIFE       = 'BTLF' 
        private constant integer CROW_FORM        = 'Amrf'
        private constant string  ATTACK           = "attack "
        private constant string  BIRTH            = "birth "
        private constant string  STAND            = "stand "
        private constant timer   STAMP            = CreateTimer()
        // Available animationSuffixes. Global for ever unit type.
        private string   array animationSuffix 
        
        // Tracks how many Hydra instances are allocated
        private integer allocCount = 0 
    endglobals
    
    struct Hydra
        //grp for a better target evaluation.
        private static group enu = CreateGroup()
        private static group grp = CreateGroup()
    
        private HydraHead list
        private HydraHead currentHead
        private HydraHead nextShootingHead
        
        private timer shoot    
        private timer lifeTimer  
    
        readonly unit    source 
        readonly player  owner 
        readonly integer typeId 
        readonly boolean summoned
        readonly real    posX          
        readonly real    posY
        readonly real    posZ
        private  real    angle
        private  real    timeScale
        private  real    interval
                 real    headHeight// is also the missile offset.
                 real    attackSpeed        
                 real    attackRange
                 real    bodysize
                 real    birthAnimationTime
                 integer heads  
                 boolean hasArcOrCurve

        // Available stub methods.
        public stub method onSummonHead takes unit head returns nothing
        endmethod
        public stub method onRemoveHead takes unit head returns nothing
        endmethod
        public stub method onTargetFilter takes unit target, player owner returns boolean
            return UnitAlive(target) and IsUnitEnemy(target, owner)
        endmethod
        public stub method onFire takes unit shooter, unit target, Missile missile returns nothing
            call missile.terminate()
        endmethod

        private real attackPeriod_p
        private real animationTime_p
        private real animationTimeOriginal
        method operator attackAnimationTime= takes real value returns nothing
            set animationTimeOriginal = value
            set animationTime_p       = value
            if (0. != attackPeriod_p and attackPeriod_p < value) then
                set timeScale         = value/attackPeriod_p
                set animationTime_p   = attackPeriod_p
            endif
        endmethod

        method operator attackPeriod= takes real value returns nothing
            set attackPeriod_p      = value 
            if (value < animationTime_p and animationTimeOriginal != 0.) then
                set timeScale       = animationTimeOriginal/value 
                set animationTime_p = value
            endif
        endmethod
        
        private real duration_p
        method operator duration= takes real time returns nothing
            if (0. <= time) then
                call PauseTimer(lifeTimer)
            elseif (summoned) and (list.size != 0) then
                call TimerStart(lifeTimer, time, false, function thistype.killHead)
            endif
            set duration_p = time
        endmethod
        
        private real scale_p
        method operator scale= takes real value returns nothing
            local HydraHead node
            if (summoned) then
                set node = list.first
                loop 
                    exitwhen node == 0
                    call SetUnitScale(node.unit, value, 0, 0)
                    set node.unitZ = node.z + headHeight*value
                    set node = node.next
                endloop
            endif
            set offset_p = offset_p*value
            set scale_p  = value
        endmethod
        
        method operator scale takes nothing returns real
            return scale_p
        endmethod
        
        private real offset_p
        method operator offset= takes real value returns nothing
            set offset_p = value*scale
        endmethod
        
        method optimize takes nothing returns nothing
            static if LIBRARY_ErrorMessage then
                debug call ThrowError((heads <= 0),                 "Hydra", "optimize", "heads",         this, "Can't use optimize() before heads are set above 0!")
                debug call ThrowError((attackPeriod_p <= 0),        "Hydra", "optimize", "attackPeriod",  this, "Can't use optimize() before attackPeriod is set!")
                debug call ThrowError((attackSpeed <= 0),           "Hydra", "optimize", "attackSpeed",   this, "Can't use optimize() before attackSpeed is set!")
                debug call ThrowError((animationTimeOriginal <= 0), "Hydra", "optimize", "animationTime", this, "Can't use optimize() before attackAnimationTime is set!")
            endif
            
            set attackPeriod = (attackSpeed/heads)
        endmethod
        
        method destroy takes nothing returns nothing
            local HydraHead head = list.first

            call ReleaseTimer(lifeTimer)
            call ReleaseTimer(shoot)
            loop
                exitwhen (0 == head)                
                if (UnitAlive(head.unit)) then
                    call onRemoveHead(head.unit)
                    call UnitApplyTimedLife(head.unit, TIMED_LIFE, 0.001)
                endif
                set head.unit   = null
                set head.target = null
                set head        = head.next
            endloop
            call list.destroy()
            
            set owner     = null            
            set shoot     = null
            set lifeTimer = null
            set source    = null
            set summoned  = false
            
            set allocCount = allocCount - 1
            if (allocCount == 0) then
                call PauseTimer(STAMP)
            endif
            call deallocate()
        endmethod
    
        // It took some time until I found the proper Z offset calculation for non arcing missiles.
        // Warcraft III terrain surface says no-no to simple geometry and congruent triangles. 
        // It is like this:
        //      1. head.unitZ == GetUnitFlyHeight(head) + headHeight*scale
        //      2. this.z     == GetUnitFlyHeight(head.target) + GetUnitBodySize(head.target)*Missile.HIT_BOX
        //      3. dZ         == LocZ(impactX, impactY) - head.UnitZ + LocZ(originX, originY)
        //      4. endZ       == head.unitZ + dZ*(maxDistance/distance) - LocZ(maxDistanceImpactX, maxDistanceImpactY)
        private static method fire takes nothing returns nothing
            local timer t        = GetExpiredTimer()
            local thistype this  = thistype(GetTimerData(t))
            local HydraHead head = currentHead
            local real sin       = Sin(head.aimAngle)
            local real cos       = Cos(head.aimAngle)
            local real ox        = head.x + offset_p*cos
            local real oy        = head.y + offset_p*sin
            local real a         = attackPeriod_p - animationTime_p
            local real d          
            local real dZ         
            local real endZ       
            local Missile missile 
            
            if (hasArcOrCurve) then
                set missile = Missile.createXYZ(ox, oy, head.unitZ, head.aimX, head.aimY, head.aimZ)
            else
                set d       = RMaxBJ(SquareRoot((ox - head.aimX)*(ox - head.aimX) + (oy - head.aimY)*(oy - head.aimY)), 1.)
                set dZ      = (Missile_GetLocZ(head.aimX, head.aimY) + head.aimZ) - (head.unitZ + Missile_GetLocZ(ox, oy))
                set endZ    = head.unitZ + (dZ*(attackRange/d) - Missile_GetLocZ(ox + attackRange*cos, oy + attackRange*sin))
                set missile = Missile.create(ox, oy, head.unitZ, head.aimAngle, attackRange, endZ)
            endif
            set missile.source = source
            set missile.owner  = owner
            set missile.data   = this
            
            // Fire stub method onFire
            call onFire(head.unit, head.target, missile)
            
            static if LIBRARY_ErrorMessage then
                debug if (hasArcOrCurve) and ((not (missile.arc != 0.)) and (not (missile.curve != 0.))) then
                    debug call ThrowWarning(true, "Hydra", "fire", "hasArcOrCurve", this, "Don't set it to true, when you don't use arced or curved missiles!")
                debug elseif not (hasArcOrCurve) and (((missile.arc != 0.) or (missile.curve != 0.))) then
                    debug call ThrowWarning(true, "Hydra", "fire", "hasArcOrCurve", this, "Arc & Curve settings are only valid if hasArcOrCurve is set to true")
                debug endif
            endif
           
            if (1. != timeScale) then
                call SetUnitTimeScale(head.unit, 1.)
            endif
            call QueueUnitAnimation(currentHead.unit ,STAND + animationSuffix[head.number])
           
            if (0. > a) then
                set a = 0.
            endif
        
            call TimerStart(t, a, false, function thistype.attemptShoot)
            set t = null
        endmethod

        /*
        *   Concept: 
        *       - Each head has an individual target, if possible.
        *       - The closest unit is the best target, since moving units are hard to hit.
        *       - The old target has the highest priority, unless the last two missiles failed.
        *         Only missiles, which hit walls or cliffs are evaluated as fails.
        *       - Returns true will initialize the shooting process.
        */
        private method evaluateTarget takes HydraHead node returns boolean
            local real dist // Distance,
            local real ndist// new distance 
            local real bdist// and best new distance.
            local unit u
            local unit new  = null    // This is the new target,
            local unit best = null    // the best new target
            local unit old  = node.target// and the old target.
            local real x
            local real y
            local real ox
            local real oy
            local HydraHead temp 
            
            // First check the previous target.
            // Evaluates stub method onTargetFilter
            if (IsUnitInRange(old, node.unit, attackRange)) and (onTargetFilter(old, owner)) then
                set old = null
                return true
            else
                set node.target = null// Clear the previous target.
                call GroupClear(thistype.grp)
                
                
                // Units targeted by the other heads of this instance, have a lower priority.
                set temp = list.first
                loop                       
                    exitwhen (0 == temp)
                    if (temp.target != null) then
                        call GroupAddUnit(thistype.grp, temp.target)
                    endif
                    set temp = temp.next
                endloop
                
                set ox = node.x
                set oy = node.y
                call GroupEnumUnitsInRange(thistype.enu, ox, oy, attackRange + Missile_MAXIMUM_COLLISION_SIZE, null)
        
                // The old target probably has an high error quote,
                // therefore it gets the lowest priority.
                call GroupRemoveUnit(thistype.enu, old)
                loop
                    set u = FirstOfGroup(thistype.enu)
                    exitwhen u == null
                    call GroupRemoveUnit(thistype.enu, u)
                    
                    if IsUnitInRange(u, node.unit, attackRange) and onTargetFilter(u, owner) then
                        set x    = GetUnitX(u)
                        set y    = GetUnitY(u)
                        set dist = ((x - ox)*(x - ox) + (y - oy)*(y - oy))//screw squareroot
                        if (new == null) or (dist < ndist) then
                            set ndist = dist
                            set new   = u
                        endif
                        
                        if not (IsUnitInGroup(u, thistype.grp)) then
                            if (best == null) or (dist < bdist) then
                                set best  = u
                                set bdist = dist
                            endif
                        endif
                    endif
                endloop
                
                if (best != null) then
                    set node.target = best
                    set best        = null
                    
                elseif (new != null) then
                    set node.target = new
                    
                elseif (onTargetFilter(old, owner) and IsUnitInRange(old, node.unit, attackRange)) then
                    set node.target = old
                endif
                set new  = null
                set old  = null
            endif

            return (node.target != null)
        endmethod
    
        private static method attemptShoot takes nothing returns nothing
            local timer t        = GetExpiredTimer()
            local thistype this  = thistype(GetTimerData(t))  
            local real elapsed   = TimerGetElapsed(STAMP)
            local HydraHead head = nextShootingHead
            local real timeout 

            if (head.lastShot + attackSpeed <= elapsed) then

                if (evaluateTarget(head)) then
                
                    set nextShootingHead = head.next
                    if (nextShootingHead == 0) then
                        set nextShootingHead = list.first
                    endif
                    
                    // Taking aim, before the shoot animation starts.
                    set head.aimX     = GetUnitX(head.target)
                    set head.aimY     = GetUnitY(head.target)       
                    set head.aimZ     = GetUnitFlyHeight(head.target) + GetUnitBodySize(head.target)*Missile.HIT_BOX
                    set head.aimAngle = Atan2(head.aimY - head.y, head.aimX - head.x)
                    set currentHead   = head
                    set head.lastShot = elapsed 
                
                    //  Initiate the shooting process.
                    call SetUnitFacing(head.unit, head.aimAngle*bj_RADTODEG)
                
                    if (1. != timeScale) then
                        call SetUnitTimeScale(head.unit, timeScale)
                    endif
                
                    call SetUnitAnimation(head.unit, ATTACK + animationSuffix[head.number])
                    call TimerStart(t, animationTime_p, false, function thistype.fire)
                    
                elseif (list.size > 1) then
                
                    // This head can't shoot, we pass in a timeout and
                    // move to the next head.
                    set head.lastShot = elapsed
                    set head          = head.next 
                    if (head == 0) then
                        set head = list.first
                    endif
                    set nextShootingHead = head
                    
                    set timeout = head.lastShot + attackSpeed - elapsed
                    if (timeout < 0) then
                        set timeout = 0.
                    elseif (timeout > attackPeriod_p) then
                        set timeout = attackPeriod_p
                    endif
                    
                    call TimerStart(t, timeout, false, function thistype.attemptShoot)
                else
                    call TimerStart(t, attackPeriod_p, false, function thistype.attemptShoot)
                endif
            else
                call TimerStart(t, RMaxBJ(0., head.lastShot + attackSpeed - elapsed), false, function thistype.attemptShoot)
            endif
            set t = null
        endmethod
        
        private static method killHead takes nothing returns nothing
            local thistype this  = thistype(GetTimerData(GetExpiredTimer()))
            local HydraHead head = list.first
            local HydraHead node = head.next
            local real time      = RMaxBJ(.0001, head.lastShot + (animationTime_p*2.) - TimerGetElapsed(STAMP))
            
            // In case this head is still in the shoot animation foreward or backwards,
            // we show that shoot animation before killing it.
            call UnitApplyTimedLife(head.unit, TIMED_LIFE, time)
            
            // Evaluate stub method onRemoveHead
            call this.onRemoveHead(head.unit)
            set head.unit = null
            call head.remove()
            if (0 == list.first) then
                call destroy()
            else
                if (nextShootingHead == head) then
                    if (node == 0) then
                        set nextShootingHead = list.first
                    else
                        set nextShootingHead = node
                    endif
                endif
                call TimerStart(lifeTimer, interval, false, function thistype.killHead)
            endif
        endmethod
        
        // Create a new head without firing off an onIndex event.
        private method addHeadEx takes real x, real y, real z, real face returns nothing
            local HydraHead node = list.enqueue()
            local string suffix  = animationSuffix[list.size]
            local boolean   prev = ToogleUnitIndexer(false)  
            local unit      head = CreateUnit(owner, typeId, x, y, face) 
            call ToogleUnitIndexer(prev)
            call SetUnitFlyHeight(head, z, 0.)
            if (1. != scale_p) then
                call SetUnitScale(head, scale_p, 1., 1.)
            endif
            call UnitAddAbility(head,     LOCUST)
            call UnitAddAbility(head,     CROW_FORM)
            call SetUnitAnimation(head,   BIRTH + suffix)
            call QueueUnitAnimation(head, STAND + suffix)
            
            set node.unit     = head        
            set node.target   = null
            set node.lastShot = TimerGetElapsed(STAMP) - attackSpeed + birthAnimationTime
            set node.x        = GetUnitX(head)
            set node.y        = GetUnitY(head)
            set node.z        = z
            set node.unitZ    = z + headHeight*scale_p
            set node.number   = list.size
                
            // Evaluate stub method onSummonHead.
            call this.onSummonHead(head)
            set head          = null
        endmethod
        
        method addHead takes real x, real y, real z, real face returns boolean
            if (summoned) and (list.size > 0) then
                call addHeadEx(x, y, z, face)
                return true
            endif
            return false
        endmethod
        
        // Periodically called until all Hydra heads are created.
        private static method spawn takes nothing returns nothing 
            local timer t       = GetExpiredTimer()
            local thistype this = thistype(GetTimerData(t))
            local real a        = angle*bj_DEGTORAD + list.size*(TWO_PI/heads)
            call addHeadEx((bodysize*scale_p)*Cos(a) + posX, (bodysize*scale_p)*Sin(a) + posY, posZ, a*bj_RADTODEG)
            
            if (list.size == 1) then
                set nextShootingHead = list.first
                call TimerStart(t, interval, true, function thistype.spawn)
            endif
            
            if (list.size >= heads) then
                call ReleaseTimer(t)
            endif
            set t = null
        endmethod
    
        method summon takes nothing returns nothing
            static if LIBRARY_ErrorMessage then
                debug call ThrowError((summoned),                     "Hydra",    "summon", "summoned",              this, "This Hydra is already summoned!")
                debug call ThrowError((typeId == 0),                  "Hydra",    "summon", "unitId",                this, "Passed in Hydra unit type id is invalid ( 0 )!")
                debug call ThrowError((heads <= 0),                   "Hydra",    "summon", "heads",                 this, "Passed in value is invalid ( <= 0 )!")
                debug call ThrowError((attackRange <= 0),             "Hydra",    "summon", "attackRange",           this, "Passed in value is invalid ( <= 0 )!")
                debug call ThrowError((attackPeriod_p <= 0),          "Hydra",    "summon", "attackPeriod",          this, "Passed in value is invalid ( <= 0 )!")
                debug call ThrowError((animationTimeOriginal <= 0),   "Hydra",    "summon", "attackAnimationTime",   this, "Passed in value is invalid ( <= 0 )!")
                debug call ThrowWarning((headHeight < 0),             "Hydra",    "summon", "headHeight",            this, "Passed in value is invalid ( below 0 )!")
                debug call ThrowWarning((GetUnitTypeId(source) == 0), "Hydra",    "summon", "GetUnitTypeId(source)", this, "Passed in unit is invalid ( null )!")
            endif
            
            if not (summoned) then
                set summoned = true
                call TimerStart(NewTimerEx(this), 0., false, function thistype.spawn)
                call TimerStart(shoot, birthAnimationTime, false, function thistype.attemptShoot)
                if (duration_p > 0.) then
                    call TimerStart(lifeTimer, duration_p, false, function thistype.killHead)
                endif
            endif
        endmethod

        static method create takes unit whichUnit, integer unitId, real x, real y, real z, real face, real spawnInterval returns Hydra 
      local thistype this      = thistype.allocate()
            set list           = HydraHead.create()
            set shoot          = NewTimerEx(this)
            set lifeTimer      = NewTimerEx(this)
            set owner          = GetOwningPlayer(whichUnit)
            
            // Set members.
            set source         = whichUnit
            set typeId         = unitId
            set posX           = x
            set posY           = y
            set posZ           = z
            set angle          = face
            set scale_p        = 1.
            set timeScale      = 1.
            set attackPeriod_p = 0.
            set offset_p       = 0.
            set duration_p     = 0.
            set animationTimeOriginal = 0.
            set birthAnimationTime = 0.
            set bodysize       = DEFAULT_BODYSIZE
            set interval       = spawnInterval
            set hasArcOrCurve  = false
            
            if (allocCount == 0) then
                call TimerStart(STAMP, 604800, false, null)
            endif
            set allocCount = allocCount + 1
                
            // Debug safety for method summon.
            debug set heads       = 0
            debug set attackSpeed = 0.
            debug set headHeight  = -1.
            debug set attackRange = 0.
            
            return this
        endmethod
    
        private static method onInit takes nothing returns nothing
            set animationSuffix[0] = "first"
            set animationSuffix[1] = "second"
            set animationSuffix[2] = "third"
            set animationSuffix[3] = "fourth"
            set animationSuffix[4] = "fifth"
        endmethod
    
    endstruct
    
    // Credits to Nestharus for a properly working
    // debug algorithm in list modules.
    private module HydraStructure 
    
        private static thistype collectionCount = 0
        private static thistype nodeCount = 0
        debug private boolean isNode
        debug private boolean isCollection
        readonly integer size 
        
        private thistype _list
        method operator list takes nothing returns thistype
            static if LIBRARY_ErrorMessage then
                debug call ThrowError(this == 0,    "List", "list", "thistype", this, "Attempted To Read Null Node.")
                debug call ThrowError(not isNode,   "List", "list", "thistype", this, "Attempted To Read Invalid Node.")
            endif
            return _list
        endmethod
        
        private thistype _next
        method operator next takes nothing returns thistype
            static if LIBRARY_ErrorMessage then
                debug call ThrowError(this == 0,    "List", "next", "thistype", this, "Attempted To Go Out Of Bounds.")
                debug call ThrowError(not isNode,   "List", "next", "thistype", this, "Attempted To Read Invalid Node.")
            endif
            return _next
        endmethod
        
        private thistype _prev
        method operator prev takes nothing returns thistype
            static if LIBRARY_ErrorMessage then
                debug call ThrowError(this == 0,    "List", "prev", "thistype", this, "Attempted To Go Out Of Bounds.")
                debug call ThrowError(not isNode,   "List", "prev", "thistype", this, "Attempted To Read Invalid Node.")
            endif
            return _prev
        endmethod
        
        private thistype _first
        method operator first takes nothing returns thistype
            static if LIBRARY_ErrorMessage then
                debug call ThrowError(this == 0,        "List", "first", "thistype", this, "Attempted To Read Null List.")
                debug call ThrowError(not isCollection, "List", "first", "thistype", this, "Attempted To Read Invalid List.")
            endif
            return _first
        endmethod
        
        private thistype _last
        method operator last takes nothing returns thistype
            static if LIBRARY_ErrorMessage then
                debug call ThrowError(this == 0,        "List", "last", "thistype", this, "Attempted To Read Null List.")
                debug call ThrowError(not isCollection, "List", "last", "thistype", this, "Attempted To Read Invalid List.")
            endif
            return _last
        endmethod
        
        private static method allocateCollection takes nothing returns thistype
            local thistype this = thistype(0)._first
            
            if (0 == this) then
                static if LIBRARY_ErrorMessage then
                    debug call ThrowError(collectionCount == 8191, "List", "allocateCollection", "thistype", 0, "Overflow.")
                endif
               
                set this = collectionCount + 1
                set collectionCount = this
            else
                set thistype(0)._first = _first
            endif
            
            return this
        endmethod
        
        private static method allocateNode takes nothing returns thistype
            local thistype this = thistype(0)._next
            
            if (0 == this) then
                
                static if LIBRARY_ErrorMessage then
                    debug call ThrowError(nodeCount == 8191, "List", "allocateNode", "thistype", 0, "Overflow.")
                endif
               
                set this = nodeCount + 1
                set nodeCount = this
            else
                set thistype(0)._next = _next
            endif
            
            return this
        endmethod
        
        static method create takes nothing returns thistype
            local thistype this = allocateCollection()
            
            debug set isCollection = true
            
            set _first = 0
            set size   = 0
            return this
        endmethod
        method enqueue takes nothing returns thistype
            local thistype node = allocateNode()
            
            static if LIBRARY_ErrorMessage then
                debug call ThrowError(this == 0,        "List", "enqueue", "thistype", this, "Attempted To Enqueue On To Null List.")
                debug call ThrowError(not isCollection, "List", "enqueue", "thistype", this, "Attempted To Enqueue On To Invalid List.")
            endif
            
            debug set node.isNode = true
            
            set node._list = this
        
            if (_first == 0) then
                set _first = node
                set _last = node
                set node._prev = 0
            else
                set _last._next = node
                set node._prev = _last
                set _last = node
            endif
            
            set node._next = 0
            set size       = size + 1
            return node
        endmethod
        method remove takes nothing returns nothing
            local thistype node = this
            set this = node._list
            
            static if LIBRARY_ErrorMessage then
                debug call ThrowError(node == 0,        "List", "remove", "thistype", this, "Attempted To Remove Null Node.")
                debug call ThrowError(not node.isNode,  "List", "remove", "thistype", this, "Attempted To Remove Invalid Node (" + I2S(node) + ").")
            endif
            
            debug set node.isNode = false
            
            set node._list = 0
        
            if (0 == node._prev) then
                set _first = node._next
            else
                set node._prev._next = node._next
            endif
            if (0 == node._next) then
                set _last = node._prev
            else
                set node._next._prev = node._prev
            endif
            
            set node._next = thistype(0)._next
            set thistype(0)._next = node
            set size = size - 1
        endmethod
        method clear takes nothing returns nothing
            debug local thistype node = _first
           
           static if LIBRARY_ErrorMessage then
                debug call ThrowError(this == 0,            "List", "clear", "thistype", this, "Attempted To Clear Null List.")
                debug call ThrowError(not isCollection,     "List", "clear", "thistype", this, "Attempted To Clear Invalid List.")
           endif
           
            static if DEBUG_MODE then
                loop
                    exitwhen node == 0
                    set node.isNode = false
                    set node = node._next
                endloop
            endif
            
            if (_first == 0) then
                return
            endif
            
            set _last._next = thistype(0)._next
            set thistype(0)._next = _first
            
            set _first = 0
            set _last = 0
        endmethod
        method destroy takes nothing returns nothing
            static if LIBRARY_ErrorMessage then
                debug call ThrowError(this == 0,            "List", "destroy", "thistype", this, "Attempted To Destroy Null List.")
                debug call ThrowError(not isCollection,     "List", "destroy", "thistype", this, "Attempted To Destroy Invalid List.")
            endif
            
            static if DEBUG_MODE then
                debug call clear()
                
                debug set isCollection = false
            else
                if (_first != 0) then
                    set _last._next = thistype(0)._next
                    set thistype(0)._next = _first
                    
                    set _last = 0
                endif
            endif
            
            set _first = thistype(0)._first
            set thistype(0)._first = this
            set size   = 0
        endmethod
    endmodule
    
endlibrary
JASS:
library HydraTemplate uses Hydra
//******************************************************************************
//*
//* A guide to individual Hydra structs 
//* ===================================
//*  
//*  First and foremost Hydra uses the vJass feature "Extending structs".
//*     - http://www.hiveworkshop.com/forums/tutorial-submission-283/vjass-meet-vjass-extending-structs-267719/#post2706474
//*  
//* Which means that each of your custom Hydra-types will extend Hydra. Example: FireHydra extends Hydra
//* That makes customization very flexible, as Hydra will evaluate several stub methods
//* in the respective child struct like the target filter or the missile setup.
//*
//*  The projectiles fired by hydra units are outsourced to library Missile,
//* as consequence you have full access granted to the Missile API, which
//* can be individual per child struct. This means projectiles fired by
//* i.e. FireHydra can have a different behaviour than those fired by an i.e. IceHydra
//* If you haven't done it already, please do a proper setup for the globals in library Missile
//*
//*  struct Hydra has a boolean member named hasArcOrCurve. You have to set it per Hydra instance you create
//* to false or true. Set it to true, if you wish to setup curve= or arc= for that custom Hydra.
//* Don't forget it, because otherwise the projectiles fired in the struct will go crazy.
//* in DEBUG_MODE you will recieve a warning, if you forget to set hasArcOrCurve.  
//*
//*  Below you will find an example struct, how a custum Hydra could look like
//* For more examples check out IceHydra, FriendlyHydra, EasterEgg, ... in this map.
//*
//*
//********************************************************************************

//=================================================================================
    
    //*  Declare a new custom hydra type which extends Hydra
    //* Here I have chosen the struct name AcidHydra.   
    struct AcidHydra extends Hydra

    //*
    //*  Acces the API of library Missile ( for detailed information read library Missile ).
    //* As already mentioned above, you have granted full access to the API of library Missile.
    //* You can declare various static method which will be called if a Missile launched from this struct,
    //* meets the condition for the respective static method.
    //*
    //* Available static methods are:
    //*
    //*     static method onCollide takes Missile missile, unit hit returns boolean
    //*          -   Runs every time the missile collides with a unit.
    //*
    //*     static method onPeriod takes Missile missile returns boolean
    //*          -   Runs every Missile_TIMER_TIMEOUT seconds.
    //*
    //*     static method onFinish takes Missile missile returns boolean
    //*          -   Runs whenever the missile finishes it's course.
    //*          -   Runs before the missile is destroyed.
    //*          -   Does not run, if a Missile is destroyed by another method.
    //*
    //*     static method onRemove takes Missile missile returns boolean
    //*          -   Runs whenever the missile is deallocated.
    //*          -   Return true will recycle a Missile delayed ( only if WRITE_DELAYED_MISSILE_RECYCLING = true )
    //*          -   Return false will recycle a Missile right away.
    //*          -   Runs always, if declared!!
    //*
    //*     static method onDestructableFilter takes nothing returns boolean
    //*          -   Runs before onDestructable.
    //*          -   May create a better filter for enumerated destructables.
    //*          -   Only method which has no impact on the Missile instance.
    //*
    //*     static method onDestructable takes Missile missile, destructable hit returns boolean
    //*          -   Runs every time the missile collides with a destructable.
    //*          -   Can use onDestructableFilter ( optional ) 
    //*          -   Runs after onDestructableFilter ( if filter is declared )
    //*
    //*       Hint: missile.data is always the respective Hydra instance.
    //*
    //*  Let's see some of the static methods I declared for this struct. 
    
        private static method onRemove takes Missile missile returns boolean
            return true
        endmethod
        
        //* Will kill the missile, when it collides i.e. with a wall.
        private static method onTerrain takes Missile missile returns boolean
            return true
        endmethod
        
        //* Will kill the missile when it deals damage.
        //* You can read missile members, like missile.source, missile.damage, ....
        private static method onCollide takes Missile missile, unit hit returns boolean
            if UnitAlive(hit) and IsUnitEnemy(hit, missile.owner) then
                return UnitDamageTarget(missile.source, hit, missile.damage, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
            endif
            return false
        endmethod
        
        //*  module MissileStruct is required, that a hydra unit can fire missiles at all.
        //* The best place for it is above the Hydra stub methods and below the Missile API.
        //* In this position the JassHelper will not generate extra pseudo-code duplicates.
        implement MissileStruct
        
    //*  Access the Hydra API: ( for detailed information read library Hydra. )
    //* As mentioned above you can customize a couple of stub methods located 
    //* in library Hydra, our parent struct. A declaration of method onFire
    //* is required, that a Hydra type of this struct can fire missiles at all. 
    //*
    //*  Available stub methods are:
    //* 
    //*      public stub method onSummonHead takes unit head returns nothing
    //*          - runs each time a new head is summoned.
    //*
    //*      public stub method onRemoveHead takes unit head returns nothing 
    //*          - runs each time a head dies.
    //*
    //*      public stub method onTargetFilter takes unit target, player owner returns boolean
    //*          - customize a target filter. As you can see each different Hydra type can have it's own filter.
    //*  
    //*      public stub method onFire takes unit shooter, unit target, Missile missile returns nothing
    //*          - customize the missile datas here. ( damage, model, arc, curve, ... )
    //*          - from this method you have to launch the missile via thistype.launch(missile)
    //*          
    //*  Let's see some example code:
    
        //*  Here we have access to the shooting head and the already created missile instance.
        //* missile.source, missile.owner are already set and don't need further changes.
        //* missile.data is already set to this AcidHydra instance, you can override it if required.
        private method onFire takes unit shooter, unit target, Missile missile returns nothing
            set missile.model      = "Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl"
            set missile.scale      = 1.
            set missile.damage     = GetRandomReal(50, 150)
            set missile.collision  = 32.
            set missile.collisionZ = 15.
            set missile.speed      = 27.
            //*  You have to launch the Missile here. It will tell library Missile, that it's
            //* a missile fired from excacly this struct.
            call thistype.launch(missile)
        endmethod
        
        //*  Another stub method, which runs each time a new head of type AcidHydra is created
        private method onSummonHead takes unit summoned returns nothing
            //*  call BJDebugMsg("summoned")
        endmethod
        
        //*  Here you can customize, which units can the AcidHydra target.
        //* Example: Just enemies structure type units are valid targets.
        private method onTargetFilter takes unit target, player owner returns boolean
            return IsUnitType(target, UNIT_TYPE_STRUCTURE) and IsUnitEnemy(target, owner) and UnitAlive(target)
        endmethod
        
    endstruct

    //*  Here you will learn how to create a AcidHydra instance.
    //* I choose a spell as example:
    private function OnCast takes nothing returns boolean
    
        //*  Declare a new Acidydra local.
        local AcidHydra hydra   
        local real x        = GetSpellTargetX()
        local real y        = GetSpellTargetY() 
        local real z        = 0.
        local real face     = GetRandomReal(0., 2*bj_PI)*bj_RADTODEG
        //*  This is the spawn interval between each heads.
        //* CreateHead - wait interval - CreatHead - wait interval - ...
        local real interval = .23

        if (GetSpellAbilityId() == 'xxxx') then
             
            //*  static method create takes unit whichUnit, integer unitid, real x, real y, z, real angle, spawnInterval returns Hydra
            set hydra                     = AcidHydra.create(GetTriggerUnit(), 'hfoo', x, y, z, face, interval)
            
            //*  Customize hydra members. 
            //* Each member has a default setting, which is done in static method create.
            //* You will recieve an error message, if you forget to setup a mandatory member.
            set hydra.heads               = 3     // By default 0.
            set hydra.attackAnimationTime = .5    // Read it in the object editor and divide it with 1/2. ( the second half is the backswing )
            set hydra.attackSpeed         = 3.    // By default 0.
            set hydra.attackPeriod        = .25   // By default 0.
            set hydra.duration            = 12    // By default 0.
            set hydra.attackRange         = 950.  // By default 0. 
            set hydra.headHeight          = 55.   // By default 0.
            set hydra.scale               = 1.    // By default 1.
            set hydra.hasArcOrCurve       = false // By default false
            set hydra.birthAnimationTime  = 0.    // By default 0.
            
            // *Summon it.
            call hydra.summon()
        endif
        return false
    endfunction
    
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        if UnitAddAbility(DemoMap_HERO, 'xxxx') then
            call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
            call TriggerAddCondition(t, Condition(function OnCast))
        endif
    endfunction
    
endscope



v1.0
Initial release.​
v2.0
Completly revised projectile system.
Can now fire with attackspeed below attack animtion time.
More intelligent shooting.​
v2.1
static method create takes unitid as argument aswell
method operator missileheight added.​
v2.2
fixed z calculation.​
v3.0
Improved setup options and performance.​
v3.1
Removed constant functions for setters.​
v4.0
Epic update. Now you can customize everything to your wishes.​
v4.1
Fixed a bug while reading node.next first found by Empirean.​
v4.2
Added
method operator scale=
method operator scale
method operator offset=
method addHead
stamp is now a global timer.
removed list as requirement.
shooting process starts, when the first head is summoned.
improved shooting behaviour.​
Credits:
Nestharus, Vexorian, Rising_Dusk​

Keywords:
Hydra, Guardian, Fire, Dragon, Serpent Ward, Ward, BPower, Diablo
Contents

Hydra (Map)

Reviews
12:29, 26th Mar 2015 IcemanBo: Creates hydra at location which shoots at enemies. Code is written very professional. Can't see any wrong with it.
Level 19
Joined
Mar 18, 2012
Messages
1,716
Yes it must a mistake inside my code. I will check it again.
Thanks for the report.

Also gonna update the link to List

Edit: I tried 15 minutes to reproduce the bug, but I couldn't. I was spamming
all abilities everywhere. Maybe the code is weak at those part where I have this:

if node.next == 0 then
set node = list.firstelse
set node = node.nextendif


A circular linked list would probably solve the problem.
As node.next will always point to the root, if it's the last node.
Is there a proper debuged written module on the Jass market?

Edit2:
i tried it again. I don't know what you did to produce that bug. Really not :(
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
Level 19
Joined
Mar 18, 2012
Messages
1,716
I found the mistake and I'm suprised I couldn't reproduce the error in game.
It should appear more often.

There was an invalid integer comparison within the function where I kill
a hydras heads. A comparison of two different types which made no sense.
JASS:
            local HydraHead head = list.first
            
            if (headCount == head) then
                call PauseTimer(stamp)
                call PauseTimer(shoot)
            endif
You can see that I compared head to headCount, which is not only simply wrong.
It also produces the node read to fail, because later in the function I remove ( head ).
The timers are not stopped and that node is eventually read in the next "attemptShoot" function callback.
Intentionally I wanted to call PauseTimer only once and only on running timers,
but I guess PauseTimer on not running timers does not hurt.

I fixed it and also added a timeout calculation, how much timed life a deallocation hydra head has,
so it doesn't die within it's last shooting animation. It wait between 0.0001 - end of aninamation time.

Thanks for the links Nestharus. I'm gonna look into it, when I come back from traveling.

Edit:
  • Uploaded fixed version. Everything should work fine now.
  • Fixed links to Nes github resources. They now point to the folder where the code is located.
Things I still could improve:

Intelligent target evaluation:
In a previous version I was able to track, if a missile hits its target or collides with i.e. the ground,
because the missile system was hardcoded within the Hydra library.
Therefore I could add an error member, which forces a missile to change its target, if it is hidden.

Solution:
In the current version, which outsources projectile handling to my Missile library this is also possible,
when the user passes in error parameters into Hydra. It's no longer an automatically running process.

Trigger eval spam:
This method onTargetFilter(u, owner), which is actually public stub method onTargetFilter takes unit target, player owner returns boolean,
runs uncontrollable times per shoot attempt, depending how many units are withing attackRange.
Not a big deal for small crowds, but the count of triggerevaluations is equal to the number of units enumerated + 1

Solution:
I could make a second Hydra version using delegates. This will create much more code per different hydra type,
but will run faster ( function call vs trigger eval ). I could track the number of trigger eval in debug mode and
give a recommendation to change the library version if the counter exceeds a specific number.
API would be the same.

method addhead & method removeHead :
So you dynamically add/remove heads like in the greek legend.
The first meets the following problem: Where to located it, which x/y is good.

The second requires a table + GetHandleId to track which list the head should be added to.
Tabke, because I don't want Hydra to fire an onIndex event of indexers. That's overhead we don't need.

different attack animation:
Currently Hydra supports "attack " + suffix, while suffixes are:
JASS:
            set thistype.animationSuffix[0] = "first"
            set thistype.animationSuffix[1] = "second"
            set thistype.animationSuffix[2] = "third"
            set thistype.animationSuffix[3] = "fourth"
            set thistype.animationSuffix[4] = "fifth"
Individual animations would require a hashtable + GetUnitTypeId as parent key.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
I'm going to update the spell this week. I have a few cool ideas, which I want to add
to the API.

Edit:
Updated to 4.2; Changelog:
  • Ripped out required methods from the List module and added them directly into the code to reduce overall code. ( removed List as requirement )
  • replaced real scale with method operator scale= , can be called now at any time, runs for every head in the list.
  • replaced real offset with method operator offset= , can be called at any time, is automatically affected by scale.
  • replaced real duration with method operator duration= , can be called at any time, may resets the life time of the Hydra.
  • the shooting process starts now after the first head is summoned and not when all heads are summoned.
  • shooting process now ends, when the last head is removed.
  • added real birthAnimationTime as set-able member to add a delay until a head starts to fire missiles.
  • You may add heads manually via method addHead.
  • Small code & documentation improvement.

I still have to work on the z impact calculation :(

Edit: I have quite a lot grammar mistakes in the documentation. I will correct them in the next update.
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
Ripped out required methods from the List module and added them directly into the code to reduce overall code. ( removed List as requirement )

I don't know if this is a good idea : ). For example, the new linked list stuff supports even better debugging capabilities and still shares the same API. You won't be able to update it by just changing the module name.

If anything, what I recommend is doing an implement List within your HydraStructure module. This'll let you go to any new updates with a single line change.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
So, BPower, I have been looking over this spell over time as I wanted to give it my best assessment.

Honestly, syntax and coding is well-optimized. You may want to fix the typo in ToogleUnitIndexer.

The biggest issue I have with it - and it's fundamental: the complexity. You mentioned dissatisfaction with stub methods, but it goes deeper than that.

- An event for many different parts of this spell.
- The high learning curve to inplement it.

At some point, you as the spellmaker must say "I will use my creative, stylistic process here and dissolve a lot of the ambiguity with this spell by making things more automatic. Now, you've made a template, which is great. However, a single implementation of this spell will mean all that complexity went to waste. One might say it's not to fault the system but the user's lack of input, but I say it is a fault if such complexity in a simple situation is a deterrent to users.

For a missile system, yes let's make sure we cover those events. But for a system like this I think what would be better served is a universal template which branches out and makes accessible the fundamental parts of the spell. Instead of making Hydra such a complex resource, make a standalone complex resource which will allow spells similar to Hydra to have those powerful pieces. Make Hydra use a generic version of the system it currently integrates, so you have a simpler Hydra spell and a more dedicated system full of a bunch of features.

You'll probably appreciate doing it when it comes time to make your next spells, knowing that you won't have to code so many parts from scratch. The challenge here will be finding which parts should stay Hydra, and which parts should be split into the other system.

Alternatively, you could say I am trying to split Hydra in two. Hur hur.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Thanks for the detailed review. I very appreciate your criticism.

What you mention is exactly, what I was thinking about when writing Hydra and
the first released result v.1.0 was similar to a normal spell with a normal global user setup.
It also had a hardcoded missile system.

Then with burning ambition I wanted to re-write the library, so users can use
Hydra in any way the want to. I didn't want to use modules, because it would
duplicate the source code of hydra for each struct. That's why I used OOP code.

My idea was that a user can have in a map in example
  • A frosty hydra
  • A fire hydra
  • A hydra which only attack dead corpses and let it explode

The learning curve is a bit long, then however everything goes very fast and
the customization level is gigantic.

I will think about changes.
 
Top