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.
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. ^^
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.
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
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.
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)
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 :/
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.
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
I thought about it for some time. Creating a system shouldn't be so difficult,
but I don't have the time to figure out a good algortihm for arcing missiles.
Dirac's needs a few obligatory fixes, but overall it is an acceptable system.
Finally I'm very satisfied with the final outcome of the Hydra spell.
If you have any criticism or ideas how to improve the current system, please feel free to tell me.
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.
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
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
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
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.
Thank you for the extensive review. I will comment and consider everything you mentioned.
Probably after the weekend, because currently I have only little time.
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.
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.
I see, thanks. I will go throw the code again very soon.
Is there already a circular list for public use submitted somewhere (collection)?
I didn't find anything, however I have to admit, spending not much time looking for it.
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.
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.
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.
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.
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 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.
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.
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.
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.
Oh, the urls are based on the filenames : |. The filenames changed from script.j to main.j, heh >.<.
Anyways, uh oh o-o. That comes from reading a deallocated node. List is still compliant with the collection testing suite, so it's definitely not a problem with the lib : ).
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.