| 02-09-2009, 07:00 AM | #1 |
What is Wc3 missing? An easy way to make spells with multiple targets. Thus is the reason for making this system of mine. I hope to create a simple way to configure spells that, for example, switch the position of two target units on the battlefield, destroy 5 target trees, or even create a death field within 6 target points. Here is the code I have so far: JASS:library MultiTargeting initializer Init requires LastOrder, TimerUtils globals private constant string CANCEL_ORDER = "taunt" //In theory this can be any order private constant integer CANCEL_ID = 'cncl' //Change it if you want private constant string CANCEL_HOTKEY = "l" //Must be lowercase private constant real CANCEL_DETECT_PERIOD = 0.04 private constant real CANCEL_MARGIN = 0.10 private constant integer MAX_NUMBER_OF_TARGETS = 10 //This indirectly limits the number of available instances of //The MultiTargetHolder struct, which shouldn't be an issue anyway constant integer MULT_POINT_TARGET = 1 //Constants used in return values constant integer MULT_UNIT_TARGET = 2 constant integer MULT_ITEM_TARGET = 3 constant integer MULT_DESTRUCTABLE_TARGET = 4 constant integer NEXT_POINT = 1 constant integer CANCEL = 2 constant integer CAST = 3 endglobals //Objectmerger for Cancel function interface MultiTargetCondition takes nothing returns integer function interface MultiTargetAction takes nothing returns nothing globals private integer CancelOrderId = 0 private keyword MultiTargetHolder public MultiTargetHolder array MTHs private integer N = 0 private MultiTargetHolder array PeriodicMTHs private timer T = null public MultiTargetHolder CurrentMTH = 0 private trigger CancelTrig = null private trigger TargetTrig = null private SpellInfo array PSpells private SpellInfo array USpells private SpellInfo array ISpells private SpellInfo array DSpells private integer PN = 0 private integer UN = 0 private integer IN = 0 private integer DN = 0 endglobals function GetCurrentTargetHolder takes nothing returns MultiTargetHolder return CurrentMTH endfunction private struct SpellInfo integer Index = 0 integer Id = 0 integer Order = 0 string HK = "" MultiTargetCondition Cond = null MultiTargetAction Cancel = null endstruct struct MultiTargetHolder unit O = null integer Targets = 0 integer Index = 0 integer Type = 0 player P = null timer T = null SpellInfo SI = 0 real array X[MAX_NUMBER_OF_TARGETS] real array Y[MAX_NUMBER_OF_TARGETS] unit array U[MAX_NUMBER_OF_TARGETS] item array I[MAX_NUMBER_OF_TARGETS] destructable array D[MAX_NUMBER_OF_TARGETS] static method Periodic takes nothing returns nothing local integer J = 0 local MultiTargetHolder M loop set M = PeriodicMTHs[J] if GetLocalPlayer() == M.P then call ForceUIKey(CANCEL_HOTKEY) endif set J = J+1 exitwhen J >= N endloop endmethod method AddToStack takes nothing returns nothing call UnitAddAbility(.O, CANCEL_ID) set PeriodicMTHs[N] = this set .Index = N set N = N+1 if N == 1 then call TimerStart(T, CANCEL_DETECT_PERIOD, true, function MultiTargetHolder.Periodic) endif endmethod method RemoveFromStack takes nothing returns nothing call UnitRemoveAbility(.O, CANCEL_ID) set N = N-1 if N == 0 then call PauseTimer(T) else set PeriodicMTHs[.Index] = PeriodicMTHs[N] endif endmethod static method Refresh takes nothing returns nothing local MultiTargetHolder M = GetTimerData(GetExpiredTimer()) call ReleaseTimer(M.T) call M.AddToStack() endmethod method NextTarget takes nothing returns nothing call DisableTrigger(CancelTrig) call DisableTrigger(TargetTrig) call PauseUnit(.O, true) call IssueImmediateOrder(.O, "stop") call PauseUnit(.O, false) call EnableTrigger(CancelTrig) call EnableTrigger(TargetTrig) if GetLocalPlayer() == .P then call ForceUIKey(.SI.HK) endif set .T = NewTimer() call SetTimerData(.T, this) call TimerStart(.T, CANCEL_MARGIN, false, function MultiTargetHolder.Refresh) endmethod static method create takes unit O, integer Type, SpellInfo SI returns MultiTargetHolder local MultiTargetHolder M = TargetHolder.allocate() set M.O = O set M.Type = Type set M.P = GetOwningPlayer(O) set M.SI = SI return M endmethod method onDestroy takes nothing returns nothing set MTHs[GetUnitIndex(.O)] = 0 endmethod endstruct private function OnTargetOrder takes nothing returns boolean local eventid E = GetTriggerEventId() local integer OId = GetIssuedOrderId() local integer J = 0 local SpellInfo SI local unit O = GetTriggerUnit() local MultiTargetHolder M = 0 local integer Index local unit U local item I local destructable D if E == EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER then loop set SI = PSpells[J] if SI.Order == OId and GetUnitAbilityLevel(O, SI.Id) > 0 then set Index = GetUnitIndex(O) set M = MTHs[Index] if M == 0 then set M = MultiTargetHolder.create(O, MULTI_POINT_TARGET, SI) set MTHs[Index] = M endif set M.X[M.Targets] = GetOrderTargetX() set M.Y[M.Targets] = GetOrderTargetY() set M.Targets = M.Targets+1 exitwhen true endif set J = J+1 exitwhen J >= PN endloop else set U = GetOrderTargetUnit() if U != null then loop set SI = USpells[J] if SI.Order == OId and GetUnitAbilityLevel(O, SI.Id) > 0 then set Index = GetUnitIndex(O) set M = MTHs[Index] if M == 0 then set M = MultiTargetHolder.create(O, MULTI_DESTRUCTABLE_TARGET, SI) set MTHs[Index] = M endif set M.U[M.Targets] = U set M.Targets = M.Targets+1 set U = null exitwhen true endif set J = J+1 exitwhen J >= UN endloop else set I = GetOrderTargetItem() if I != null then loop set SI = ISpells[J] if SI.Order == OId and GetUnitAbilityLevel(O, SI.Id) > 0 then set Index = GetUnitIndex(O) set M = MTHs[Index] if M == 0 then set M = MultiTargetHolder.create(O, MULTI_DESTRUCTABLE_TARGET, SI) set MTHs[Index] = M endif set M.I[M.Targets] = I set M.Targets = M.Targets+1 set I = null exitwhen true endif set J = J+1 exitwhen J >= IN endloop else set D = GetOrderTargetDestructable() if D != null then loop set SI = DSpells[J] if SI.Order == OId and GetUnitAbilityLevel(O, SI.Id) > 0 then set Index = GetUnitIndex(O) set M = MTHs[Index] if M == 0 then set M = MultiTargetHolder.create(O, MULTI_DESTRUCTABLE_TARGET, SI) set MTHs[Index] = M endif set M.D[M.Targets] = D set M.Targets = M.Targets+1 set D = null exitwhen true endif set J = J+1 exitwhen J >= DN endloop endif endif endif endif if M != 0 then call M.RemoveFromStack() set CurrentMTH = M set Index = M.SI.Cond.evaluate() if Index == NEXT then call M.NextTarget() elseif Index == CANCEL then call M.SI.Cancel.execute() call M.destroy() endif endif return false endfunction private function OnCancelOrder takes nothing returns boolean local MultiTargetHolder M local unit U = GetTriggerUnit() if GetIssuedOrderId() == CancelOrderId and GetUnitAbilityLevel(U, CANCEL_ID) > 0 then set M = MTHs[GetUnitIndex(U)] set CurrentMTH = M call M.SI.Cancel.execute() call M.RemoveFromStack() call M.destroy() endif set U = null return false endfunction function RegisterMultiTargetEvent takes integer EventType, integer SpellId, string SpellOrder, string SpellHotKey, MultiTargetCondition Cond, MultiTargetAction Act, MultiTargetAction Cancel returns nothing local SpellInfo SI = SpellInfo.create() set SI.Index = N set SI.Id = SpellId set SI.Order = OrderId(SpellOrder) set SI.HK = SpellHotKey set SI.TN = 1 set SpellInfos[N] = SI set N = N+1 if EventType == MULT_POINT_TARGET then set PSpells[PN] = SI set PN = PN+1 elseif EventType == MULT_UNIT_TARGET then set USpells[PN] = SI set UN = UN+1 elseif EventType == MULT_ITEM_TARGET then set ISpells[PN] = SI set IN = IN+1 elseif EventType == MULT_DESTRUCTABLE_TARGET then set DSpells[PN] = SI set DN = DN+1 endif endfunction private function Init takes nothing returns nothing set SpellInfos[0] = 0 set PSpells[0] = 0 set USpells[0] = 0 set ISpells[0] = 0 set DSpells[0] = 0 set CancelOrderId = OrderId(CANCEL_ORDER) set T = CreateTimer() set CancelTrig = CreateTrigger() call TriggerRegisterAnyUnitEvent(CancelTrig, EVENT_PLAYER_UNIT_ISSUED_ORDER) call TriggerAddCondition(CancelTrig, Condition(function OnCancelOrder) set TargetTrig = CreateTrigger() call TriggerRegisterAnyUnitEvent(TargetTrig, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER) call TriggerRegisterAnyUnitEvent(TargetTrig, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER) call TriggerAddCondition(TargetTrig, Condition(function OnTargetOrder) endfunction endlibrary At any rate, the way this works is you would call RegisterMultiTargetEvent(<Type of targeting>, <The spell cast that triggers this event>, <The orderstring of said spell>, <The hotkey of said spell>, <A function to evaluate when a new target is 'found;>, <A function to execute when the targeting is cancelled>) The "type" can be MULTI_POINT_TARGET, MULTI_UNIT_TARGET, MULTI_ITEM_TARGET, or MULTI_DESTRUCTABLE_TARGET. The first function would have the format function interface MultiTargetCondition takes nothing returns integer, and would return one of three things: NEXT, CANCEL, or CAST.
The cancel function can neither take nor return anything, but may do whatever it wants. It is not required that it destroy the Targeting instance. And that's pretty much as best as I can explain it... I'll give an example. This spell would switch the position of two units: JASS:scope Switch initializer Init globals private constant integer SPELL_ID = 'Swch' private constant string SPELL_HOTKEY = "w" private constant string SPELL_ORDER = "transmute" endglobals private function TargetCancel takes nothing returns nothing local MultiTargetHolder M = GetCurrentTargetHolder() if M.Targets == 1 then call SetUnitVertexColor(M.U[0], 255, 255, 255, 0) endfunction private function TargetConditions takes nothing returns integer local MultiTargetHolder M = GetCurrentTargetHolder() if M.Targets == 1 then if GetLocalPlayer() == M.P then call SetUnitVertexColor(M.U[0], 150, 150, 255, 0) //Unless I got alpha mixed up endif return NEXT elseif M.Targets == 2 then return CAST endif endfunction private function CastConditions takes nothing returns boolean return GetSpellAbilityId() == SPELL_ID endfunction private function Cast takes nothing returns nothing local MultiTargetHolder M = MTHs[GetUnitIndex(GetTriggerUnit())] local real X0 = GetUnitX(M.U[0]) local real Y0 = GetUnitY(M.U[0]) local real X1 = GetUnitX(M.U[1]) local real Y1 = GetUnitY(M.U[1]) call SetUnitX(M.U[0], X1) call SetUnitY(M.U[0], Y1) call SetUnitX(M.U[1], X0) call SetUnitY(M.U[1], Y0) call M.destroy() endfunction private function Init takes nothing returns nothing local trigger T = CreateTrigger() call RegisterMultiTargetEvent(MULTI_UNIT_TARGET, SPELL_ID, SPELL_ORDER, SPELL_HOTKEY, function TargetConditions, function TargetCancel) call TriggerRegisterAnyUnitEventBJ(T, EVENT_PLAYER_UNIT_SPELL_EFFECT) call TriggerAddCondition(T, Condition(function CastConditions)) call TriggerAddAction(T, function Cast) set T = null endfunction endscope Yeah... So I'm looking for comments on the syntax/how things are done. And I guess just showcasing the idea here. An issue with this system right now is that you can only have multiple targets of the same type; in the future I would like to make it so that you can have any type of targets for each time you register the event. This would require some reworking of things with actually casting the spell, and stuff like that. |
| 02-09-2009, 10:14 AM | #2 |
Your 'MULTI_..._TARGET' constants actually say 'MULT'. You forgot the I. Just noticed you used 'MULTI' in the example spell. EDIT: You also forgot an 'endif' in the example code. |
| 02-09-2009, 10:26 AM | #3 |
I didn't read the script 'coz I am in a hurry to say that I think this is a very good idea. I think Anitarf or somebody made something like this for 'Tripwire' - a submitted spell. When I tested it I also though "what a good idea". lol |
| 02-10-2009, 05:29 AM | #4 |
And the method Anitarft used for Tripwire (with a few changes) is the method I used in Flare for the 10th Spell Session. What I meant by "suggestions on syntax" is: how would you envision this working? (Not: where are some syntax errors?) I mean; how would you like to access the stored targets when the spell is cast, and that sort of thing? I sort-of have the struct thing going on, but I don't know if that's a very good solution. Perhaps there's a more elegant way to go about this? |
| 02-10-2009, 05:38 AM | #5 |
Maybe something like this: call TriggerRegisterMultiSpellEffect( t, spellid ) ( or something similair ) and then in actions GetOrderPointX1() GetOrderPointY1() GetOrderTarget1() or Targets.x(1) Targets.y(1) or Targets.x[1] Targets.y[1] Targets.widget[1] Targets.unit[1] I would probably prefer the last one mentioned |
| 02-10-2009, 06:18 AM | #6 |
The problem with doing it through triggers is this: The condition function needs to differentiate between 3 possible scenarios (cancel, get another target, or continue with cast), and you can't do that with just returning a boolean. Also, it doesn't include re-issuing order functionality yet, but I plan to add that by implementing LastOrder. Edit: I like the idea of caster:Target1 syntax. |
| 02-10-2009, 11:27 PM | #7 |
I don't understand what you're talking about, and I think it's because you don't get what I mean. :P Anyway, if I were to make my own TriggerRegister event, the user would also add a conditionfunc to that trigger, which I would evaluate to see what should be done when a target is detected. That would need to determine whether to continue finding targets, let the cast happen, or cancel the casting. However, this conditionfunc can only return true or false, which is not sufficient. When to do all three of those could be different for all spells, so the user needs to supply an appropriate function (I can't just evaluate some default system function), which is why I can't just use the trigger's ConditionFunc. I can't think of a way to set it up so that the user doesn't need to supply the proper function (TargetConditions, in the Switch spell) and can still use TriggerAddCondition() and TriggerAddAction() normally. Aside from that, there needs to be a cancel function too. |
| 02-11-2009, 12:18 AM | #8 |
I wouldn't alter how triggers work. -I'd keep it as: TriggerRegisterSomeEvent TriggerAddCondition TriggerAddAction Maybe you can just detect when a spell is casted, determine the spell and the configuration of its multi target aspect according to the system. |
