Jesus4Lyf
Good Idea™
- Reaction score
- 397
SpellStruct
Version 1.0.7
Requirements:
- Jass NewGen
JASS:
//
//
// Spell Struct
// By Jesus4Lyf.
// Version 1.0.7.
//
// What is SpellStruct?
// ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
// SpellStruct is a system designed for the sake of rapid spell development.
// It grants features which can be entirely encapsulated in a struct type per
// ability. It handles event response creation, timer attachment, trigger
// attachment, area of effect (AoE) enumeration, unit attachment, and all
// spells made using it should be automatically MUI/leakless and rather efficient.
//
// How to implement?
// ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
// Simply create a new trigger object called SpellStruct, go to 'Edit -> Convert
// to Custom Text', and replace everything that's there with this script.
//
// _______________________
// || ||
// || SpellStruct Usage ||
// || ||
// ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
// Writing a Simple SpellStruct:
// ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
// - To use SpellStruct, write SpellStructs. These are structs which extend
// SpellStruct, and implement SpellStruct.
//
// - Everything is optional to implement/use, except setting thistype.abil='Axxx'
// in static method onInit.
//
// - Example:
//
/* struct MySpell extends SpellStruct
implement SpellStruct
private static method onInit takes nothing returns nothing
set thistype.abil='A000'
endmethod
endstruct
*/
// Event Responses:
// ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
// - The following is a list of event responses that come with SpellStructs.
//
// - Event responses are stored on your struct as members.
/*
// Members of your SpellStruct:
unit caster // The casting unit.
integer level // The level of the ability on the casting unit.
integer order // The order id of the caster when it began casting the spell.
unit targetUnit // The target unit of the ability being cast (null if none).
item targetItem // The target item of the ability being cast (null if none).
destructable targetDest // The target destructable of the ability being cast (null if none).
widget targetWidget // The target unit, item or destructable (see above).
real targetX // The point targetted when channeling began (X/Y coordinates).
real targetY // If spell is targetted, this is the original position of the target.
player owner // The owner of the casting unit, when channeling began.
integer abilId // The ability id of the ability being cast.
// Extra methods:
real casterX // The caster's X coordinate (inlines to GetUnitX(this.caster))
real casterY // The caster's Y coordinate (inlines to GetUnitY(this.caster))
*/
// - The above event response list may be used anywhere in your SpellStruct,
// and at any time.
//
// - You may implement methods which are called when the normal Warcraft III
// spell events would fire. Some of the Warcraft III event responses are
// broken for certain events, sometimes intermittently, but these are fixed
// when using SpellStruct.
// Also, usually in Warcraft III, these are implemented in a way that cycles
// through all triggered abilities to see if the spell cast is the spell the
// trigger is for. In SpellStruct, this is changed so that Warcraft III will
// jump straight to the method for the spell that was cast.
//
// - These methods, which are called when events fire, are non-static. This means
// any members you add in your SpellStruct can be accessed from within the method.
// This is achieved with unit attachment (internally).
//
// - Example:
//
/* struct MySpell extends SpellStruct
implement SpellStruct
// Reserved method names for event methods, in order of firing:
method onCreate takes nothing returns nothing // fires just before channel
// If this is an AoE spell...
set this.aoe=this.level*200
// Maybe you want to control the lifetime of the struct yourself:
set this.autoDestroy=false // there is a thistype.autoDestroyDefault too,
// which is true by default.
// So you can conditionally control the lifetime.
// Do not destroy structs before the spell ends.
endmethod
method onChannel takes nothing returns nothing // Unit starts channeling a spell.
endmethod
method onStartCast takes nothing returns nothing // Fires much as the same as channel.
endmethod
method onEffect takes nothing returns nothing // When the spell has successfully cast, mana deducted.
endmethod // Will only fire if channeling successfully completed.
method onFinish takes nothing returns nothing // When the effect finishes (or is interrupted).
endmethod // This will only fire if the effect fired.
method onStopCast takes nothing returns nothing // When the spell stops being cast, or is cancelled.
endmethod
private static method onInit takes nothing returns nothing
set thistype.abil='Axxx'
set thistype.autoDestroyDefault=true // set to true by default.
// may be overridden for each
// instance by setting
// this.autoDestroy=true/false
// AutoDestroyed structs are destroyed after the spell stops casting.
set thistype.defaultAoE=200.0 // optional.
endmethod
endstruct
*/
// Disabling Auto-Destruction:
// ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
// - SpellStructs are created just before the .onChannel event method is
// called.
//
// - By default, SpellStructs are automatically destroyed, and this occurs
// just after the method .onStopCast is called.
//
// - You may extend this lifetime in two ways. If you wish to manually
// manage the lifetime of a SpellStruct, you may do either of the following:
/* struct MySpell extends SpellStruct
implement SpellStruct
method onStopCast takes nothing returns nothing
call SpellStruct(this).destroy() // can call this at any time. JassHelper bug requires SpellStruct(this).
endmethod // no more event methods fire after destroying a SpellStruct.
method onCreate takes nothing returns nothing
set this.autoDestroy=false // Either this
endmethod
private static method onInit takes nothing returns nothing
set thistype.abil='Axxx'
set thistype.autoDestroyDefault=false // or this.
endmethod
endstruct
*/
// - Setting thistype.autoDestroyDefault to false causes .autoDestroy to
// be set to false just before onCreate is called, for all new instances.
//
// - Setting this.autoDestroy to false stops just the current instance
// from auto destruction. Setting it back to true will cause the struct
// to be destroyed if it usually would have been by then, else continue
// to monitor it for auto destruction.
//
// Locking:
// ¯¯¯¯¯¯¯¯¯¯
// - You may also add locks to an instance. This is done using this.addLock(),
// and this.removeLock(). Calling this.addLock increments a counter, and
// calling this.removeLock() decrements it. While it is greater than 0,
// a SpellStruct will not be autodestroyed. Decrementing it back to 0
// after the .onStopCast method has fired while .autoDestroy is true will
// destroy the struct automatically. This is a garbage collection mechanism.
//
// - You may check if a struct has no locks on it by using .isNotLocked.
//
// - Example:
//
/* struct MySpell extends SpellStruct
implement SpellStruct
private integer ticks=57
// T32x example.
private method periodic takes nothing returns nothing
set this.ticks=this.ticks-1
if this.ticks==0 then
call BJDebugMsg("Fired 57 times")
call this.stopPeriodic()
call this.removeLock() // Remove lock called here.
endif // will only destroy the struct if casting finished,
endmethod // so that event methods will continue to fire and
implement T32x // all data is available.
method onEffect takes nothing returns nothing
call this.startPeriodic()
call this.addLock() // We want the struct to exist until .stopPeriodic is called.
endmethod // Locks are useful because we could attach this instance to any
// number of things, which can each unlock it when they are done with it.
private static method onInit takes nothing returns nothing
set thistype.abil='Axxx'
endmethod
endstruct
*/
// Timers:
// ¯¯¯¯¯¯¯¯¯
// - If you have TimerUtils in your map, SpellStruct will operate using
// TimerUtils data attachment for timers.
// - If you don't have TimerUtils, but have Recycle, SpellStruct will
// attach to Recycled timers using GetHandleId and a hashtable.
// - If you have neither, SpellStruct will create timers dynamically,
// and pause and destroy them when they are done with. Attachment
// will be done with GetHandleId and a hashtable.
//
// - .startTimer(method, period) will start a timer for the given method,
// for the current spell instance. This means all spell event responses
// will be available from within the callback. This timer will keep
// firing until you stop it using .stopTimer(method).
//
// - Starting a timer in this way automatically adds a lock to the struct,
// and stopping a timer removes a lock. This is to guarantee that when
// a timer method fires, all data is available and valid (while a struct
// has locks, it will not be auto destroyed).
//
// - Manually calling .destroy on a spell struct will stop all timers for
// that struct automatically.
//
// - Because it attaches both the method to call and the struct instance
// to the timer, and then fires the method with .execute(), it is
// recommended that you use this only for timers that are reasonably
// infrequent. Using T32x with SpellStruct is recommended for high
// frequency (low period) timers.
//
// - Example:
//
/* struct MySpell extends SpellStruct
implement SpellStruct
private method whenTimerExpires takes nothing returns nothing
call KillUnit(this.targetUnit) // Kill the target of the spell.
call this.stopTimer(thistype.whenTimerExpires)
endmethod
method onEffect takes nothing returns nothing
call this.startTimer(thistype.whenTimerExpires,5.0) // Run .whenTimerExpires in 5.0 seconds.
endmethod
private static method onInit takes nothing returns nothing
set thistype.abil='Axxx'
endmethod
endstruct
*/
// Triggers:
// ¯¯¯¯¯¯¯¯¯¯¯
// - Trigger attaching works using GetHandleId and a hashtable.
//
// - .createTrigger(method) will create a trigger with the method as it's
// action. Do not use DestroyTrigger to remove this trigger, you must
// use .destroyTrigger(method) to destroy it instead. This saves having
// to store the trigger in any sort of variable, generally. Destroying
// a trigger using SpellStruct has protection against the double free
// bug in Warcraft III, even if you use TriggerSleepAction.
//
// - Creating a trigger in this way automatically adds a lock to the struct,
// and destroying it removes this lock. This is to guarantee that when
// a timer method fires, all data is available and valid (while a struct
// has locks, it will not be auto destroyed).
//
// - Manually calling .destroy on a spell struct will destroy all triggers
// for that struct automatically.
//
// - Example:
//
/* struct MySpell extends SpellStruct
implement SpellStruct
// Gets a random real between -300.0 and 300.0.
private static constant real targetOffset=300.0
private static constant method getOffset takes nothing returns real // inlines
return GetRandomReal(-thistype.targetOffset, thistype.targetOffset)
endmethod
// For skipping an order, when reissuing it.
private boolean skipOrder = false
private method onTargetPointOrder takes nothing returns nothing
if this.skipOrder then
set this.skipOrder=false
else
set this.skipOrder=true
call IssuePointOrderById(this.targetUnit, GetIssuedOrderId(), GetOrderPointX() + thistype.getOffset(), GetOrderPointY() + thistype.getOffset())
endif
endmethod
private method onEffect takes nothing returns nothing
// Blurs the aim of the target for 5.0 seconds.
call TriggerRegisterUnitEvent(this.createTrigger(thistype.onTargetPointOrder), this.targetUnit, EVENT_UNIT_ISSUED_POINT_ORDER)
call TriggerSleepAction(5.0) // a lock from the trigger will stop the struct from being destroyed.
call this.destroyTrigger(thistype.onTargetPointOrder)
endmethod
private static method onInit takes nothing returns nothing
set thistype.abil='Axxx'
endmethod
endstruct
*/
// AoE (Area of Effect) enumeration:
// ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
// - You may set the AoE of a spell instance using set this.aoe = x, or you
// may set the default value for the aoe of a spell using set
// thistype.defaultAoE = x. This will set the value of .aoe to the value
// specified before onCreate is called.
//
// - You may enumerate the units in AoE of the target point, the current
// position of the targetted object, or the caster. This requires a unit
// filter to be applied.
//
// - This enum does not clear the group before hand, like native enums.
// Actually, it should even be safe to use with dynamic groups.
//
/* struct MySpell extends SpellStruct
implement SpellStruct
private method myFilter takes unit u returns boolean
return not IsUnitType(u, UNIT_TYPE_DEAD)
endmethod
private method onEffect takes nothing returns nothing
local group g = CreateGroup()
call this.enumUnitsInAoE(g, thistype.myFilter) // Like a default WC3 ability, enum in range of the point targetted when channeling began.
call this.enumUnitsInAoETarget(g, thistype.myFilter) // Enums all units current within aoe of the targetted widget (unit/item/destructable).
call this.enumUnitsInAoECaster(g, thistype.myFilter) // Enums all units current within aoe of the caster.
//...
endmethod
private static method onInit takes nothing returns nothing
set thistype.abil='Axxx'
set thistype.defaultAoE=500.0
endmethod
endstruct
*/
// - You may also skip groups altogether and do actions for all units within
// aoe of the target point/target/caster.
//
/* struct MySpell extends SpellStruct
implement SpellStruct
private method myAction takes unit u returns nothing
if not IsUnitType(u, UNIT_TYPE_DEAD) then
call SetUnitX(u, this.casterX)
call SetUnitY(u, this.casterY)
endif
endmethod
private method onEffect takes nothing returns nothing
set this.aoe = this.level * 100.0 + 100.0
call this.forUnitsInAoE(thistype.myAction) // Like a default WC3 ability, units in range of the point targetted when channeling began.
call this.forUnitsInAoETarget(thistype.myAction) // For all units current within aoe of the targetted widget (unit/item/destructable).
call this.forUnitsInAoECaster(thistype.myAction) // For all units current within aoe of the caster.
//...
endmethod
private static method onInit takes nothing returns nothing
set thistype.abil='Axxx'
endmethod
endstruct
*/
// - You may also check, for a single unit, to see if it is within the AoE
// of the target point, target or caster, using the following:
// - this.isUnitInAoE(myUnit) // within aoe of target point.
// - this.isUnitInAoETarget(myUnit) // within aoe of target.
// - this.isUnitInAoECaster(myUnit) // within aoe of caster.
// - The above return booleans.
//
// - All AoE functionality takes into account the collision size of the
// enumerated units. This matches better with Warcraft III AoE detection,
// which highlights units the spell will hit in green for AoE abilities.
//
// Miscellaneous:
// ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
// - call this.forGroup(g, callback method takes unit)
// Example:
//
/* method myCallback takes unit u returns nothing
call SetWidgetLife(u, GetWidgetLife(this.caster))
endmethod
// in some method:
call this.forGroup(g, thistype.myCallback) // set life for all units in group to the caster's life.
*/
// - real this.getDistanceToTargetWidget() // The distance between the caster and the target widget.
// - real this.getDistanceToTargetPoint() // The distance between the caster and the original point targetted.
// - real this.getAngleToTargetWidget() // The angle from the caster to the target object (in radians).
// - real this.getAngleToTargetPoint() // The angle from the caster to the target location (in radians).
//
// Thanks:
// ¯¯¯¯¯¯¯¯¯
// - Romek, for helping me with the interface hint to make the methods
// optional.
//
library SpellStruct uses optional TimerUtils, optional Recycle
//===========================================================================
// Configurables
//
globals
private constant real MAX_UNIT_COLLISION_SIZE=256.0
endglobals
//===========================================================================
// Header Declarations
//
globals
private group GROUP=CreateGroup()
private hashtable STORE=InitHashtable() // attaches to:
// abilid, methodkey
// abilid, unit
// timer, key
// trigger, key
// spellstruct, method (timer)
// spellstruct, method (trigger)
endglobals
private function interface Method takes integer this returns nothing
private function interface UnitMethodFilter takes integer this, unit u returns boolean
private function interface UnitMethod takes integer this, unit u returns nothing
private function interface Allocator takes nothing returns integer
//===========================================================================
// Recursion stack
//
private module Stack
static thistype top=0 // thistype(0) throws a syntax error, does not compile
static method increment takes nothing returns nothing
set thistype.top=thistype(thistype.top+1)
endmethod
static method decrement takes nothing returns nothing
set thistype.top=thistype(thistype.top-1)
endmethod
endmodule
//===========================================================================
// Defaults
//
private interface DefaultsInterface
// For methods that are not implemented.
// In order of firing.
method onCreate takes nothing returns nothing defaults nothing
method onChannel takes nothing returns nothing defaults nothing
method onStartCast takes nothing returns nothing defaults nothing
method onEffect takes nothing returns nothing defaults nothing // May not always fire in casting a given spell.
method onFinish takes nothing returns nothing defaults nothing // May not fire in casting a spell, Blizzard's event response fails.
method onStopCast takes nothing returns nothing defaults nothing // Blizzard's event response sometimes fails.
method cleanup takes nothing returns nothing defaults nothing
endinterface
//===========================================================================
// Event Handlers
//
globals
private constant key ALLOCATOR
endglobals
globals//locals
private DefaultsInterface ThisCastData
private integer CastingAbility
endglobals
//! textmacro SpellStruct__EventResponse takes METHOD_NAME, FUNCTION_NAME, FIRST_EVENT, LAST_EVENT
private function $FUNCTION_NAME$ takes nothing returns boolean
set CastingAbility=GetSpellAbilityId()
static if $FIRST_EVENT$ then
set ThisCastData=DefaultsInterface(LoadInteger(STORE,CastingAbility,ALLOCATOR))
if ThisCastData==0 then
return false
endif
set ThisCastData=DefaultsInterface(Allocator(LoadInteger(STORE,CastingAbility,ALLOCATOR)).evaluate())
call SaveInteger(STORE,CastingAbility,GetHandleId(GetTriggerUnit()),ThisCastData)
else
set ThisCastData=DefaultsInterface(LoadInteger(STORE,CastingAbility,GetHandleId(GetTriggerUnit())))
endif
//call BJDebugMsg("$FUNCTION_NAME$ - Method: "+I2S(LoadInteger(STORE,CastingAbility,$METHOD_KEY$))+" for struct "+I2S(ThisCastData))
if ThisCastData!=0 then
call ThisCastData.$METHOD_NAME$.execute()
static if $LAST_EVENT$ then
call ThisCastData.cleanup.evaluate()
endif
endif
return false
endfunction
//! endtextmacro
//! runtextmacro SpellStruct__EventResponse("onChannel","OnChannel","true","false")
//! runtextmacro SpellStruct__EventResponse("onStartCast","OnStartCast","false","false")
//! runtextmacro SpellStruct__EventResponse("onEffect","OnEffect","false","false")
//! runtextmacro SpellStruct__EventResponse("onFinish","OnFinish","false","false")
//! runtextmacro SpellStruct__EventResponse("onStopCast","OnStopCast","false","true")
//===========================================================================
// Attachment.
//
private struct ChainAttach
trigger trigger
timer timer
integer instance
Method callback
thistype next
thistype prev
endstruct
//===========================================================================
// Timers (requires Attachment).
//
globals
private constant key TIMER_DATA
endglobals
private function SetTimerStruct takes timer t, integer data returns nothing
static if LIBRARY_TimerUtils then
call SetTimerData(t,data)
else
call SaveInteger(STORE,GetHandleId(t),TIMER_DATA,data)
endif
endfunction
private function GetTimerStruct takes timer t returns integer
static if LIBRARY_TimerUtils then
return GetTimerData(t)
else
return LoadInteger(STORE,GetHandleId(t),TIMER_DATA)
endif
endfunction
//! textmacro SpellStruct__GetTimer takes VARIABLE
static if LIBRARY_TimerUtils then
set $VARIABLE$=NewTimer()
elseif LIBRARY_Recycle then
set $VARIABLE$=Timer.get()
else
set $VARIABLE$=CreateTimer()
endif
//! endtextmacro
//! textmacro SpellStruct__ReleaseTimer takes TIMER
static if LIBRARY_TimerUtils then
call ReleaseTimer($TIMER$)
elseif LIBRARY_Recycle then
call RemoveSavedInteger(STORE,GetHandleId($TIMER$),TIMER_DATA)
call Timer.release($TIMER$)
else
call RemoveSavedInteger(STORE,GetHandleId($TIMER$),TIMER_DATA)
call PauseTimer($TIMER$)
call DestroyTimer($TIMER$)
//set $TIMER$=null // using globals, unnecessary.
endif
//! endtextmacro
//===========================================================================
// Triggers (requires Attachment).
//
globals
private constant key TRIGGER_DATA
endglobals
private function SetTriggerStruct takes trigger t, integer data returns nothing
call SaveInteger(STORE,GetHandleId(t),TRIGGER_DATA,data)
endfunction
private function GetTriggerStruct takes trigger t returns integer
return LoadInteger(STORE,GetHandleId(t),TRIGGER_DATA)
endfunction
//! textmacro SpellStruct__FlushTrigger takes TRIGGER
call RemoveSavedInteger(STORE,GetHandleId($TRIGGER$),TRIGGER_DATA)
//! endtextmacro
//===========================================================================
// AoE enumeration.
//
private struct EnumStack extends array
implement Stack
integer instance
real x
real y
real range
integer callback
group for
endstruct
private function InternalEnum takes nothing returns boolean // Could be recursive.
local unit u=GetFilterUnit() // can't be a global due to recursion.
if IsUnitInRangeXY(u,EnumStack.top.x,EnumStack.top.y,EnumStack.top.range) then // Factors in collision sizes.
if UnitMethodFilter(EnumStack.top.callback).evaluate(EnumStack.top.instance,u) then
call GroupAddUnit(EnumStack.top.for,u)
endif
endif
set u=null
return false
endfunction
private function InternalFor takes nothing returns boolean // Could be recursive.
local unit u=GetFilterUnit() // can't be a global due to recursion.
if IsUnitInRangeXY(u,EnumStack.top.x,EnumStack.top.y,EnumStack.top.range) then // Factors in collision sizes.
call UnitMethod(EnumStack.top.callback).execute(EnumStack.top.instance,u)
endif
set u=null
return false
endfunction
//===========================================================================
// Exposed Interface - SpellStruct
//
struct SpellStruct extends DefaultsInterface
//===========================================================================
// Various Spell-based Methods.
//
method getDistanceToTargetWidget takes nothing returns real
local real x=GetWidgetX(this.targetWidget)-this.casterX
local real y=GetWidgetY(this.targetWidget)-this.casterY
return SquareRoot(x*x+y*y)
endmethod
method getDistanceToTargetPoint takes nothing returns real
local real x=this.targetX-this.casterX
local real y=this.targetY-this.casterY
return SquareRoot(x*x+y*y)
endmethod
method getAngleToTargetWidget takes nothing returns real // radians.
return Atan2(GetWidgetY(this.targetWidget)-this.casterY,GetWidgetX(this.targetWidget)-this.casterX)
endmethod
method getAngleToTargetPoint takes nothing returns real // radians.
return Atan2(this.targetY-this.casterY,this.targetX-this.casterX)
endmethod
//===========================================================================
// ForGroup.
//
private static method forGroupCallback takes nothing returns nothing
call UnitMethod(EnumStack.top.callback).execute(EnumStack.top.instance,GetEnumUnit()) // bigger crimes have been committed, but not much bigger.
endmethod
method forGroup takes group g, UnitMethod callback returns nothing
call EnumStack.increment() // just borrowing that stack... would break if event reponses accessed the stack directly, but they don't.
set EnumStack.top.instance=this
set EnumStack.top.callback=callback
call ForGroup(g,function thistype.forGroupCallback)
call EnumStack.decrement()
endmethod
//===========================================================================
// AoE enumeration.
//
real aoe
method enumUnitsInAoE takes group whichGroup, UnitMethodFilter filter returns nothing /*eg: method filter takes unit u returns boolean*/
call EnumStack.increment()
set EnumStack.top.instance=this
set EnumStack.top.x=this.targetX
set EnumStack.top.y=this.targetY
set EnumStack.top.range=this.aoe
set EnumStack.top.callback=filter
set EnumStack.top.for=whichGroup
call GroupEnumUnitsInRange(GROUP,EnumStack.top.x,EnumStack.top.y,EnumStack.top.range+MAX_UNIT_COLLISION_SIZE,Filter(function InternalEnum))
call EnumStack.decrement()
endmethod
method enumUnitsInAoETarget takes group whichGroup, UnitMethodFilter filter returns nothing /*eg: method filter takes unit u returns boolean*/
call EnumStack.increment()
set EnumStack.top.instance=this
set EnumStack.top.x=GetWidgetX(this.targetWidget)
set EnumStack.top.y=GetWidgetY(this.targetWidget)
set EnumStack.top.range=this.aoe
set EnumStack.top.callback=filter
set EnumStack.top.for=whichGroup
call GroupEnumUnitsInRange(GROUP,EnumStack.top.x,EnumStack.top.y,EnumStack.top.range+MAX_UNIT_COLLISION_SIZE,Filter(function InternalEnum))
call EnumStack.decrement()
endmethod
method enumUnitsInAoECaster takes group whichGroup, UnitMethodFilter filter returns nothing /*eg: method filter takes unit u returns boolean*/
call EnumStack.increment()
set EnumStack.top.instance=this
set EnumStack.top.x=GetUnitX(this.caster)
set EnumStack.top.y=GetUnitY(this.caster)
set EnumStack.top.range=this.aoe
set EnumStack.top.callback=filter
set EnumStack.top.for=whichGroup
call GroupEnumUnitsInRange(GROUP,EnumStack.top.x,EnumStack.top.y,EnumStack.top.range+MAX_UNIT_COLLISION_SIZE,Filter(function InternalEnum))
call EnumStack.decrement()
endmethod
method forUnitsInAoE takes UnitMethod callback returns nothing /*eg: method callback takes unit u returns nothing*/
call EnumStack.increment()
set EnumStack.top.instance=this
set EnumStack.top.x=this.targetX
set EnumStack.top.y=this.targetY
set EnumStack.top.range=this.aoe
set EnumStack.top.callback=callback
call GroupEnumUnitsInRange(GROUP,EnumStack.top.x,EnumStack.top.y,EnumStack.top.range+MAX_UNIT_COLLISION_SIZE,Filter(function InternalFor))
call EnumStack.decrement()
endmethod
method forUnitsInAoETarget takes UnitMethod callback returns nothing /*eg: method callback takes unit u returns nothing*/
call EnumStack.increment()
set EnumStack.top.instance=this
set EnumStack.top.x=GetWidgetX(this.targetWidget)
set EnumStack.top.y=GetWidgetY(this.targetWidget)
set EnumStack.top.range=this.aoe
set EnumStack.top.callback=callback
call GroupEnumUnitsInRange(GROUP,EnumStack.top.x,EnumStack.top.y,EnumStack.top.range+MAX_UNIT_COLLISION_SIZE,Filter(function InternalFor))
call EnumStack.decrement()
endmethod
method forUnitsInAoECaster takes UnitMethod callback returns nothing /*eg: method callback takes unit u returns nothing*/
call EnumStack.increment()
set EnumStack.top.instance=this
set EnumStack.top.x=GetUnitX(this.caster)
set EnumStack.top.y=GetUnitY(this.caster)
set EnumStack.top.range=this.aoe
set EnumStack.top.callback=callback
call GroupEnumUnitsInRange(GROUP,EnumStack.top.x,EnumStack.top.y,EnumStack.top.range+MAX_UNIT_COLLISION_SIZE,Filter(function InternalFor))
call EnumStack.decrement()
endmethod
method isUnitInAoE takes unit u returns boolean
return IsUnitInRangeXY(u,this.targetX,this.targetY,this.aoe)
endmethod
method isUnitInAoETarget takes unit u returns boolean
return IsUnitInRangeXY(u,GetWidgetX(this.targetWidget),GetWidgetY(this.targetWidget),this.aoe)
endmethod
method isUnitInAoECaster takes unit u returns boolean
return IsUnitInRangeXY(u,GetUnitX(this.caster),GetUnitY(this.caster),this.aoe)
endmethod
//===========================================================================
// Auto Cleanup.
//
private boolean doAutoDestroy // set in module's create method.
/*protected*/ boolean hasStoppedCasting=false
method operator autoDestroy= takes boolean flag returns nothing
if flag then
if this.hasStoppedCasting and this.isNotLocked then
set this.hasStoppedCasting=false // double free safety
call this.destroy()
else
set this.doAutoDestroy=true
endif
else
set this.doAutoDestroy=false
endif
endmethod
method operator autoDestroy takes nothing returns boolean
return this.doAutoDestroy
endmethod
//===========================================================================
// Locking.
//
private integer lockLevel=0
method operator isLocked takes nothing returns boolean
return this.lockLevel>0
endmethod
method operator isNotLocked takes nothing returns boolean
return this.lockLevel==0
endmethod
method addLock takes nothing returns nothing
set this.lockLevel=this.lockLevel+1
endmethod
method removeLock takes nothing returns nothing
set this.lockLevel=this.lockLevel-1
if this.hasStoppedCasting and this.isNotLocked and this.doAutoDestroy then
set this.hasStoppedCasting=false // double free safety
call this.destroy()
endif
endmethod
//===========================================================================
// Attachment.
//
private static ChainAttach attachNode
private static ChainAttach attachHead
//! textmacro SpellStruct__CreateAttachmentHead
set thistype.attachHead=ChainAttach.create()
set thistype.attachHead.next=thistype.attachHead
set thistype.attachHead.prev=thistype.attachHead
//! endtextmacro
//! textmacro SpellStruct__CreateAttachmentNode takes HEAD
// Create node.
set thistype.attachNode=ChainAttach.create()
// Link node (at end of list).
set thistype.attachHead=$HEAD$
set thistype.attachHead.prev.next=thistype.attachNode
set thistype.attachNode.prev=thistype.attachHead.prev
set thistype.attachHead.prev=thistype.attachNode
set thistype.attachNode.next=thistype.attachHead
//! endtextmacro
//! textmacro SpellStruct__AttachmentChainLoop takes HEAD
set thistype.attachHead=$HEAD$
set thistype.attachNode=thistype.attachHead.next
loop
exitwhen thistype.attachNode==thistype.attachHead
//! endtextmacro
//! textmacro SpellStruct__AttachmentChainEndloop
set thistype.attachNode=thistype.attachNode.next
endloop
//! endtextmacro
//===========================================================================
// Timers (requires Locking).
//
private ChainAttach timerAttachments
private static method timerCallback takes nothing returns nothing
set thistype.attachNode=GetTimerStruct(GetExpiredTimer()) // first time since h2i/gamecache I've needed this.
call thistype.attachNode.callback.execute(thistype.attachNode.instance) // good reason to use T32 instead.
// cannot use thistype.attachNode anymore, value may have changed.
endmethod
method startTimer takes Method callback, real period returns nothing
debug if HaveSavedInteger(STORE,this,callback) then
debug call BJDebugMsg("SpellStruct Error: started periodic method twice for struct.")
debug endif
// Make node and attach data.
//! runtextmacro SpellStruct__CreateAttachmentNode("this.timerAttachments")
set thistype.attachNode.instance=this
set thistype.attachNode.callback=callback
// Create timer and attach data.
//! runtextmacro SpellStruct__GetTimer("thistype.attachNode.timer")
call SetTimerStruct(thistype.attachNode.timer,thistype.attachNode)
// Attach node to struct/method.
call SaveInteger(STORE,this,callback,thistype.attachNode)
// Start timer.
call TimerStart(thistype.attachNode.timer,period,true,function thistype.timerCallback)
call this.addLock()
endmethod
method stopTimer takes Method callback returns nothing
set thistype.attachNode=ChainAttach(LoadInteger(STORE,this,callback))
// Unchain attachment
set thistype.attachNode.next.prev=thistype.attachNode.prev
set thistype.attachNode.prev.next=thistype.attachNode.next
// Release Timer.
//! runtextmacro SpellStruct__ReleaseTimer("thistype.attachNode.timer")
call RemoveSavedInteger(STORE,this,callback)
call this.removeLock()
endmethod
//===========================================================================
// Triggers (requires Locking).
//
private ChainAttach triggerAttachments
private static method triggerCallback takes nothing returns boolean
set thistype.attachNode=GetTriggerStruct(GetTriggeringTrigger()) // first time since h2i/gamecache I've needed this.
call thistype.attachNode.callback.execute(thistype.attachNode.instance) // good reason to use T32 instead.
// cannot use thistype.attachNode anymore, value may have changed.
return false
endmethod
method createTrigger takes Method callback returns trigger
debug if HaveSavedInteger(STORE,this,callback) then
debug call BJDebugMsg("SpellStruct Error: created two triggers for the same method for struct.")
debug endif
// Make node and attach data.
//! runtextmacro SpellStruct__CreateAttachmentNode("this.triggerAttachments")
set thistype.attachNode.instance=this
set thistype.attachNode.callback=callback
// Create timer and attach data.
set thistype.attachNode.trigger=CreateTrigger()
call SetTriggerStruct(thistype.attachNode.trigger,thistype.attachNode)
// Attach node to struct/method.
call SaveInteger(STORE,this,callback,thistype.attachNode)
// Init trigger.
call TriggerAddCondition(thistype.attachNode.trigger,Filter(function thistype.triggerCallback))
call this.addLock()
return thistype.attachNode.trigger
endmethod
method destroyTrigger takes Method callback returns nothing
set thistype.attachNode=ChainAttach(LoadInteger(STORE,this,callback))
// Unchain attachment
set thistype.attachNode.next.prev=thistype.attachNode.prev
set thistype.attachNode.prev.next=thistype.attachNode.next
// Destroy trigger
//! runtextmacro SpellStruct__FlushTrigger("thistype.attachNode.trigger")
call DestroyTrigger(thistype.attachNode.trigger)
call RemoveSavedInteger(STORE,this,callback)
call this.removeLock()
endmethod
//===========================================================================
// Event responses.
//
readonly integer abilId
readonly unit caster
readonly integer level
readonly integer order
readonly unit targetUnit
readonly widget targetWidget
readonly destructable targetDest
readonly item targetItem
readonly location targetLoc
readonly real targetX // for target point of AoE spells.
readonly real targetY // for target point of AoE spells.
readonly player owner
method operator casterX takes nothing returns real
return GetUnitX(this.caster)
endmethod
method operator casterY takes nothing returns real
return GetUnitY(this.caster)
endmethod
private static location loc
static method create takes nothing returns thistype
local thistype this=thistype.allocate()
//===========================================================================
// Event responses.
//
set this.abilId=GetSpellAbilityId()
set this.caster=GetTriggerUnit()
set this.owner=GetOwningPlayer(this.caster)
set this.level=GetUnitAbilityLevel(this.caster,this.abilId)
set this.order=GetUnitCurrentOrder(this.caster)
// Target stuff
set this.targetUnit=GetSpellTargetUnit()
if this.targetUnit==null then
set this.targetDest=GetSpellTargetDestructable()
if this.targetDest==null then
set this.targetItem=GetSpellTargetItem()
if this.targetItem==null then
set this.targetWidget=null
set thistype.loc=GetSpellTargetLoc()
if thistype.loc==null then
set this.targetX=GetUnitX(this.caster)
set this.targetY=GetUnitY(this.caster)
else
set this.targetX=GetLocationX(thistype.loc)
set this.targetY=GetLocationY(thistype.loc)
call RemoveLocation(thistype.loc)
set thistype.loc=null // worthwhile
endif
else
set this.targetWidget=this.targetItem
set this.targetX=GetItemX(this.targetItem)
set this.targetY=GetItemY(this.targetItem)
endif
else
set this.targetWidget=this.targetDest
set this.targetItem=null
set this.targetX=GetWidgetX(this.targetDest) // shorter
set this.targetY=GetWidgetY(this.targetDest)
endif
else
set this.targetWidget=this.targetUnit
set this.targetDest=null
set this.targetItem=null
set this.targetX=GetUnitX(this.targetUnit)
set this.targetY=GetUnitY(this.targetUnit)
endif
//===========================================================================
// Attachment (timer & trigger).
//
//! runtextmacro SpellStruct__CreateAttachmentHead()
set this.timerAttachments=thistype.attachHead
//! runtextmacro SpellStruct__CreateAttachmentHead()
set this.triggerAttachments=thistype.attachHead
return this
endmethod
method destroy takes nothing returns nothing
if this.hasStoppedCasting then
set this.hasStoppedCasting=false // random double free protection on autoDestroy stuff.
else
call RemoveSavedInteger(STORE,this.abilId,GetHandleId(this.caster))
endif
//===========================================================================
// Timers
//
//! runtextmacro SpellStruct__AttachmentChainLoop("this.timerAttachments")
//! runtextmacro SpellStruct__ReleaseTimer("thistype.attachNode.timer")
call RemoveSavedInteger(STORE,this,thistype.attachNode.callback)
//! runtextmacro SpellStruct__AttachmentChainEndloop()
call this.timerAttachments.destroy()
//===========================================================================
// Triggers
//
//! runtextmacro SpellStruct__AttachmentChainLoop("this.triggerAttachments")
//! runtextmacro SpellStruct__FlushTrigger("thistype.attachNode.trigger")
call DestroyTrigger(thistype.attachNode.trigger)
call RemoveSavedInteger(STORE,this,thistype.attachNode.callback)
//! runtextmacro SpellStruct__AttachmentChainEndloop()
call this.triggerAttachments.destroy()
call this.deallocate()
endmethod
endstruct
//===========================================================================
// Exposed Module - SpellStruct
//
module SpellStruct
//===========================================================================
// AoE enumeration.
//
static real defaultAoE=0.
//===========================================================================
// Setting up the struct.
//
private static integer currentAbil=0
static method operator abil= takes integer abilId returns nothing
if thistype.currentAbil!=0 then
call RemoveSavedInteger(STORE,thistype.currentAbil,ALLOCATOR)
endif
set thistype.currentAbil=abilId
if abilId!=0 then
call SaveInteger(STORE,abilId,ALLOCATOR,thistype.create)
endif
endmethod
static method operator abil takes nothing returns integer
return thistype.currentAbil
endmethod
//===========================================================================
// Auto Cleanup.
//
private static boolean doAutoDestroyDefault=true
static method operator autoDestroyDefault= takes boolean flag returns nothing
set thistype.doAutoDestroyDefault=flag
endmethod
static method operator autoDestroyDefault takes nothing returns boolean
return thistype.doAutoDestroyDefault
endmethod
method cleanup takes nothing returns nothing // only runs if not destroyed
call RemoveSavedInteger(STORE,this.abilId,GetHandleId(this.caster))
set this.hasStoppedCasting=true // can't be readonly because of this.
if this.autoDestroy and this.isNotLocked then
call SpellStruct(this).destroy() // Jasshelper bug? Had to typecast "this".
endif
endmethod
//===========================================================================
// Struct Allocation.
//
private static method create takes nothing returns thistype
local thistype this=thistype.allocate()
// General stuff
set this.autoDestroy=thistype.autoDestroyDefault
set this.aoe=thistype.defaultAoE
//static if thistype.onCreate!=DEFAULTS.onCreate then
call this.onCreate.evaluate() // in case of thread terminate
//endif
return this
endmethod
endmodule
//===========================================================================
// Init
//
private struct Init extends array
private static method onInit takes nothing returns nothing
//===========================================================================
// Event Responses
//
local trigger t
//! textmacro SpellStruct__RegisterEvent takes EVENT, FUNCTION
set t=CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t,$EVENT$)
call TriggerAddCondition(t,Filter(function $FUNCTION$))
//! endtextmacro
//! runtextmacro SpellStruct__RegisterEvent("EVENT_PLAYER_UNIT_SPELL_CHANNEL","OnChannel")
//! runtextmacro SpellStruct__RegisterEvent("EVENT_PLAYER_UNIT_SPELL_CAST","OnStartCast")
//! runtextmacro SpellStruct__RegisterEvent("EVENT_PLAYER_UNIT_SPELL_EFFECT","OnEffect")
//! runtextmacro SpellStruct__RegisterEvent("EVENT_PLAYER_UNIT_SPELL_FINISH","OnFinish")
//! runtextmacro SpellStruct__RegisterEvent("EVENT_PLAYER_UNIT_SPELL_ENDCAST","OnStopCast")
set t=null
endmethod
endstruct
endlibrary
This is not an efficiency based system. It is not inefficient, but it is slightly heavyweight in some areas to ensure it runs stably and supports all reasonable uses, and cleans up even mild WC3 errors and provides strong functionality.
I intend to build on this in future. For now, there is a JassHelper bug that stands in the way of this system until I find a work-around or it is resolved. That bug is that all methods must be implemented - they are not optional as intended.
Demonstration #1 (Life Drain):
JASS:
struct LifeDrain extends SpellStruct
implement SpellStruct
// The following ripped from <a href="http://www.thehelper.net/forums/showthread.php?t=133865" class="link link--internal">http://www.thehelper.net/forums/showthread.php?t=133865</a>
private static method setDistanceToUnit takes unit from, unit to, real dist returns nothing
local real x=GetUnitX(to)-GetUnitX(from)
local real y=GetUnitY(to)-GetUnitY(from)
local real factor=dist/SquareRoot(x*x+y*y)
call SetUnitX(to,GetUnitX(from)+x*factor)
call SetUnitY(to,GetUnitY(from)+y*factor)
endmethod
private method periodic takes nothing returns nothing
//if not this.isUnitInAoECaster(this.targetUnit) then
call thistype.setDistanceToUnit(this.targetUnit,this.caster,this.aoe)
//endif
endmethod
implement T32x
method onEffect takes nothing returns nothing
call this.startPeriodic()
endmethod
method onFinish takes nothing returns nothing
call this.stopPeriodic()
endmethod
private static method onInit takes nothing returns nothing
set thistype.abil=039;ANdr039;
set thistype.defaultAoE=500.0
endmethod
endstruct
JASS:
struct DeathCoil extends SpellStruct
implement SpellStruct
method onCreate takes nothing returns nothing
set this.aoe=this.level*200
endmethod
private method kill takes unit u returns nothing
// note that you have access to everything in the struct in these for group methods.
call KillUnit(u)
endmethod
private method afterWait takes nothing returns nothing
call this.forUnitsInAoETarget(thistype.kill)
call this.stopTimer(thistype.afterWait)
endmethod
private method onEffect takes nothing returns nothing
//call BJDebugMsg(GetUnitName(this.caster)+" deathcoiled "+GetUnitName(this.targetUnit))
call this.startTimer(thistype.afterWait,2.0)
endmethod
private static method onInit takes nothing returns nothing
set thistype.abil=039;AUdc039;
endmethod
endstruct
Updates:
- Version 1.0.7: Made the .createTrigger() method actually return the trigger. Documented that you must call SpellStruct(this).destroy() to destroy due to a JassHelper bug.
- Version 1.0.6: Removed a bug that caused errors when casting abilities that were not SpellStruct based.
- Version 1.0.5 (BETA): Updated documentation. Made .targetX and .targetY set to the caster's x and y in spells that target neither a widget or point. Added .getAngleToTargetWidget() and .getAngleToTargetPoint().
- Version 1.0.4 (BETA): Got event response methods to be optional.
- Version 1.0.3 (BETA): Added .hasStoppedCasting, made the struct get destroyed when autoDestroy is set to true after the struct would normally be destroyed. Added .addLock() and .removeLock(), and .isNotLocked. Added .startTimer(method, timeout), .stopTimer(method), .createTrigger(method) and .destroyTrigger(method). Added .abilId event response. Fixed .forUnitsInAoE and .enumUnitsInAoE which worked in the demo by sheer chance (struct id happened to be the method id, was using the wrong one). Fixed onStopCast being called twice and structs never being auto-destroyed. Added support for careless destruction of the struct - all triggers and timers attached to will also immediate cease, and no more event responses shall occur.
- Version 1.0.2 (BETA): Fixed stack incrementing instead of decrementing.
- Version 1.0.1 (BETA): Added .owner, .casterX, .casterY, .getDistanceToTargetWidget(), .getDistanceToTargetPoint(), and split some functionality into the base SpellStruct to prevent code duplication.
- Version 1.0.0 (BETA): Release.