SpellEvent

Scripts

14 years
edited 8 years
I feel the documentation in the library explains it well enough, so there's not much I can add here.
It requires Table, by the way.

Code (jass) Select
1
library SpellEvent initializer Init requires Table
2
3
//*****************************************************************
4
//*  SPELL EVENT LIBRARY 1.3
5
//*
6
//*  written by: Anitarf
7
//*  requires: -Table
8
//*
9
//*  Maps with many triggered spells require many triggers that run
10
//*  on spell events. Whenever a spell is cast, all those triggers
11
//*  need to be evaluated by the game even though only one actually
12
//*  needs to run. This library has been written to reduce the
13
//*  number of triggers in such maps; instead of having a trigger
14
//*  per spell, this library contains a single trigger which then
15
//*  runs only the code associated with the spell that's actually
16
//*  being cast.
17
//*
18
//*  Perhaps more significant than the marginal speed gain is the
19
//*  feature that allows you to access all the spell event
20
//*  responses from all spell events, something that the native
21
//*  functions senselessly do not support. With this system you can
22
//*  for example easily get the target unit of the spell on the
23
//*  casting finish event.
24
//*
25
//*  All functions following the Response function interface that
26
//*  is defined at the start of this library can be used to respond
27
//*  to spell events. You can register a response with one of the
28
//*  following functions, each for a different spell event:
29
//*
30
//*    function RegisterSpellChannelResponse takes integer spellId, Response r returns nothing
31
//*    function RegisterSpellCastResponse takes integer spellId, Response r returns nothing
32
//*    function RegisterSpellEffectResponse takes integer spellId, Response r returns nothing
33
//*    function RegisterSpellFinishResponse takes integer spellId, Response r returns nothing
34
//*    function RegisterSpellEndCastResponse takes integer spellId, Response r returns nothing
35
//*
36
//*  The first event occurs at the very start of the spell, when
37
//*  the spell's casting time begins; most spells have 0 casting
38
//*  time, so in most cases this first event occurs at the same
39
//*  time as the second one, which runs when the unit actually
40
//*  begins casting a spell by starting its spell animation. The
41
//*  third event occurs when the spell effect actually takes place,
42
//*  which happens sometime into the unit's spell animation
43
//*  depending on the unit's "Animation - Cast Point" property.
44
//*  The fourth event runs if the unit finishes casting the spell
45
//*  uninterrupted, which might be important for channeling spells.
46
//*  The last event runs when the unit stops casting the spell,
47
//*  regardless of whether it finished casting or was interrupted.
48
//*
49
//*  If you specify a spell id when registering a response then
50
//*  that response will only run when that ability is cast; only
51
//*  one function per ability per event is supported, if you
52
//*  register more responses then only the last one registered will
53
//*  be called. If, however, you pass 0 as the ability id parameter
54
//*  then the registered function will run for all spells. Up to
55
//*  8190 functions can be registered this way for each event.
56
//*  These functions will be called before the ability's specific
57
//*  function in the order they were registered.
58
//*
59
//*  This library provides its own event responses that work
60
//*  better than the Blizzard's bugged native cast event responses.
61
//*  They still won't work after a wait, but unlike Blizzard's
62
//*  natives they will work on all spell events.
63
//*
64
//*  Here are usage examples for all event responses:
65
//*
66
//*    local integer a = SpellEvent.AbilityId
67
//*    local unit u = SpellEvent.CastingUnit
68
//*    local unit t = SpellEvent.TargetUnit
69
//*    local item i = SpellEvent.TargetItem
70
//*    local destructable d = SpellEvent.TargetDestructable
71
//*    local location l = SpellEvent.TargetLoc
72
//*    local real x = SpellEvent.TargetX
73
//*    local real y = SpellEvent.TargetY
74
//*    local boolean b = SpellEvent.CastFinished
75
//*
76
//*  SpellEvent.TargetLoc is provided for odd people who insist on
77
//*  using locations, note that if you use it you have to cleanup
78
//*  the returned location yourself.
79
//*
80
//*  SpellEvent.CastFinished boolean is intended only for the
81
//*  EndCast event as it tells you whether the spell finished or
82
//*  was interrupted.
83
//*
84
//*
85
//*  Note that a few spells such as Berserk and Wind Walk behave
86
//*  somewhat differently from regular spells: they are cast
87
//*  instantly without regard for cast animation times, they do not
88
//*  interrupt the unit's current order, as well as any spell it
89
//*  may be casting. SpellEvent 1.1 now handles such spells without
90
//*  errors provided they are truly instant (without casting time).
91
//*
92
//*  It also turned out that a few rare abilities like Charge Gold
93
//*  & Lumber trigger a spell effect event, but not any other.
94
//*  SpellEvent 1.2 no longer ignores these lone effect events.
95
//*
96
//*  It also turned out that removing the spell ability would run
97
//*  the endcast event, so if the ability was removed from one of
98
//*  the effect callbacks the spell event data would get cleaned
99
//*  up prematurely, SpellEvent 1.3 fixes this issue.
100
//*****************************************************************
101
102
    // use the RegisterSpell*Response functions to add spell event responses to the library
103
    public function interface Response takes nothing returns nothing
104
105
// ================================================================
106
107
    private keyword effectDone
108
    private keyword init
109
    private keyword get
110
    private keyword destroy
111
112
    private struct spellEvent
113
        private static HandleTable casterTable
114
        boolean effectDone=false
115
116
        integer AbilityId
117
        unit CastingUnit
118
        unit TargetUnit
119
        item TargetItem=null
120
        destructable TargetDestructable=null
121
        real TargetX=0.0
122
        real TargetY=0.0
123
        boolean CastFinished=false
124
125
        readonly boolean destroyWhenDone=false
126
        
127
        // Some abilities like Berserk can be cast instantly without interrupting
128
        // the caster's current order, which includes any spells the caster may
129
        // already be casting. The following member allows the system to recover
130
        // the original spellEvent when such an instant spell overwrites it.
131
        private spellEvent interrupt
132
133
        method operator TargetLoc takes nothing returns location
134
            return Location(.TargetX, .TargetY)
135
        endmethod
136
        
137
        private static method create takes nothing returns spellEvent
138
            return spellEvent.allocate()
139
        endmethod
140
        static method init takes nothing returns spellEvent
141
            local spellEvent s=spellEvent.allocate()
142
            set s.AbilityId = GetSpellAbilityId()
143
            set s.CastingUnit = GetTriggerUnit()
144
            set s.TargetUnit = GetSpellTargetUnit()
145
            if s.TargetUnit != null then
146
                set s.TargetX = GetUnitX(s.TargetUnit)
147
                set s.TargetY = GetUnitY(s.TargetUnit)
148
            else
149
                set s.TargetDestructable = GetSpellTargetDestructable()
150
                if s.TargetDestructable != null then
151
                    set s.TargetX = GetDestructableX(s.TargetDestructable)
152
                    set s.TargetY = GetDestructableY(s.TargetDestructable)
153
                else
154
                    set s.TargetItem = GetSpellTargetItem()
155
                    if s.TargetItem != null then
156
                        set s.TargetX = GetItemX(s.TargetItem)
157
                        set s.TargetY = GetItemY(s.TargetItem)
158
                    else
159
                        set s.TargetX = GetSpellTargetX()
160
                        set s.TargetY = GetSpellTargetY()
161
                    endif
162
                endif
163
            endif
164
            set s.interrupt = spellEvent.casterTable[s.CastingUnit]
165
            set spellEvent.casterTable[s.CastingUnit]=integer(s)
166
            return s
167
        endmethod
168
        method destroy takes nothing returns nothing
169
            if SpellEvent!=0 then
170
                // the library is in the middle of running callbacks, this can happen if
171
                // the spell ability gets removed from the unit in one of the callbacks
172
                set .destroyWhenDone=true
173
                return
174
            endif
175
            if .interrupt!=0 then
176
                // this spell interrupted another spell, some instant spells can do this
177
                set spellEvent.casterTable[.CastingUnit]=.interrupt
178
            else
179
                call spellEvent.casterTable.flush(.CastingUnit)
180
            endif
181
            set .CastingUnit=null
182
            call .deallocate()
183
        endmethod
184
185
        static method get takes unit caster returns spellEvent
186
            return spellEvent(spellEvent.casterTable[caster])
187
        endmethod
188
        static method onInit takes nothing returns nothing
189
            set .casterTable=HandleTable.create()
190
        endmethod
191
    endstruct
192
    
193
    globals
194
        spellEvent SpellEvent=0
195
    endglobals
196
    
197
// ================================================================
198
199
    //! textmacro spellEvent_make takes name
200
    globals
201
        private Response array $name$CallList
202
        private integer $name$CallCount=0
203
        private Table $name$Table
204
    endglobals
205
206
    private function $name$Calls takes integer id returns nothing
207
        local integer i=0
208
        local spellEvent previous=SpellEvent
209
        set SpellEvent=spellEvent.get(GetTriggerUnit())
210
        loop
211
            exitwhen i>=$name$CallCount
212
            call $name$CallList[i].evaluate()
213
            set i=i+1
214
        endloop
215
        if $name$Table.exists(id) then
216
            call Response($name$Table[id]).evaluate()
217
        endif
218
        if SpellEvent.destroyWhenDone then
219
            set SpellEvent=0
220
            call spellEvent.get(GetTriggerUnit()).destroy()
221
        endif
222
        set SpellEvent=previous
223
    endfunction
224
225
    function RegisterSpell$name$Response takes integer spellId, Response r returns nothing
226
        if spellId==0 then
227
            set $name$CallList[$name$CallCount]=r
228
            set $name$CallCount=$name$CallCount+1
229
        else
230
            set $name$Table[spellId]=integer(r)
231
        endif
232
    endfunction
233
    //! endtextmacro
234
235
    //! runtextmacro spellEvent_make("Channel")
236
    //! runtextmacro spellEvent_make("Cast")
237
    //! runtextmacro spellEvent_make("Effect")
238
    //! runtextmacro spellEvent_make("Finish")
239
    //! runtextmacro spellEvent_make("EndCast")
240
241
// ================================================================
242
243
    globals
244
        // Morph abilities like Metamorphosis will cause an additional spell effect
245
        // event to run when the caster morphs back to its original form. To avoid
246
        // such duplicates, SpellEvent is designed to ignore any effect event that
247
        // does not have a matching channel event preceding it.
248
        // However, there are also rare abilities, like Charge Gold&Lumber, which
249
        // only cause an effect event to run, so these events must not be ignored
250
        // even though they occur without a matching channel event. This Table
251
        // tracks ability IDs of spells that did cause a channel event so that when
252
        // a spell is cast that doesn't cause one, its effect event is not ignored.
253
        private Table CastAfterChannel
254
    endglobals
255
256
    private function Channel takes nothing returns nothing
257
        call spellEvent.init()
258
        call ChannelCalls(GetSpellAbilityId())
259
    endfunction
260
261
    private function Cast takes nothing returns nothing
262
        call CastCalls(GetSpellAbilityId())
263
    endfunction
264
265
    private function Effect takes nothing returns nothing
266
        local spellEvent s=spellEvent.get(GetTriggerUnit())
267
        local integer id=GetSpellAbilityId()
268
        if s!=0 and not s.effectDone then
269
            set s.effectDone=true
270
            call EffectCalls(id)
271
            if not CastAfterChannel.exists(id) then
272
                set CastAfterChannel[id]=1
273
            endif
274
        elseif not CastAfterChannel.exists(id) then
275
            set s = spellEvent.init()
276
            call EffectCalls(id)
277
            call s.destroy()
278
        endif
279
    endfunction
280
281
    private function Finish takes nothing returns nothing
282
        set spellEvent.get(GetTriggerUnit()).CastFinished=true
283
        call FinishCalls(GetSpellAbilityId())
284
    endfunction
285
286
    private function EndCast takes nothing returns nothing
287
        call EndCastCalls(GetSpellAbilityId())
288
        call spellEvent.get(GetTriggerUnit()).destroy()
289
    endfunction
290
291
// ================================================================
292
293
    private function InitTrigger takes playerunitevent e, code c returns nothing
294
        local trigger t=CreateTrigger()
295
        call TriggerRegisterAnyUnitEventBJ( t, e )
296
        call TriggerAddAction(t, c)
297
        set t=null
298
    endfunction
299
    private function Init takes nothing returns nothing
300
        set ChannelTable=Table.create()
301
        set CastTable=Table.create()
302
        set EffectTable=Table.create()
303
        set FinishTable=Table.create()
304
        set EndCastTable=Table.create()
305
        call InitTrigger(EVENT_PLAYER_UNIT_SPELL_CHANNEL, function Channel)
306
        call InitTrigger(EVENT_PLAYER_UNIT_SPELL_CAST, function Cast)
307
        call InitTrigger(EVENT_PLAYER_UNIT_SPELL_EFFECT, function Effect)
308
        call InitTrigger(EVENT_PLAYER_UNIT_SPELL_FINISH, function Finish)
309
        call InitTrigger(EVENT_PLAYER_UNIT_SPELL_ENDCAST, function EndCast)
310
        set CastAfterChannel=Table.create()
311
    endfunction
312
313
endlibrary


Usage example

Code (jass) Select
1
scope QuenchLife initializer Init
2
// A single target channeling spell that kills the target unit if the channeling is finished uninterrupted.
3
4
    globals
5
        private constant integer ABILITY_ID = 'A000'
6
        private constant string DEATH_EFFECT = "WriteTheEffectModelPathHere"
7
        private constant string EFFECT_ATTACHMENT = "chest"
8
    endglobals
9
10
    private function OnCast takes nothing returns nothing
11
        local unit t=SpellEvent.TargetUnit //you couldn't get the target unit on this event without this script
12
        call AddSpecialEffectTarget(DEATH_EFFECT, t, EFFECT_ATTACHMENT) //even silly example spells need some eyecandy
13
        call KillUnit(t) //the unit won't give any bounty because I'm too lazy to use the damage natives, this is just an example anyway
14
        set t=null
15
    endfunction
16
17
    private function Init takes nothing returns nothing
18
        call RegisterSpellFinishResponse(ABILITY_ID, OnCast)
19
    endfunction
20
endscope
Yes, if all your spells are made with triggers and use this library then you can even do this

Code (jass) Select
1
library SpellChaos initializer Init
2
    globals
3
        private constant real CHANCE_FOR_SPELLS_TO_MISFIRE = 0.1
4
        private constant real CHANCE_FOR_SPELLS_TO_FIZZLE = 0.1
5
    endglobals
6
7
    private function OnChannel takes nothing returns nothing
8
        local unit t = SpellEvent.TargetUnit
9
        if GetRandomReal(0.0,1.0) < CHANCE_FOR_SPELLS_TO_FIZZLE then
10
            set SpellEvent.AbilityId = 0 //we can do this, or we can even transmute the spell into another spell. Crazy, isn't it?
11
                                         //of course, doing this at any time other than at the very start of the spell could cause bugs,
12
                                         //like a spell not finishing correctly because only half of it's code would run, for example.
13
        elseif t != null and GetRandomReal(0.0,1.0) < CHANCE_FOR_SPELLS_TO_MISFIRE then
14
            set SpellEvent.TargetUnit = SpellEvent.CastingUnit //redirect any spell, how awesome is that?
15
        endif
16
        set t = null
17
    endfunction
18
19
    private function Init takes nothing returns nothing
20
        call RegisterSpellChannelResponse(0, OnChannel) //again, changing spell response values should only be done on the first spell event
21
                                                     //before any spell-specific code runs, else you could run in trouble.
22
    endfunction
23
endscope