• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] Combo system level based. Need help with efficiency

Status
Not open for further replies.
Level 2
Joined
Oct 17, 2017
Messages
9
Hi guys, I'm messing around with a combo system to learn JASS. Yeah, I know, I'm late to the party haha.
So, I currently have a script that makes a combo with Q and W skills. More skills will be added as finishers and things like that but basically when you use Q or W, you get the next skills in the combo chain.

I wish the combos to be level based or even learn-able in a research building but I'm wondering what the most efficient way is to do this. I've programmed a bit in C++ and C# and it feels like I would be tempted to think I could make a class with my hero and fill in the variables for skills as needed and if they are empty, well, there's no skills added so the hero can't do the combo he doesn't have access to. Is there such a thing in JASS? Should I make global variables for every hero? There would technically be around 25 skills or so involved in every character's combos so that would make hundreds of variables. Any thoughts? Here is my code in case.

Please more that I am learning, any tips would be greatly appreciated wetter it has to do with the question or not. There might be some way to make this code half the length that I don't know.

EDIT: Extra question that I've been wondering too. Is there a way to detect if an ability is within a certain range of id? "Is ability ID between A020 and A024", if so then...
I mean, they are considered Ingegers

JASS:
scope SWSlashScope initializer Trig_SWSlash

globals
    private group grp
    private player tempOwner
    private unit un
    private timer t1=CreateTimer()
    private timer t2=CreateTimer()
    private integer skill_temp1
    private integer skill_temp2
    private integer ANIM
    private integer SLASH
    private integer DSLASH
    private integer abilDamage
    private real angBetween
    private real tempFacing
    private real ENUMRANGE
    private real SECTORSIZE
    private real speed
    private constant integer SLASH1='A021'
    private constant integer SLASH2='A022'
    private constant integer SLASH3='A023'
    private constant integer SLASH4='A024'
    private constant integer DSLASH1='A025'
    private constant integer DSLASH2='A026'
    private constant integer DSLASH3='A027'
    private constant integer DSLASH4='A028'
    private constant integer ANIMATION1=4
    private constant integer ANIMATION2=5
    private constant real speed1=1.0
    private constant real speed2=1.5
    private constant real ENUMRANGE1=200
    private constant real ENUMRANGE2=250
    private constant real SECTORSIZE1=0.5
    private constant real SECTORSIZE2=0.9
endglobals

//===========================================================================

    private function handler takes nothing returns nothing
        call UnitRemoveAbility(un, SLASH)
        call UnitRemoveAbility(un, DSLASH)
        call UnitAddAbility(un, skill_temp1)
        call UnitAddAbility(un, skill_temp2)
        call BJDebugMsg("Timer!")
        set  tempOwner = null
        set un = null
    endfunction

    private function f takes nothing returns boolean
        local unit filt = GetFilterUnit()
        local real theta
        set angBetween=Atan2(GetUnitY(filt)-GetUnitY(un),GetUnitX(filt)-GetUnitX(un))
        set theta = Cos(tempFacing-angBetween)
        if IsUnitEnemy(filt,tempOwner) and theta>SECTORSIZE  then
            call UnitDamageTarget(un, filt, abilDamage, true, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_NORMAL, null)
        endif
        set filt = null
        return false
    endfunction
 
    private function handler2 takes nothing returns nothing
        call SetUnitAnimationByIndex( un, ANIM)
        call BJDebugMsg("Timer2!")
        call GroupEnumUnitsInRange(grp,GetUnitX(un),GetUnitY(un),ENUMRANGE,Filter(function f))
        call DestroyGroup(grp)
    endfunction
 
    private function p takes nothing returns nothing
            call BJDebugMsg(I2S(abilDamage))
            set tempOwner=GetOwningPlayer(un)
            set tempFacing=GetUnitFacing(un)*bj_DEGTORAD
            set grp = CreateGroup()
            call TimerStart(t2,0.0,false,function handler2)
            call TimerStart(t1,speed,false,function handler)
    endfunction


//===========================================================================


function SWSlash_Condition takes nothing returns boolean

        if GetSpellAbilityId()==SLASH1 then
            set un = GetTriggerUnit()
            set abilDamage = GetHeroStr(un, true) * 10
            set ENUMRANGE=ENUMRANGE1
            set SECTORSIZE=SECTORSIZE1
            set SLASH = SLASH1
            set DSLASH = DSLASH1
            set ANIM = ANIMATION1
            set skill_temp1=SLASH2
            set skill_temp2=DSLASH2
            set speed= speed1
            call p()
            call UnitRemoveAbility(un, DSLASH)
        endif
     
        if GetSpellAbilityId()==SLASH2 then
            set un = GetTriggerUnit()
            set abilDamage = GetHeroStr(un, true) * 11
            set ENUMRANGE=ENUMRANGE1
            set SECTORSIZE=SECTORSIZE1
            set SLASH = SLASH2
            set DSLASH = DSLASH2
            set ANIM = ANIMATION1
            set skill_temp1=SLASH3
            set skill_temp2=DSLASH3
            set speed= speed1
         
            call p()
            call UnitRemoveAbility(un, DSLASH)
        endif
     
        if GetSpellAbilityId()==SLASH3 then
            set un = GetTriggerUnit()
            set abilDamage = GetHeroStr(un, true) * 12
            set ENUMRANGE=ENUMRANGE1
            set SECTORSIZE=SECTORSIZE1
            set SLASH = SLASH3
            set DSLASH = DSLASH3
            set ANIM = ANIMATION1
            set skill_temp1=SLASH4
            set skill_temp2=DSLASH4
            set speed= speed1
         
            call p()
            call UnitRemoveAbility(un, DSLASH)
        endif
     
        if GetSpellAbilityId()==SLASH4 then
            set un = GetTriggerUnit()
            set abilDamage = GetHeroStr(un, true) * 15
            set ENUMRANGE=ENUMRANGE1
            set SECTORSIZE=SECTORSIZE1
            set SLASH = SLASH4
            set DSLASH = DSLASH4
            set ANIM = ANIMATION1
            set skill_temp1=SLASH1
            set skill_temp2=DSLASH1
            set speed= speed1
         
            call p()
            call UnitRemoveAbility(un, DSLASH)
        endif
     
        if GetSpellAbilityId()==DSLASH1 then
            set un = GetTriggerUnit()
            set abilDamage = GetHeroStr(un, true) * 15
            set ENUMRANGE=ENUMRANGE2
            set SECTORSIZE=SECTORSIZE2
            set SLASH = SLASH1
            set DSLASH = DSLASH1
            set ANIM = ANIMATION2
            set skill_temp1=SLASH2
            set skill_temp2=DSLASH2
            set speed= speed2
         
            call p()
            call UnitRemoveAbility(un, SLASH)
        endif
     
        if GetSpellAbilityId()==DSLASH2 then
            set un = GetTriggerUnit()
            set abilDamage = GetHeroStr(un, true) * 17
            set ENUMRANGE=ENUMRANGE2
            set SECTORSIZE=SECTORSIZE2
            set SLASH = SLASH2
            set DSLASH = DSLASH2
            set ANIM = ANIMATION2
            set skill_temp1=SLASH3
            set skill_temp2=DSLASH3
            set speed= speed2
         
            call p()
            call UnitRemoveAbility(un, SLASH)
        endif
     
        if GetSpellAbilityId()==DSLASH3 then
            set un = GetTriggerUnit()
            set abilDamage = GetHeroStr(un, true) * 19
            set ENUMRANGE=ENUMRANGE2
            set SECTORSIZE=SECTORSIZE2
            set SLASH = SLASH3
            set DSLASH = DSLASH3
            set ANIM = ANIMATION2
            set skill_temp1=SLASH4
            set skill_temp2=DSLASH4
            set speed= speed2
         
            call p()
            call UnitRemoveAbility(un, SLASH)
        endif
     
        if GetSpellAbilityId()==DSLASH4 then
            set un = GetTriggerUnit()
            set abilDamage = GetHeroStr(un, true) * 25
            set ENUMRANGE=ENUMRANGE2
            set SECTORSIZE=SECTORSIZE2
            set SLASH = SLASH4
            set DSLASH = DSLASH4
            set ANIM = ANIMATION2
            set skill_temp1=SLASH1
            set skill_temp2=DSLASH1
            set speed= speed2
         
            call p()
            call UnitRemoveAbility(un, SLASH)
        endif
     
        return false
endfunction


//===========================================================================


function Trig_SWSlash takes nothing returns nothing
    set gg_trg_SWSlash = CreateTrigger(  )
        call TriggerRegisterAnyUnitEventBJ(gg_trg_SWSlash, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition( gg_trg_SWSlash, function SWSlash_Condition )
endfunction

endscope
 
Last edited by a moderator:
Level 26
Joined
Aug 18, 2009
Messages
4,097
I do not quite understand your description but you should model the mapping exactly as you described, like build yourself functions that take a number of source abilities and that store your result under that combination, make abstractions and code reusable. As you said, 'A020' is an integer (256s system), so you can just put (x >= 'A020' and x <= 'A024') for a boolean expression.
 
Level 2
Joined
Oct 17, 2017
Messages
9
Hmm.. Not entirely sure how to pass the numbers to the funcitons. I'm always afraid of performance issues when it comes to wc3.
If I do "If (x >= 'A020' and x <= 'A024') is true then", should I branch in a If then else for every apell ID or is there a way to directly run a funciton that will know by spell ID all the parameters needed by building a struct or somehting?
If what I am asking is basic knowledge, don't hesitate to tell me to go read more tutorials, this si literraly the first JASS script I ever write. A lot of it is inspired on different people explaining different topics.


EDIT: Also, for my previous description, what I meant is that I want hero to be able to learn combos. For instance, if the hero is level 1 and Presses Q, he wont get the next ability because he is not level 5 (this is an exemple). He would always hit with the first Q which is 'A020'. When he hits level 5, it would go 'A020' then 'A021' back to 'A020'. At level 10, he would be able to chain 'A020', 'A021' and 'A022' until he is high level enough to have all the combos. That would require an extra condition on all the checks to see if he meets the requirements.
I'm having a hard time putting words to my ideas , sorry haha
 
Last edited:
Level 26
Joined
Aug 18, 2009
Messages
4,097
While this might not be the easiest first problem, you can do like arbitrary mappings in wc3 and while I did say you can range compare the object ids above, I do not think doing so is recommendable because it injects dependencies. I doubt you have an id generator, it becomes difficult to change later on and is bad for code analysis. The usual way is to fix every ability id in a constant and use that constant for semantics and refactorability.

Sure you can dynamize the conditions and the whole algorithm but you need to have a structured data source. The coherences you just described must be translated to code.

pseudo code:

Code:
  constant q0 = 'A020';
  constant q1 = 'A021';
  constant q2 = 'A022';
  constant q3 = 'A023';

  def Combo {
    def getCollection();

    def add(abil, level) {
      collection.add(new Pair(abil, level));
    }
  }

  def init() {
    q = new Combo(q0);

    q.add(q1, 5);
    q.add(q2, 10);
    q.add(q3, 15);
  }

Of course the level requirement can be left out if the pattern is always the same. In jass there are arrays and hashtable mostly as standard containers but you should create a library for common stuff like linked lists, maps, sets etc. Afaik vJass does not come with a library, wurst does for example. You may search around the forums for standard containers if you do not want to write them yourself. Another option would be to hardcode the mappings like make a constant function:

Code:
  def getQ(level) {
    if (level < 5) return q0;

    if (level <= 5) return q1;
    if (level <= 10) return q2;
    if (level <= 15) return q3;

    return qLast;
  }
 
Level 2
Joined
Oct 17, 2017
Messages
9
Interesting. I'm going to have to look it up when I get home.
I did mess around with something which may be a good way to greatly reduce my code but I would need to be able to access something which I am not sure I can.
I will be "premapping" the combos by ID numbers. I revamped my code to make it more vJass standard (names and structure)

Abilities within 'A0xx' are first hits, 'A1xx' are seccond hits, etc.. each hero having 7 skills for each combo so a possibility of 14 heroes within the 'Axxx' range.

(sorry, not home so I can't post the exact code)

In my condition trigger I would have something like:
JavaScript:
    private function conditions takes nothing returns boolean 
        if GetSpellAbilityId()<'A400' then //to know that it's a hero ability
            set abilityId = GetSpellAbilityId()
            return true
        else
        endif      
        return false
    endfunction

The action trigger would have this, but is missing a key element:
JavaScript:
    private function actions takes nothing returns nothing
        local integer i = -6
        local unit caster = GetTriggerUnit()
        loop
        exitwhen i > 6
            call UnitRemoveAbility(caster, (abilityId+i))
        set i = i + 1
        endloop
      //some other code
        set caster = null
    endfunction

it would then run a times for S seconds (S being the speed of the skills, which I will have to make a library for my skills speed, damage, etc formulas I believe..).
Now the thing I tried to find is how to manipulate an ability ID to be able to add and remove the skills properly. By having "i" run from -6 to 6, I am sure to remove all the skills the hero has. Now, is there a way to detect which skills he actually had and "get unitAddAbility(caster, (removedAbility+100)) or something like that?
Or simply a way to loop through the abilities of a hero and remove them one by one adding ab ability with the same ID + 100?
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
GetUnitAbilityLevel(unit, abilId) or save the state yourself. Yes, if you insist on doing arithmetic on the ability ids. The single quotes in 'A000' indicate 256s digit system though, so 'A100' != 'A000' + 100, 'A100' = 'A000' + '100' - '000', however, jass allows you only one digit or four digits between '', so can write 'A000' + '0100' - '0000' instead. '0' is not 0 either, it's the extended ascii code, '0' is 48. Yeah, there are also libraries for that because there is a couple of applications where you require a conversion between both representations.
 
Level 2
Joined
Oct 17, 2017
Messages
9
Well I did manage to do 'A021' + 1 and it gave me 'A022'. Unless it was a fluke. The GetUnitAbilityLevel(unit, abilId) would imply that the ability has levels though (now that I think about it... that might be simpler!!) but what I am trying to find is lets say... Hero casts 'A138' then get all his current abilities ('A135', 'A136, 'A137', 'A138', 'A139, 'A140', 'A141') and add 100 to them (if that works as 1 did). Is that possible? Maybe working with ability levels would fix that.
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
GetUnitAbilityLevel returns 0 if the unit does not have the ability or its level if it does have it. It does not matter if the ability is a single-level ability or not, then it's just 1.

'A021' is 65*256^3+48*256^2+50*256^1+49*256^0

you see the last digit has a factor of 1. It happens '1'-'0' is 1, as the digit symbols in the ascii code have the same distance (are lined up). It would be different if ascii '0' mapped to decimal 10 and '1' to 77 for example.

As you said yourself, you can iterate over the abilities/check their existence or as I said memorize the abilities you added/removed (saving state).
 
Last edited:
Level 2
Joined
Oct 17, 2017
Messages
9
Thanks a lot for the help, I'll brainstorm on that and work on my code to make it a system instead of treating only the "slash" abilities of a swordsman. That way, all the heroes will be able to combo (swap skills when used) and separate codes will deal with the damage, effects, etc.
I appreciate the help! I'll post my code when done or if i have more questions in the next few days :)
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,179
One can use hashtables to map arbitrary data in a dynamic way. With a parent key of a unit's handle ID you can map values to that unit. With a parent key of an ability type ID you can map values to that ability type. Hashtables are also fast, only 2-3 times slower than a single array lookup.

One must remember to delete the parent key when its unit is removed otherwise the mapped data will persist indefinitely and could possibly be considered as having leaked. For child keys one can assign every piece of data a unique key which can be used to recall the piece of data. One may be tempted to generate this key from a string hash to assign it a human friendly name, but be aware that this has a small chance of hash collisions so is not recommended. JASSHelper can automatically generate unique key integers using special keywords. There is a 255 limit to the number of hash table objects that can be made during a session so one can allocate many for dedicated purposes but it is ill advised to allocate them dynamically. Hash tables use non dynamic bucket arrays, this means that their performance will degrade from O(1) towards O(n) if enough mappings are stored in them, this is in the order of thousands if not ten-thousands.
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
Hashtables vs globals/arrays is non-trivial. Some thousands of entries is nothing actually. Insertion/Deletion can become very slow. The bulk deletion functions FlushChildHashtable/FlushParentHashtable are blazingly fast however. I used to apply this when an object is being destroyed, discarding all its data at once because they were stored under the same parent key + objects were scattered onto different hashtables by assigning them a hashtable on allocation. On the other hand, you often require more clean up, especially when there are other objects involved. It might make sense to install extra routines then that dissolve their parts but do not touch the destroyed object's mappings or that come as bulk instructions.

Ex:
A unit can have aura objects stacked on it, storing them in a list. During the unit's life cycle, you can individually add and remove auras. When the unit faces destruction, you would iterate over the list to destroy the owned aura objects but instead of calling the normal remove aura method, you just invoke their destructor and flush the whole list afterwards.

Data structures such as lists fit badly into the array-OO of vJass or wurst, it's common to realize them via hashtable but you may argue if the data structure could use the unit's id as parent key because then you could even reduce the aforementioned flushing of different attributes to one per unit. Then again, maybe some other part of the code wants to read the auras of the unit after they were destroyed and mistakenly can still access them, so you have to pay attention or insert additional checks like if the unit is being destroyed.

A common use case of hashtables is also to create a link from native objects to jass wrappers. You want to store your own data on units and have a Unit class. The native events of wc3 are still required however, so you obtain the wrapper from the event responses to be able to enter your sandbox. (Units do have the custom value field however.)

Lastly, there are spezialized use cases like when you want to map to a combination/tuple of sources. Ex: Assigning temperature values to (x, y, z)-coordinates.
 
Status
Not open for further replies.
Top