Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
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.
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.
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
Also keep in mind that eventually they are going to move to like 1a1a and 1t1t form. Beyond that, they are going to go to like List<T>, but that'll be when the compiler is ready : ).
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.
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.
I'll consider using List ( new ) again, when making an update on Hydra.
Currently I'm waiting for user reviews and ingame experiences. I'm also testing the spell
myself ( making a map ).
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.
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.
This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
By continuing to use this site, you are consenting to our use of cookies.