• 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!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[JASS] Events + Activatable Spell Questions

Status
Not open for further replies.
Level 14
Joined
Jul 26, 2008
Messages
1,009
I could use some help. I'm pretty sure I'm not doing this Activatable spell correctly, so helpful advice there would be great. I know Activatables can be tricky.

[Solved!]Also my other question is when it comes to multiple events how do they work? Lets say I'm doing this:

JASS:
    call TriggerRegisterAnyUnitEventBJ( Trigger, EVENT_PLAYER_UNIT_SPELL_CAST )
    call TriggerRegisterAnyUnitEventBJ( Trigger, EVENT_PLAYER_UNIT_ISSUED_ORDER )

Is it going to be an Or, or is it going to be an And? I'm trying to do a transformation spell and I want to save his abilities in local variables before he transforms, then give him new ones based on what his old ones are after the change. This will also happen when reverting back to normal. Perhaps there's an easier way to do this though, like finding out when the spell starts and ends? Recommendations?

[Unsolved!]Also, here is the Activatable spell that isn't working. So far the problem must lie somewhere in the Conditions or Trigger Initialize, because I'm getting no debug messages when testing.

JASS:
globals
    hashtable HashFindBlood
 set HashFindBlood = InitHashtable()
endglobals

function FindBlood_Timer takes nothing returns nothing
    local timer tim = GetExpiredTimer()
    local unit caster = LoadUnitHandle(HashFindBlood, GetHandleId(tim), 1)
    local group lowblood = CreateGroup()
    local unit temp
    local integer abillvl = GetUnitAbilityLevel(caster, 'fibl')
    local integer orderid = LoadInteger(HashFindBlood, GetHandleId(tim), 2)
    local real tempHP
    call BJDebugMsg("Timer Action Begins, skill activated/unactivated")
    //Register Locals
    if GetUnitAbilityLevel(caster, 'B000') <= 0 or GetUnitState(caster, UNIT_STATE_MANA) <= 10. or orderid == OrderId("unimmolation") then
        call BJDebugMsg("Unit has not enough mana, the Buff is gone, or the unit has used unimmolation")
    //Does unit not have ImmolationDetection buff, less than 10 mana, or is disabling the skill? Then release the timer and remove skills.
        call UnitRemoveAbility(caster, 'aspd')
        call UnitRemoveAbility(caster, 'mspd')
        call ReleaseTimer(tim)
    else
        call GroupEnumUnitsInRange(lowblood, GetUnitX(caster), GetUnitY(caster), 100.0 + 400 * abillvl, null)
        call BJDebugMsg("The enemies in range are grouped")
        //Put all units within 500 radius near caster into lowblood
        loop
            set temp = FirstOfGroup(lowblood)
        exitwhen temp == null
            if IsUnitEnemy(temp, GetOwningPlayer(caster)) then
                call BJDebugMsg("Unit checked is an enemy")
            //Make sure unit is enemy, otherwise remove from group
                set tempHP = GetWidgetLife(temp) / GetUnitState(temp, UNIT_STATE_MAX_LIFE)
                //Set a real variable to check the target's life as a %
                if tempHP <= 0.20 + 0.05 * abillvl and tempHP >= 0 then
                    call BJDebugMsg("Unit checked has low HP")
                //Check HP of enemy, if it's below 30% then run the calls
                    call UnitAddAbility(caster, 'mspd')
                    call SetUnitAbilityLevel(caster, 'mspd', abillvl * 2)
                    call UnitAddAbility(caster, 'aspd')
                    call SetUnitAbilityLevel(caster, 'aspd', abillvl)
                    //Add Movespeed and attackspeed skills to the caster and set them to the right level
                else
                    //If the current target is not less than 30% HP the skills are removed, the unit is removed, and we go on to check the next unit
                    call BJDebugMsg("Unit doesn't have LowHP, keep checking")
                    call UnitRemoveAbility(caster, 'mspd')
                    call UnitRemoveAbility(caster, 'aspd')
                    call GroupRemoveUnit(lowblood, temp)
                endif
            else
                call GroupRemoveUnit(lowblood, temp)
            endif
        endloop
    endif
 set caster = null
 set lowblood = null
endfunction
    
function FindBlood_Actions takes nothing returns nothing
 local unit caster = GetTriggerUnit()
 local timer tim = NewTimer()
 local integer orderid = GetIssuedOrderId()
 call BJDebugMsg("First actions fire")
 //Register caster, timer, and OrderId
    call SaveUnitHandle(HashFindBlood, GetHandleId(tim), 1, caster)
    call SaveInteger(HashFindBlood, GetHandleId(tim), 2, orderid)
    call TimerStart(tim, 0.5, true, function FindBlood_Timer)
    //Transfer variables, start time
 set caster = null
endfunction

function FindBlood_Conditions takes nothing returns boolean
    //Pretty much is the hero casting a spell with immolation ID, allows checking OrderID
    return GetUnitAbilityLevel(GetTriggerUnit(),'fibl') > 0 and (GetIssuedOrderId() == OrderId("unimmolation") or GetIssuedOrderId() == OrderId("immolation"))
endfunction
        
//===========================================================================
function InitTrig_FindBlood takes nothing returns nothing
 local trigger FindBlood = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ( FindBlood, EVENT_PLAYER_UNIT_ISSUED_ORDER )
    call TriggerAddCondition( FindBlood, function FindBlood_Conditions )
    call TriggerAddAction( FindBlood, function FindBlood_Actions )
endfunction

EDIT: I've recently attempted the spell in vJASS. Of course It's nowhere near perfect and I don't fully understand the tutorial provided, like how do I set the caster variable as the Triggering Unit? o_O It didn't really explain that or a multitude of other things. Also when I save, it gives this error:

Invalid typecast

For this line:

JASS:
//
    call SetTimerData(tim, integer(D))
//

Here is the vJASS code:

JASS:
scope FindBlood
    
globals
    private constant integer    SpellID = 'fibl'    //Spell Rawcode
    private constant integer    BuffID  = 'B000'    //Buff Rawcode
    private constant real       dur       = 0.1       //Periodic of timer
endglobals

private struct Data
    unit caster
    integer orderid
    
    static method create takes unit c returns Data
     local Data D = Data.allocate()
        set D.caster = c
        set D.orderid = id
     return D
    endmethod
     
     
     
endstruct

private function FindBlood_Timer takes nothing returns nothing
    local timer tim = GetExpiredTimer()
    local Data D = Data.create(GetTimerData(tim))
    local group lowblood = CreateGroup()
    local unit temp
    local integer abillvl = GetUnitAbilityLevel(D.caster, SpellID)
    local real tempHP
    call BJDebugMsg("Timer Action Begins, skill activated/unactivated")
    //Register Locals
    if GetUnitAbilityLevel(D.caster, BuffID) <= 0 or GetUnitState(D.caster, UNIT_STATE_MANA) <= 10. or D.orderid == OrderId("unimmolation") then
        call BJDebugMsg("Unit has not enough mana, the Buff is gone, or the unit has used unimmolation")
    //Does unit not have ImmolationDetection buff, less than 10 mana, or is disabling the skill? Then release the timer and remove skills.
        call UnitRemoveAbility(D.caster, 'aspd')
        call UnitRemoveAbility(D.caster, 'mspd')
        call ReleaseTimer(tim)
    else
        call GroupEnumUnitsInRange(lowblood, GetUnitX(D.caster), GetUnitY(D.caster), 500.0 + 400. * (abillvl - 1.), null)
        call BJDebugMsg("The enemies in range are grouped")
        //Put all units within 500 radius near caster into lowblood
        loop
            set temp = FirstOfGroup(lowblood)
        exitwhen temp == null
            if IsUnitEnemy(temp, GetOwningPlayer(D.caster)) then
                call BJDebugMsg("Unit checked is an enemy")
            //Make sure unit is enemy, otherwise remove from group
                set tempHP = GetWidgetLife(temp) / GetUnitState(temp, UNIT_STATE_MAX_LIFE)
                //Set a real variable to check the target's life as a %
                if tempHP <= 0.25 + 0.05 * (abillvl - 1.) and tempHP >= 0 then
                    call BJDebugMsg("Unit checked has low HP")
                //Check HP of enemy, if it's below 30% but the unit isn't dead then run the calls
                    call UnitAddAbility(D.caster, 'mspd')
                    call SetUnitAbilityLevel(D.caster, 'mspd', abillvl * 2)
                    call UnitAddAbility(D.caster, 'aspd')
                    call SetUnitAbilityLevel(D.caster, 'aspd', abillvl)
                    //Add Movespeed and attackspeed skills to the caster and set them to the right level
                else
                    //If the current target is not less than 30% HP the skills are removed, the unit is removed, and we go on to check the next unit
                    call BJDebugMsg("Unit doesn't have LowHP, keep checking")
                    call UnitRemoveAbility(D.caster, 'mspd')
                    call UnitRemoveAbility(D.caster, 'aspd')
                    call GroupRemoveUnit(lowblood, temp)
                endif
            else
                call GroupRemoveUnit(lowblood, temp)
            endif
        endloop
    endif
 call D.destroy()
 set lowblood = null
endfunction
    
private function FindBlood_Actions takes nothing returns nothing
 local timer tim = NewTimer()
 local Data D = Data.create(GetTriggerUnit())
    //Setup our data to be transfered
    call BJDebugMsg("First actions fire")
    call SetTimerData(tim, D)
    call TimerStart(tim, dur, true, function FindBlood_Timer)
 set tim = null
endfunction

private function FindBlood_Conditions takes nothing returns boolean
    //Pretty much is the hero casting a spell with immolation ID, allows checking OrderID
    return GetUnitAbilityLevel(GetTriggerUnit(),SpellID) > 0 and (GetIssuedOrderId() == OrderId("unimmolation") or GetIssuedOrderId() == OrderId("immolation"))
endfunction
        
//===========================================================================
function InitTrig_FindBlood takes nothing returns nothing
 local trigger FindBlood = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ( FindBlood, EVENT_PLAYER_UNIT_ISSUED_ORDER )
    call TriggerAddCondition( FindBlood, function FindBlood_Conditions )
    call TriggerAddAction( FindBlood, function FindBlood_Actions )
endfunction

endscope
 
Last edited:
Level 14
Joined
Nov 18, 2007
Messages
1,084
When you want your trigger to register multiple events, that method should work alright, but make sure that the actions won't conflict with each other...

Not really related to your problem but may be handy for future triggering...

Use coordinates instead of locations.

You don't need to set your local trigger variables to null.

I think you may want to use EVENT_PLAYER_UNIT_SPELL_EFFECT instead of EVENT_PLAYER_UNIT_SPELL_CAST

Use GetWidgetLife(temp) instead of GetUnitState(temp, UNIT_STATE_LIFE).

If you're using TimerUtils, you may not need to even use a hashtable... Then again, that only applies if you can use vJass.
 
Level 14
Joined
Jul 26, 2008
Messages
1,009
Thanks Watermelon, I updated my spell to reflect your suggested changes.

I've also added BJDebugMsg's so I could assess the issues as they arise. At present the trigger's main action won't even come up though, so the problem lies earlier, somewhere in the conditions or the trigger set up. Any insight would be nice, as I assume I set the code up properly.

As for the Events, I was wanting to get the spell data off the unit before the transformation occured, so I was thinking of using SPELL_CAST. But there's likely a better method. Also I'd like to skip the hashtables and use vJASS. I've got the manual and read about structs, privs, public, libraries, etc. a long time ago, but the manual was a bit too difficult to understand as it was rather technical.

Really the tough part is figuring out how to do this form thing properly. I'd rather not use Chaos, it has some issues. I think it also removes the level of the unit, and this spell is for heroes.

So I'm trying to figure out how to figure out when the transformation has occured so I can re-add the spells. That would solve the problem entirely.
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
Spotted a problem:
JASS:
call TriggerAddCondition(FindBlood, function FindBlood_Conditions)
should be
JASS:
call TriggerAddCondition(FindBlood, Condition(function FindBlood_Conditions))
A tutorial that I found useful for learning how to use structs and timers was this.

I agree with Justify and xBlackRose that you should initialize your global variables, if you can.

Edit: You may not need to even check if the user has deactivated immolation since checking for the buff should work fine. By the way, you may want to add another condition to your existing one since this could conflict with other spells based off of immolation. Example:
JASS:
GetIssuedOrderId() == OrderId("immolation") and GetUnitAbilityLevel(GetTriggerUnit(),YOUR_SPELL_ID) > 0
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
Also, why is it better to check for the buff? This would trigger every order while having the buff.
Sorry, I sort of goofed up on that part..
However, I think the way the spell checks for when the caster has deactivated the spell is inefficient right now as the second timer it creates from "unimmolation" doesn't really achieve anything...

In addition, a level check is unnecessary. If it is 0, it's impossbile to "immolation", isn't it?
This is only recommended for a specific scenario: the map uses multiple spells based off of Immolation.
If you don't have that level check, then other spells based off of Immolation and some abilities whose orderstrings can be changed to "immolation" (like Channel and Spellbook) could fire the FindBlood trigger.
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
If you have two spells of the same base spell, both are activated whenever you use one of the both. That's the reason why units can't have two of the same spell.
Apparently I still can't explain to you... So I will make an example.

SpellA is an ability based off of Immolation. It's the FindBlood ability.
SpellB is an ability based off of Immolation. It's a random ability used by the map.

UnitA is a unique unit who has SpellA.
UnitB is a unique unit who has SpellB.

When UnitA uses SpellA, the FindBlood trigger will be fired. Good, that's supposed to happen.
When UnitB uses SpellB, the FinalBlood trigger will be fired. But wait, that's not supposed to happen!

Do you understand why I would recommend the level check now?
 
Level 14
Joined
Jul 26, 2008
Messages
1,009
I get what you're saying about the unimmolate thing. I think.

So should I remove all things related to unimmolation and just let it run off of immolation, since it seems to take care of all it's issues of not doing it's effects when immolation is off, thanks to the buff check?

Also I fixed the problem with making sure findblood being cast is unique for the spell and orderstrings of "immolation" by putting in a spell level check. I'll update my current spell on the first post to reflect my changes and go and read up on structs.
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
I get what you're saying about the unimmolate thing. I think.

So should I remove all things related to unimmolation and just let it run off of immolation, since it seems to take care of all it's issues of not doing it's effects when immolation is off, thanks to the buff check?
There could be a problem with just checking for the buff though since if the player is fast enough, he could activate, deactivate, then reactivate immolation before the first timer could get released.

The tutorial I linked you is a little bit outdated but it can be useful somewhat for a starting point.

Your create method for data doesn't need to take all the variables that will be used by the struct.

For your problem with SetTimerData(tim, integer(D)), you haven't declared D yet. (And you could just do SetTimerData(tim, D). You need to declare D as local Data D = Data.create(PUT PARAMETERS HERE)
You may also want to take a look at this tutorial: http://www.hiveworkshop.com/forums/jass-ai-scripts-tutorials-280/vjass-oop-lesson-128236/

Sorry that I'm sort of bad at explaining these things...
 
Level 14
Joined
Jul 26, 2008
Messages
1,009
Ah, your explanation has helped me realize how the values of the variables are inputted into the structs. :D

Not to mention it cleared up the issues I was having understanding how to use and work structs. I'll see if I can't rearrange my vJASS so it reflects what I have learned just now.

Now I'm curious as how to improve the efficiency of the immolation checks. I need the buffcheck and the orderstring check, so what is inefficient about it?

EDIT: The code is returning errors, but I'm pretty sure I have it all set up properly according to the outline of the tutorial. :\ Here is the error:

JASS:
//!Error is: Undeclared variable id
        set s__FindBlood___Data_orderid[D]=id
//Note: It's within the method create.

JASS:
//!Error is: Cannot convert integer to unit
    local integer D= s__FindBlood___Data_create(GetTimerData(tim))
//NOTE: It is set below the Timer where the local variables are declared.


I've updated the code on the original post to reflect the changes I"ve made.

EDIT2: I've gotten the normal, non-vJASS working properly. I had to remove and re-add the trigger as it wasn't compiled properly. I could tell when the trigger wasn't returning the BJDebugMsg I put in the conditions. Still would like to get the vJASS working though, as understanding vJASS better would be so helpful ^^
 
Last edited:
Level 14
Joined
Jul 26, 2008
Messages
1,009
Enter Resting. . .

I don't think I understood a word of that, Just, but I'm pretty sure I already did that on watermelon's recommendation. I should remove that block of code and highlight the new one I need help with :/

I hate how when compiling vJASS for errors you get jarbled coding thanks to randomization. :\
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
In your create method for Data, add another parameter that takes "integer id" and make sure you also change that in your FindBlood_Actions.

Ex:
JASS:
static method create takes unit c, integer id returns Data
In your FindBlood_Actions:
JASS:
local Data D = Data.create(GetTriggerUnit(),GetIssuedOrderId())
Right now though, the check should still be buggy and should only be working now because of the buff check. Here's an example why:

Unit A casts the spell and its information will be stored by Struct A to determine the periodic checking of the spell. When it deactivates the spell, Struct B will be created to say the spell stopped. However, I don't think Struct B explicitly tells Struct A to stop because the caster has deactivated the spell; Struct A should have stopped because the buff was removed from the caster. In order words, Struct B was completely useless and didn't really achieve anything.

However, don't rely completely on the buff check as it can screw up your spell too. Time for another example:

Unit A activates the spell, setting off Struct A. Then it quickly deactivates the spell, creating Struct B, which would be useless and get destroyed. However, if the player is super spammy enough to activate Immolation within the timer check, 0.1 in this case, he could avoid making Struct A destroyed, causing its timer to still run effects while Struct C also run its own periodic function, making the unit have double the effects from FindBlood.
It may seem like an unlikely scenario, but if you had the timer check higher than 0.1, it could be a problem.

If you want it to work without bugs, I think you would need to use Table to pass information to a struct.
If you want further reference for making your spell I can post a code I've done to add an aura while Defend is on and remove it when Defend is off.
 
Last edited:
Level 14
Joined
Jul 26, 2008
Messages
1,009
Ahh thanks! I also figured out that I was doing Data.create(GetTimerData(tim)) when I should've just been doing Data(GetTimerData(tim)).

As well your suggestion also provided me with the help I needed. The code at least compiles correctly now, and I can start updating my spells that use hashtables to structs.

I'll look into tables while I temporarily throw a minor cooldown on my activatable as a getaround. But as per your example isn't there a way to bridge Struct B into telling it to stop Struct A from going so that Struct B's purpose is redeemed? I suppose that's where tables comes in?

Anyways an example would be great to see, as it will certainly tell me how to optimize my spell.
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
JASS:
//Spell is basically an improved Defend that bestows an aura for the caster. Needs TimerUtils and Table to at least work. xepreload is there for easier preloading of abilities. 
scope Defend initializer Init
    globals
        private constant string ORDER = "defend"
        private constant string UNORDER = "undefend"
        private constant integer SPELL_ID = 'A00U' //Raw id of the custom Defend ability.
        private constant integer SPELLBOOK_ID = 'A012' //Use a spellbook ability because you don't want the player to see the aura 
        private constant integer AURA_ID = 'A011'  //Aura ability in the spellbook. Need to have this to properly set its level
        private constant real TIMER_LOOP = 0.1 //The timer check.
        private HandleTable info //This will be used to pass info to structs. By the way, this can't be initialized. I'm not really sure why.
    endglobals
    
    private struct Data
        unit cast //Caster unit.
        integer lvl //Level of the caster. 
        boolean deactivate = false //This will be used to determine when the caster has issued the "undefend" order. Initialized as false to work properly.
        timer t //Timer variable will be used by the struct to do periodic things.
        
        static method onLoop takes nothing returns nothing
            local Data D = Data(GetTimerData(GetExpiredTimer())) //Remember that D.t is the same as GetExpiredTimer() in this case
            if UnitAlive(D.cast) and not D.deactivate then //Checks if the unit is alive and if he didn't deactivate the spell. UnitAlive is a native. To use it, you must have "native UnitAlive takes unit id returns boolean" somewhere before it's used.
                //Do any thing you want done by the spell periodically here.
                //Updates the AURA_ID if the D.lvl is lower than the current level.
                if D.lvl < GetUnitAbilityLevel(D.cast,SPELL_ID) then
                    set D.lvl = GetUnitAbilityLevel(D.cast,SPELL_ID)
                    call SetUnitAbilityLevel(D.cast,AURA_ID,D.lvl)
                endif
            else//Cleanup because one of the conditions wasn't met
                call UnitMakeAbilityPermanent(D.cast,false,SPELLBOOK_ID)
                call UnitRemoveAbility(D.cast,SPELLBOOK_ID)
                call info.flush(D.cast) //Do this to remove memory usage
                set D.cast = null //Set to null to prevent leaks
                call ReleaseTimer(D.t) //Release since it's no longer needed
                set D.t = null //Set to null to prevent leaks
                call D.destroy() //Always destroy your structs after you're done using them!
            endif
        endmethod
        
        static method create takes unit c returns Data
            local Data D = Data.allocate()
            set D.cast = c
            set D.lvl = GetUnitAbilityLevel(D.cast,SPELL_ID)
            //Do anything you want here upon the spell's activation 
            call UnitAddAbility(D.cast,SPELLBOOK_ID)    
            call UnitMakeAbilityPermanent(D.cast,true,SPELLBOOK_ID)
            call SetUnitAbilityLevel(D.cast,AURA_ID,D.lvl)
            //Timer stuff
            set D.t = NewTimer() //Creates the timer
            call SetTimerData(D.t,D) //Sets the D to the timer so it can carry info
            call TimerStart(D.t,TIMER_LOOP,true,function Data.onLoop) //Basically starts the timer
            set info[D.cast] = D //Sets D to the HandleTable so that it can be accessed by referring the the casting unit
            return D
        endmethod
    endstruct
    
    private function Deactivate takes nothing returns boolean
        local Data D
        if GetIssuedOrderId() == OrderId(UNORDER) and info.exists(GetTriggerUnit()) then //Checks if the issued order was "undefend" and if there was some info existing in the HandleTable for GetTriggerUnit()
            set D = info[GetTriggerUnit()] //Sets D to the struct stored by info. It refers to the triggering unit to get the correct struct.
            set D.deactivate = true //Do this to tell that the unit has deactivated the spell
        endif
        return false
    endfunction
    
    private function Activation takes nothing returns boolean
        if GetIssuedOrderId() == OrderId(ORDER) and GetUnitAbilityLevel(GetTriggerUnit(),SPELL_ID) > 0 then
            call Data.create(GetTriggerUnit()) //Calls the creation of the struct
        endif
        return false
    endfunction

    private function Init takes nothing returns nothing //Makes two different triggers so that they won't conflict with each other
        local trigger t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_ORDER)
        call TriggerAddCondition(t, Condition( function Activation)) //Faster to do things in here instead of actions. Don't do it if you use waits though.
        set t = CreateTrigger() //A different trigger from before will be created
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_ORDER)
        call TriggerAddCondition( t, Condition( function Deactivate))
        set info = HandleTable.create() //Make a new instance for this here.
        //Over here is just basically Preloading and doesn't really impact the spell in any way except to reduce initial lag when the abilities are first used.
        call DisableSpellbook(SPELLBOOK_ID) //A custom funciton I made so that this ability would get disabled for all players. 
        call XE_PreloadAbility(SPELLBOOK_ID) //A custom function in the xepreload library to preload abilities.
        call XE_PreloadAbility(AURA_ID)        
    endfunction
endscope
The use of Table makes it so that only one struct would be used when the unit casts Defend.
 
Status
Not open for further replies.
Top