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

Moderator

M

Moderator

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.
 
In method considerTarget your only factor for "best" target is the distance to unit. But it would make sense also to check for unit's life, to potentially attack a unit with low life.
Before you make a new group enumeration, you check if old target is still valid. In my opinion the check is not needed, because you might find a "better" target in a new enumeration.
And anyway, only checking for range and UnitAlive(node.aim) is not enough for old target. You again need to call your filter function, to ensure correct result. (the owner of target might have changed, for example)
JASS:
if best == null then
    set node.aim = new
else
    set node.aim = best
endif
set best  = null
set new  = null
^You don't need to null best, if it's null already. ^^
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
^You don't need to null best, if it's null already. ^^
:0

Man, those kind of optimizations are really really unnecessary. I would rather ignore those in my reviews (but not in my codes tho).

@BPow

I can't compile your code, you got some errors in summon method
JASS:
        method summon takes nothing returns nothing
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I can't compile your code, you got some errors in summon method
Thank you. I forgot an endif when debug mode is disabled.

But it would make sense also to check for unit's life, to potentially attack a unit with low life.
Yes, no ... there are endless possibilities for higher priority targets (stunned, disabled, percentlife, life, distance, ...)
Now when you play the test map, you will see that it's more and more unlikely to hit an moving enemy which is more far away.
Whereas close units can still be hit, even when they are moving.

Before you make a new group enumeration, you check if old target is still valid. In my opinion the check is not needed, because you might find a "better" target in a new enumeration.
I think if an head aquired a target, it should focus it as long as possible.

And anyway, only checking for range and UnitAlive(node.aim) is not enough for old target.
Yes that's true.

You don't need to null best, if it's null already. ^^
Yes


Missiles now consider terrain height aswell.
- Missiles will be destroyed on hills, assuming that this hill has a higher z offset than the missiles offset.
- Missiles keep their fly height, when there is a canyon in their path. (visual improvement)
- Missiles will not collide with unit in canyons, assuming their z + DEFAULT_UNIT_Z offset is lower than the z offset of the missile.

Flying units can be hit, but missiles do not fly in a curve or alter their fly height
In conclusion Hydra can't shoot uphill or downhill, just on the same terrain level as it is summoned on
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
  • Trigger/Code:
    • I hope you're not forget about the convention for constant variable naming: CONSTANT_VARIABLE. No exception for constant struct members. JPAG
      .
    • JASS:
      set d    = SquareRoot((x - originX)*(x - originX) + (y - originY)*(y - originY))
      It's not a right way to calculate total traveled distance of a missile. Should be something like this instead:
      current dist = current dist + velocity

      If you want to keep it that way (because the missile trajectory is just simply linear), then variable d is unneeded.
      .
    • JASS:
              @If you have BoundSentinel@ in your map you don't need a periodic 
              check if the protectile is within bounds.
      You can't list WorldBounds as requirement then? If user has BoundSentinel already, WorldBounds will be no use at all whereas it's required => burden.
      .
    • JASS:
      readonly string modelpath
      It shouldn't be there. For what iydm? Can we read/use it for something?
      .
    • MAXIMUM_COLLISION_SIZE should be removed. That makes the missile's AoE (hit radius) calculation become very vague for user, for me either.
      JASS:
      call GroupEnumUnitsInRange(thistype.enu, x, y, aoe@+MAXIMUM_COLLISION_SIZE???@, null)
      .
    • Well, your code is very fine as always.
    .
    .
  • Object data & Misc:
    • Just right
    .
    .
  • Rating:
    I can't expect the code to be any faster. You can spam it massively without any meaningful lag or fps drop. Concept is simple, neat, and the most important: original. Object data is fine. Visual effect is just right and fitting. The spell code itself looks very modular, allowing you to implement it in very various style of spell casting.

    Overall 4/5 and vote for Approved
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I don't see it so strict for struct members, never did.

It's not a right way to calculate total traveled distance of a missile. Should be something like this instead:
Perfectly returns the distance between launching point and the current position and that's excacly what is needed.

You can't list WorldBounds as requirement then? If user has BoundSentinel already, WorldBounds will be no use at all whereas it's required => burden.
Hydra uses UnitIndexer, which uses WorldBounds. There is no way you don't have WorldBounds in your map if you want to have Hydra.

readonly string modelpath It shouldn't be there. For what iydm? Can we read/use it for something?
I'm not sure how to perfectly solve this, yet. Since the spell is highly configurable you can shoot different missiles e.g. fireballs or acid missiles.
If that missile collides with an enemy, you can read the used modelpath and apply for example a buff or change the damage option.
Another solution would be to assign fixed indexes to missiles. 1 for fireball, 2 for frost and read that index onCollide.

MAXIMUM_COLLISION_SIZE should be removed. That makes the missile's AoE (hit radius) calculation become very vague for user, for me either.
No!
While a missile can have a collision size of 40, a nearby townhall has a collision size of 196. It would not be within the groupenum, even though it should be.
It's also not vague because of the next line: IsUnitInRange(u, missile.unit, aoe)
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
BPower said:
I don't see it so strict for struct members, never did.
Said so because JavaScripting has the same convention, they use CAPITALIZED for constant struct/class/whatever members.

BPower said:
I'm not sure how to perfectly solve this, yet. Since the spell is highly configurable you can shoot different missiles e.g. fireballs or acid missiles.
If that missile collides with an enemy, you can read the used modelpath and apply for example a buff or change the damage option.
Another solution would be to assign fixed indexes to missiles. 1 for fireball, 2 for frost and read that index onCollide.
Well, you have returned thistype in create method. I believe user can use it instead of checking for missile's model. So removing it is your solution.
JASS:
 static method create takes unit who, real x, real y @returns thistype@

BPower said:
No!
While a missile can have a collision size of 40, a nearby townhall has a collision size of 196. It would not be within the groupenum, even though it should be.
Is that a trick to get unit's collision size? I don't get it, each unit has different collision size.
Ah! I get it. ^^

But soo, I can conclude IsUnitInRange(u, missile.unit, aoe) will always return false for units outside the aoe? No matter if their collision collide with the aoe circle. You should make a function called IsUnitCollideCircle instead. Well it needs another function called GetUnitCollisionSize :/
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Btw I don't think the concept is neither simple nor original, but of course neat.
The outcome looks simple: summons 3 units which are periodically fires missile. Although it has leviathan code.
And yes it's original, afaik, since I haven't seen similliar spell around spell section so far. Seriously, you can't make something very original today, at least it's close to impossible.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Huge update.
Hydra:
- can now shoot with any attackspeed
- properly adjusts attack aninmation speed.
- improved target evaluation
- better debug options
Projectiles:
- can fly uphill/downhill
- collide with cliffs
- support onCollision/onDestructable/onExpire
- Take z axis into consideration, when it comes to collisions
- No longer uses CTL, but just one normal timer and a linked list
 

Deleted member 219079

D

Deleted member 219079

So you've optimized this to the point where you inline RMaxBJs?

Reminds me of making a program in TI-basic.

Edit: +rep for the effort
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
There are two small "mistakes" in the z collision calculation, depending on the missile's collision size.

1. Correct target z offset towards the ground, because currently the missile system assumes that units in collision range have the same ground level as the missile.
In rare cases this may be wrong (cliffs & missile collision size above a tile size).

2. The collision in z axis is measured from foot to head of the target and the center of the missile.
We already assume that missiles are squares in x/y collision. I will add a member for z collision, which leaves the option to make it spherical, but also egg-shaped or flat.

Update will come after the New Year.

EDIT: Updated and solved both aspects mentioned above.
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
HydraCollision review

Documentation typo
You can *savely* delete onCollide, onExpire and/or onDestructable,


I recommend making these variables private and putting them in another macro

Nothing's stopping you from using two macros.

JASS:
globals
    // For demostration issues I set up unique missile strings. 
    // Do not privatize missile file variables! They must be readable from the Hydra library.
    // constant string MY_MISSILE = "path\\..."
    
    constant string CHIMAERA_ACID_MISSILE = "Abilities\\Weapons\\ChimaeraAcidMissile\\ChimaeraAcidMissile.mdl" 
    constant string FROST_WYRM_MISSILE    = "Abilities\\Weapons\\FrostWyrmMissile\\FrostWyrmMissile.mdl"
endglobals

This should be private

static method onCollide takes thistype this, unit picked returns boolean

The level of customization here is fantastic. However, if you are going to focus on nice customization like this, it might be be better to make it instanced customization rather than global static customization. For example, you can have a weaker form of the Hydra spell or a stronger form. Maybe do instanced global settings and when you create an instance of a hydra spell, you pass in what settings you want to use. I would then make the callback a part of those settings and then have general behavior.

Hydra review

These global settings are the same as mentioned above. These also may be based on level.

JASS:
    globals
        // Initial missile offset in the shooting process. Use method operator missileoffset to change it individually.
        private constant real HYDRA_HEAD_DEFAULT_HEIGHT   = 55.
        // Attack animation duration.
        private constant real HEAD_ATTACK_ANIMATION_POINT = .5
        // The initial facing angle.
        private constant real FIRST_FACING_ANGLE          = 225.
        // The polar projection angle for the first head.
        private constant real FIRST_HEAD_ANGLE            = 90.*bj_DEGTORAD
        // Time to wait between spawning each head.
        private constant real HEAD_SPAWN_INTERVAL         = .23
        
        // Largest collision size in your map. Townhall e.g. has 196.
        private constant real MAXIMUM_COLLISION_SIZE      = 196.
        // Minimum fly height for projectiles, before they get destroyed.
        private constant real MINIMUM_MISSILE_OFFSET      = 0.
        // Projectiles consider the z axis aswell. Every unit uses this value as
        // default body height. In order to hit a target, it must be in 
        // DEFAULT_UNIT_Z of the missiles offset. High quality maps
        // may use a custom z size for each unit type and unit used.
        private constant real DEFAULT_UNIT_Z              = 96.
    endglobals

There is a location z library in the JASS section now -> http://www.hiveworkshop.com/forums/jass-resources-412/snippet-zlibrary-237821/

JASS:
    globals
        private location LOC = Location(0., 0.)
    endglobals
    private function LocationZ takes real x, real y returns real
        call MoveLocation(LOC, x, y)
        return GetLocationZ(LOC)
    endfunction

AllocFast will do this. It is under the Alloc submission.

JASS:
    // Hydra missile system. Allocation module
    // Pro: Fastest way of allocation.
    // Con: Big initial loop.
    private module PROJECTILE_INITIALIZER_CODE
        private static method onInit takes nothing returns nothing
            local integer i = 0
            set rc[8191] = 0
            loop
                set rc[i] = i + 1
                
                exitwhen i == 8190
                set i = i + 1
            endloop
        endmethod
    endmodule

There are several Linked List data structure modules that will do this

I would probably recommend this one for your purposes

https://github.com/nestharus/JASS/blob/master/jass/Data Structures/StaticUniqueList/script.j

The StaticUnique list will handle allocation for you and it does specialized allocation for lists. It'll save you some variables. It won't specifically use the AllocFast algorithm though. You can copy the module code and modify it to your needs if you want a custom solution. You can also drop the methods you don't need.

JASS:
        // Linked List
        private static integer array n
        private static integer array p

Replae with Alloc

private static integer array rc

Just give the variables proper names. You shouldn't have to comment on them with their actual names.

JASS:
        private real cos    // Cosinus
        private real sin    // Sinus
        private real dist   // Maximum travel distance
        private real ox     // Origin x
        private real oy     // Origin y
        private real oz     // Origin z
        private real of     // Origin fly height
        private real ag     // Angle
        private real vel    // Velocity
        private real zAdd   // Z increment per loop 
        private real zInc   // Total z increment so far

You are using ErrorMessage, so why don't you do ThrowWarning or whatever the thing is called ; ).

It seems silly to write your own.

JASS:
            debug if (sfx != null) then
            debug call BJDebugMsg("WARNING: Library Hydra, struct projectile, method operator model=, sfx is not null for instance " + I2S(this)) 
            debug endif
            debug if (file == null) then
            debug call BJDebugMsg("WARNING: Library Hydra, struct projectile, method operator model=, file is null for instance " + I2S(this)) 
            debug endif

Right now you are embedding a timer system into the resource, which muddles your code with extra things like this. This is kind of sloppy.

JASS:
            if (0 == n[0]) then
                call PauseTimer(thistype.t)
            endif

You don't need the DEBUG_MODE there. Also, and/or don't work with static ifs.

JASS:
            static if LIBRARY_ErrorMessage and DEBUG_MODE then
                debug call ThrowError(rc[this] != -1, "Projectile", "terminate", "thistype", this, "Attempted To Deallocate Null Instance.")
            endif

Improper name
private static method cb takes nothing returns nothing

This depends on the object movement type. Be sure to let people know not to change the movement type of the object or to use a specific movement type.
JASS:
                set f    = -(z - oz) + of + zInc// Keep the correct fly height,
                                                // in case the terrain elevates or flattens.

Static if doesn't support and/or
static if not LIBRARY_BoundSentinel and LIBRARY_WorldBounds then

Since you are using UnitIndexer, I recommend you pass around unit indexes instead of units. This will reduce handle ref count management. Furthermore, users will either be depending on unit-specific data, meaning that they will be using GetUnitUserData, or they will rely on the unit type id, which is a simple array read with Unit Indexer. This is a choice though, but I have personally found that passing around a UnitIndex is more beneficial than passing around a unit. Although, this largely depends on the function. Some of your functions, like unit default height, would be best with a UnitIndex.

This should probably be a ThrowWarning
JASS:
            debug if (z < MINIMUM_MISSILE_OFFSET) then
            debug call BJDebugMsg(SCOPE_PREFIX + " WARNING: struct projectile, static method create, passed in z below MINIMUM_MISSILE_OFFSET")
            debug endif

A lot of extra logic that doesn't need to be in the code
JASS:
            set n[this] = 0
            set p[this] = p[0]
            set n[p[0]] = this
            set p[0]    = this

There is a new resource called OnInit or something. Feel free to use this instead of writing special modular initializers from now on.

implement PROJECTILE_INITIALIZER_CODE

The regular Alloc uses this allocation method

JASS:
    /*
    *   Simple allocation module.
    *   Pro: Less overhead on init.
    *   Con: Slightly slower than in Projectile. 
    */

This method exists outside of debug mode for Alloc

You can also check for memory leaks

JASS:
        method operator exists takes nothing returns boolean
            return (recycler[this] == -1)
        endmethod

This, and the subsequence methods, should probably be attackSpeed, attackPeriod, attackRange, and so on. This isn't Lua : P.

There also don't seem to be getters for this stuff, only setters.

JASS:
method operator attackspeed= takes real value returns nothing
method operator attackperiod= takes real value returns nothing
method operator attackrange= takes real value returns nothing
method operator missilemodel= takes string path returns nothing
method operator scale= takes real value returns nothing
method operator lowerDamageBound= takes real value returns nothing
method operator upperDamageBound= takes real value returns nothing
method operator missileoffset= takes real value returns nothing

I would use an array here
JASS:
        private static method headAnimationSuffix takes  integer index returns string
            if     (0 == index) then
                return thistype.FIRST
            elseif (1 == index) then
                return thistype.SECOND
            else
                return thistype.THIRD
            endif
        endmethod

This seems to be double free detection. Alloc handles this. Also should be a ThrowError.

JASS:
            debug if not this.exists then
            debug call BJDebugMsg(SCOPE_PREFIX + " ERROR: method destroy, thistype " + I2S(this) + " Is Not Allocated!")
            debug return
            debug endif

Should probably rename node to head.
JASS:
            loop
                exitwhen 0 == node
                if (UnitAlive(node.unit)) then
                    call KillUnit(node.unit)
                endif
                set node.unit = null
                set node.aim  = null
                set node = node.next
            endloop

This is checked for twice in destroy for some reason. You have it once towards the top and once towards the bottom.
JASS:
            static if LIBRARY_ErrorMessage and DEBUG_MODE then
                debug call ThrowError(recycler[this] != -1, "Alloc", "deallocate", "thistype", this, "Attempted To Deallocate Null Instance.")
            endif

You used ThrowWarning here but nowhere else?

JASS:
            static if LIBRARY_ErrorMessage and DEBUG_MODE then
                call ThrowWarning(this.mind > this.maxd, "Hydra", "fire", "mininum - maximum damage", this, "mindamage is higher than max damage.")
            endif

p isn't a very good name

local Projectile p = Projectile.create(ox, oy, this.height*this.size, this.angle)

This should just be a repeating timer. Under the animation attack speed setter is where you want to restart a timer. A repeating timer is quite a bit faster than a timer that is started over and over again.

call TimerStart(t, a, false, function thistype.attemptShoot)

For determining the next head to fire, I think that you should store the heads inside of a queue, not a stack. Even more, I think you should store the heads in a list. When a head dies, it is removed from the list. This way you don't have to check if a head is alive during your timer loop. In fact, you should use a circular list. You have an active head in the list. The active head is always the best one to fire. If the node being removed is the active head, advance the active head forward by 1. Whenever the active head fires, advance it by 1. This will remove your loop entirely and make the entire thing O(1) instead of O(n).

JASS:
            loop
                exitwhen 0 == node
                if UnitAlive(node.unit) then
                    if (node.lastShot + this.timeout <= elapsed) then
                        if (0 == picked) or (picked.lastShot > node.lastShot) then 
                            set picked    = node
                        endif
                    endif
                elseif (node.unit != null) then
                    set node.unit  = null
                    set this.count = this.count - 1
                endif
                set node = node.next
            endloop


It is ok that the heads do not have a unit index. They can't damage and they can't be damaged, meaning that they are purely visual.

I recommend that, since you aren't doing a unit index for the head, you start a timer for each one based on the lifetime. When the timer expires, remove the head from the list. This will remove your periodic check.

I recommend that this stuff turn into warnings and one error at the end.

JASS:
            debug local integer error = 0
            debug if (period < 0.) then
            debug call BJDebugMsg(SCOPE_PREFIX + " ERROR: method summon, attackperiod is less than 0!")
            debug set error = error + 1
            debug endif
            
            debug if (life < 0.) then
            debug call BJDebugMsg(SCOPE_PREFIX +" ERROR: method summon, duration is less than 0!")
            debug set error = error + 1
            debug endif
                
            debug if (range < 0.) then
            debug call BJDebugMsg(SCOPE_PREFIX + " ERROR: method summon, range is less than 0!")
            debug set error = error + 1
            debug endif
                
            debug if (file == null) then
            debug call BJDebugMsg(SCOPE_PREFIX + "ERROR: method summon, modelfile is null!")
            debug set error = error + 1
            debug endif
                
            debug if (error > 0) then
            debug call BJDebugMsg(SCOPE_PREFIX + " " + I2S(error) + " errors occured within Hydra instance " + I2S(this) + ". Shutting down Hydra. Check your settings.")
            debug set error = 1/0
            debug endif



I give this a rating of 4/5. Some improvements can be made, but it should be approved.
 

Deleted member 219079

D

Deleted member 219079

Static if doesn't support and/or
>>>>
jasshelper manual said:
static ifs are like normal ifs, except that a) the condition must contain only constant booleans, the and operator and the not operator and b) They are evaluated during compile time. Which means that the code that is not matched to its condition is simply ignored.

He only highlighted errors and advertises his own resources, how is that a review?
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I started to change a few things you mentioned. However I do not agree on every point you made.

For example I'm listing Alloc as dependency, because the module initializer I wrote does the same.
For those who really wish to use Alloc/AllocFast can easily replace this small peace of code.
My personal opinion is that many users will not import a resource, if it has too many external
dependencies listed. I also wouldn't.

You were criticising the timer system I used for the Projectile. I guess you recommend to use CTL instead.
I will consider using it.

All in all it is going slowly, because I don't have much time at the moment.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Stuff like this will always be false because vJASS does not evaluate anything except for single boolean values that may have a not in front of them

static if LIBRARY_ErrorMessage and DEBUG_MODE then
Incorrect.
JASS:
      private function Init takes nothing returns nothing
            static if DEBUG_MODE and not HELLO and HELLO2 and LIBRARY_Table then
                call BJDebugMsg("HELLO WORLD")
            endif
        endfunction
Will Print HELLO WORLD.

Change one argument, so it compiles to false and it won't print anything.
 
Really great spell. You made a nice code structure and it has good modularity.

JASS:
//static method create takes unit who, real x, real y returns thistype 
set hydra = Hydra.create(GetTriggerUnit(), 'o002', x , y, 225.)
^The comment is not very correct, if you know what I mean. :D

No... I only see one thing I would add. Rest looks all fine to me. You sometimes null things that don't need to be nulled,
but I know you know it yourself and just do it because of habit. We could discuss about how to make prioroties
for the nextTarget filter, but it's okay as it is now and would probably be just more personal.

So, could you add a safety check for if hydra got removed from game? In a case the user removes units by a player
this could happen and then the hydra heads will bug. Once it's removed it should not attack anymore,
but just being destroyed properly.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Yes I can apply some code improvements and safety checks.
- The data structure should be optimized.
- I should to reconsider the current API

I'm always open for discussions, especially towards the target filter.
But I think the heads don't have to be too clever, in my opinion it's important to evaluate if
they actually can hit the target, they are aiming at.

I will make it on the weekend.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I ran in some unexpected issues. I will update the map this week.

UPDATE:
- removed setters which seemed not useful.
- removed constant functions in favour of setters, mainly applies to missile data.
- add method optimize()
- attack period is now working properly with attack speed and attack animation.
- fixed a bug: When one head couldn't attack the whole hydra was not attacking.
- heads can now be set in range of 1 to x, and are no longer constant.
- better debug features
- added 4 "labs" to explore how the system is working.

What I didn't do:
- Replaced List with SinglyCircularList which would be the best data structure.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
There is a a warning message, if this case is happening. It's in attemptShoot()
I would recommend to use destroy and re-create it the way you want.
If someone really wants something like addHead() or removeHead(), then it could be easily implemented. But I don't think it's useful to have by default.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I'm planning a huge update on Hydra. What I want to change:

1.) Use Missile for projectile movement.
2.) Demo includes two types of Hydra libraries.

The first has the same structure as it is now ( just integrates Missile )
That one should be used if you just use 1-2 Hydra spells in your map.

The second has the following convention: struct MyStruct extends Hydra
That one should be used if you want to create multiple Hydra type spells in your map (3 - x).
It will allow you to controll the shooting process and collision evaluation directly in your struct.
Basically it's more flexible, with little extra overhead.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
It's done. The syntax for Hydras is now.

struct FireHydra extends Hydra
struct IceHydra extends Hydra

Everything can be customized in each respective struct,
so Hydra is no longe running with long if the else condition blocks.

Example:
JASS:
scope FireHydra initializer Init

    globals
        private constant integer HYDRA_ABILITY = 'A002'
        private constant integer HYDRA_UNIT_ID = 'o002'
    endglobals

    struct FireHydra extends Hydra

        private static method onRemove takes Missile missile returns boolean
            return true
        endmethod
    
        private static method onTerrain takes Missile missile returns boolean
            return true
        endmethod
    
        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
    
        implement MissileStruct
        
        method onFire takes unit shooter, 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.        
            call thistype.launch(missile)
        endmethod
        
        method onTargetFilter takes unit target, player owner returns boolean
            return UnitAlive(target) and IsUnitEnemy(target, owner) 
        endmethod
    
        method onRemoveHead takes unit head returns nothing
        endmethod
    
        method onSummonHead takes unit head returns nothing
        endmethod
        
    endstruct

    private function OnCast takes nothing returns boolean
        local FireHydra hydra   
        local real x    = GetSpellTargetX()
        local real y    = GetSpellTargetY() 
        local real z    = 0.
        local real face = GetRandomReal(0., 2*bj_PI)*bj_RADTODEG
        local real interval = .23

        if (GetSpellAbilityId() == HYDRA_ABILITY) then
            set hydra                     = FireHydra.create(GetTriggerUnit(), HYDRA_UNIT_ID, x, y, z, face, interval)
            set hydra.heads               = 3   
            set hydra.attackAnimationTime = .5 
            set hydra.attackSpeed         = 3.  
            set hydra.attackPeriod        = .25
            set hydra.duration            = 12
            set hydra.attackRange         = 950.
            set hydra.headHeight          = 55. 
            set hydra.scale               = 1.  
            set hydra.hasArcOrCurve      = false
            call hydra.summon()
        endif
        return false
    endfunction
    
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        if UnitAddAbility(DemoMap_HERO, HYDRA_ABILITY) then
            call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
            call TriggerAddCondition(t, Condition(function OnCast))
        endif
    endfunction
    
endscope

I could do a second version with delegates and the same API for those who need a extrem performace.
This will finally generate much more code in your map file. Dunno if it's simething we need.
 
Top