• 🏆 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!

Custom buffs anyone?

Status
Not open for further replies.
Level 21
Joined
Mar 27, 2012
Messages
3,232
I've been brainstorming about this for ages and now finally reached a graspable solution. Atm I have yet to iron out all the kinks, but the prototype mostly works.

Why custom buffs?
Default blizzard buffs come with many limitations:
1. Must always use an ability to grant them.
2. Similar abilities usually don't stack, meaning that you can't have variations of an ability in use.
3. Stat gains have to be premade in object editor.
4. No decent way to add new stat types, such as flat lifesteal.

By now I have fixed all of them.

At first I made a bonusmod, but that didn't prove to be enough, because a bonusmod is only a core system. It does nothing more than provide stats, but it does not keep track of their sources.

Features:
*Can place an ability on the unit(if the specified integer(buff ID) corresponds to one). This is mostly for visuals, but can be used for else too.
*All stats are added to a buff separately upon creation (defined in the invoking code).
*Stacks - Having multiple buffs of the same type increases the level of their ability (no limit, even if the ability has a limit in object editor)
*Stacks2 - Multiples of the same buff can be on a unit, if you allow them to be.
*Duration defined by triggers only.
*Some extra stats: lifesteal rating(like percentage, but diminishing), flat lifesteal, piercing(ignores armor), efficiency(reduces mana cost of custom spells).
*Most normal stats: armor, damage, attack rate, life, mana.

New stats can be added fairly easily by changing the code, but it is unlikely that I'll make it semi-automatic due to design constraints.

The example map has a custom version of inner fire that is cast on every unit when any spell is cast. This is more a proof of concept and thus, lacks some features that could easily be added, such as: buffs disappearing upon death, percentage damage bonus.

All attached .j files are required (Nestharus mode, anyone?) with the addition of ExtraConstants, that I couldn't upload here due to already having it uploaded in another thread.

What do you think?
 

Attachments

  • UnitBoost Test.w3x
    20.3 KB · Views: 118
  • TimedStats.j
    1.4 KB · Views: 123
  • StatManager.j
    2.3 KB · Views: 123
  • Bonus.j
    5.7 KB · Views: 99
  • BonusAbils.j
    6.8 KB · Views: 103
  • ExtraFunctions.j
    4.2 KB · Views: 143
Level 21
Joined
Mar 27, 2012
Messages
3,232
Back when I hadn't started making this I actually looked for similar systems. I only found this, but it's simply unacceptable to me due to using structs, which I can't read. Looking at it now I also see other issues, such as using AutoIndex.
I can guarantee that my buff system is bound to be faster than anything with struct methods.

Also, the one on hive is GUI, while mine is not.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Many of those didn't exist when I first started toying with the idea. Bonusmod was sort of an inspiration to me though and I quickly coded my own (why do most public libraries have to have horrible API?).
In the end what I saw was that:
1. Blizzard's own buff system is simply bad if you want to do anything different.
2. All known systems used unreadable code(structs) or were otherwise incomprehensible/unmaintainable to me.
3. I like to code my resources myself, because that way I understand exactly how they work and fit them to my needs without unneeded features or other complications.
 
Level 22
Joined
Sep 24, 2005
Messages
4,821
Are you trying to build a buff system framework with plugins or something? Something like that would be good... I kinda remember something like that... I think it was Nes' spell framework...

Also, #2 seems highly subjective, nothing to consider about though, since this is for personal use.
 
Last edited:
Level 21
Joined
Mar 27, 2012
Messages
3,232
The way I do this doesn't assume plugins. If there is a way to make a dynamically extendable textmacro, then I would gladly make plugins, but as of yet I know of no such way.

As of the system, stat management is intended to be semi-automatic. A spell requires the library to have access to its functions and then simply uses them like normal. Any stat addition/subtraction is done automatically, as they are standardized (No one needs more than 1 good bonusmod per map).
There will be a separate function for each modifiable stat and thus, the system doesn't support plugins, but will likely be easy enough to expand once you realize how it works.

Creating new stat types is actually handled in StatManager, not this system. Each stat is handled completely separate at the moment, although I do intend to start evaluating the values of various stats later on.

What I mean by dynamic macros is this.
 
Level 15
Joined
Aug 7, 2013
Messages
1,337
A bit confused on the dynamic macros--since your "extendor" calls need to be run before the actual //! runtextmacro or whatever, it would just require making a script to parse the files, apply the extendor changes, and then the output would be the script that jass helper actually handles.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
A bit confused on the dynamic macros--since your "extendor" calls need to be run before the actual //! runtextmacro or whatever, it would just require making a script to parse the files, apply the extendor changes, and then the output would be the script that jass helper actually handles.

Technically yes. I imagine it as another preprocessor step for JASShelper.
However, this is just a dream feature.
 
Level 15
Joined
Aug 7, 2013
Messages
1,337
Technically yes. I imagine it as another preprocessor step for JASShelper.
However, this is just a dream feature.

Well if you keep all your scripts in separate .j files or whatever, then it'd be quite easy to do.

In my own project I run several Python scripts over my .j files before they get passed to jass helper, though I call these additional syntactic words things like "//insert" or "//merge." (these are for both objects and vJASS).

So you should go ahead and do that, unless your project(s) are stuck in TESH.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Well if you keep all your scripts in separate .j files or whatever, then it'd be quite easy to do.

In my own project I run several Python scripts over my .j files before they get passed to jass helper, though I call these additional syntactic words things like "//insert" or "//merge." (these are for both objects and vJASS).

So you should go ahead and do that, unless your project(s) are stuck in TESH.

I know some python, but I'm not really good enough at it to make such a program myself.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Update:
Rewrote the whole thing, because the previous implementation only allowed stat bonuses. This one allows user-defined buffs through the use of custom create/destroy functions.
This version(albeit with some modifications) will probably be the one I'll start using.

This time I also provided a custom dispel functionality(as shown by the orc)

Right-click someone with cleric to place buff. Right-click with orc to remove. The stack limit is 10.

Example map attached.
 

Attachments

  • UnitPerk.w3x
    22.3 KB · Views: 93
Level 21
Joined
Mar 27, 2012
Messages
3,232
All the required resources are in previous posts (StatManager,Bonus,BonusAbils(for implementation only), ExtraConstants,ExtraFunctions). All of the requirements are actually for the implementation, not the system itself.

I have not started coding the plugins yet.

Specs(What the system is supposed to be like)
JASS:
/*
The goal: To write a system that can handle perks in a fashion similar to buffs
in vanilla warcraft, but with additional capabilities.

Requirements:
. Each perk must be uniquely identifiable. 
. Perk effects must be possible to define in application code, not the system. 
. Each perk must be possible to track back to its original source(unit) and identified by type. 
. Each unit can be queried to know the full list of its perks, with all associated data, 
including stacks of each perk type and the total amount of perks on unit. 
. Bare bones - The system itself doesn't provide anything more than necessary. 

Plugins(optional):
. Support for timeout duration and prompt removal. 
. A system akin to Spell Steal in vanilla warcraft, transferring the effects of a buff to a different unit. 

Structure:
. Each perk has a destroy and create method. Optionally it may have a decay method, 
if the perk can be removed in several ways. 
. The create method calls InitPerk(type,owner,target) once, in order to attach data to the perk and acquire a list index. 
. Perks are organized in a doubly linked list, with perks of the same unit next to eachother. 
. Each unit has a variable in an array for perk count and first perk index. 
*/

Core of the system
JASS:
//This is the core of the system. It handles perk indexing in order to allow easy use of arrays elsewhere. 
library UnitPerk initializer in
    globals
        hashtable PerkTable = InitHashtable()
        
        integer array UnitPerkType
        unit array UnitPerkOwner
        unit array UnitPerkTarget
        integer array UnitPerkCount
        integer array UnitPerkPointer
        integer array UnitPerkNext
        integer array UnitPerkPrev
    
        integer array UnDex
        integer UnDexes
        
        integer ToDestroy
        
        constant integer PERK_DESTROY = 1
    endglobals
    
    function DestroyPerk takes integer index returns nothing
        set ToDestroy = index
        call TriggerEvaluate(LoadTriggerHandle(PerkTable,UnitPerkType[index],PERK_DESTROY))
    endfunction
    
    function InitPerk takes integer perktype,unit target,unit owner returns integer
        //Necessary data
        local integer ID = GetUnitUserData(target)
        //Taking a perk ID from the stack and unallocating the taken ID to prevent overlap
        local integer index = UnDex[UnDexes]
        set UnDexes = UnDexes - 1
        
        //Initializing the perk
        set UnitPerkType[index] = perktype
        set UnitPerkOwner[index] = owner
        set UnitPerkTarget[index] = target
        
        //Incrementing the buff counter(ability level)
        if GetUnitAbilityLevel(target,perktype) > 0 then
            call Debug(I2S(GetUnitAbilityLevel(target,perktype)))
            call IncUnitAbilityLevel(target,perktype)
            //call SetUnitAbilityLevel(target,perktype,GetUnitAbilityLevel(target,perktype)+1)
            call Debug(I2S(GetUnitAbilityLevel(target,perktype)))
            //call IncUnitAbilityLevel(target,perktype)
        else
            call UnitAddAbility(target,perktype)
        endif
        
        if UnitPerkCount[ID] == 0 then
            //Starting a new doubly linked list
            set UnitPerkCount[ID] = 1
            
            set UnitPerkPointer[ID] = index
            set UnitPerkNext[index] = index
            set UnitPerkPrev[index] = index
        else
            set UnitPerkCount[ID] = UnitPerkCount[ID] + 1
            
            //Swapping the new perk with the one at the pointer. 
            set UnitPerkPrev[UnitPerkPointer[ID]] = index
            set UnitPerkNext[UnitPerkPrev[UnitPerkPointer[ID]]] = index
            //
            set UnitPerkPrev[index] = UnitPerkPrev[UnitPerkPointer[ID]]
            set UnitPerkNext[index] = UnitPerkPointer[ID]
            
            //Setting the pointer to the new perk.
            set UnitPerkPointer[ID] = index
            
        endif
        
        //Return the new perk in order to allow attaching data to it. 
        debug call Debug("Array index of new perk is: "+I2S(index))
        return index
    endfunction
    
    function ClearPerk takes integer index returns nothing
        //Necessary for cleaning up
        local unit target = UnitPerkTarget[index]
        local integer perktype = UnitPerkType[index]
        local integer ID = GetUnitUserData(target)
        
        //Decrementing the buff counter(ability level)
        if GetUnitAbilityLevel(target,perktype) > 1 then
            call Debug(I2S(GetUnitAbilityLevel(target,perktype)))
            call SetUnitAbilityLevel(target,perktype,GetUnitAbilityLevel(target,perktype)-1)
            call Debug(I2S(GetUnitAbilityLevel(target,perktype)))
        else
            call UnitRemoveAbility(target,perktype)
        endif
        
        if UnitPerkCount[ID] == 1 then
            set UnitPerkCount[ID] = 0
            set UnitPerkPointer[ID] = -1
        else
            //Pointing next and previous perk to eachother
            set UnitPerkPrev[UnitPerkNext[index]] = UnitPerkPrev[index]
            set UnitPerkNext[UnitPerkPrev[index]] = UnitPerkNext[index]
            
            set UnitPerkCount[ID] = UnitPerkCount[ID] - 1
            set UnitPerkPointer[ID] = UnitPerkNext[index]
        endif
        
        set UnDexes = UnDexes + 1
        set UnDex[UnDexes] = index
        
        set UnitPerkType[index] = -1
    endfunction
    
    private function in takes nothing returns nothing
        set UnDexes = 1
        loop            
            set UnDex[UnDexes] = 8191-UnDexes
            
            exitwhen UnDexes == 8190
            set UnDexes = UnDexes + 1
        endloop
    endfunction
endlibrary

Example spell using it(inner fire)
JASS:
//! import "StatManager.j"
// import "BonusAbils.j"
library InnerFire initializer in requires UnitPerk,StatManager
    globals
        private integer array Arm
        private integer array UnitID
        private unit array Unit
        
        private constant integer PerkID = 'AInF'
    endglobals
    
    function InnerFireCreate takes unit target,unit owner,integer armor returns integer
        local integer index = InitPerk(PerkID,target,owner)
        set Unit[index] = target
        set UnitID[index] = GetUnitUserData(target)
        call AddUnitArm(target,UnitID[index],armor)
        set Arm[index] = armor
        return index
    endfunction
    
    private function Destroy takes nothing returns boolean
        call AddUnitArm(Unit[ToDestroy],UnitID[ToDestroy],-Arm[ToDestroy])
        call ClearPerk(ToDestroy)
        return false
    endfunction
    
    private function in takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerAddCondition(t,Condition(function Destroy))
        call SaveTriggerHandle(PerkTable,PerkID,PERK_DESTROY,t)
    endfunction
endlibrary

A simple trigger for casting the inner fires. The inner fire "buff" is based on item armor bonus (+1)
  • Inner Fire Gesture
    • Events
      • Unit - A unit Is issued an order targeting an object
    • Conditions
      • (Triggering unit) Equal to (==) Cleric 0001 <gen>
    • Actions
      • Multiple FunctionsIf (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (Level of Inner Fire (+1) for (Target unit of issued order)) Less than (<) 10
        • Then - Actions
          • Custom script: call InnerFireCreate(GetOrderTargetUnit(),GetTriggerUnit(),3)
        • Else - Actions
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
I didnt read the whole code, but:
- why dont you use bonusmod? It seems like you implemented your own copy of it.. why?
- why dont you use structs? I heard you dont like them, but they would make your code look nicer (since most of your code is array spamming)
- Was the design your idea? J4Ls BuffStruct has a somewhat similar approach, but (apart from some bugs) it seems a little more defined and powerful..
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
I didnt read the whole code, but:
- why dont you use bonusmod? It seems like you implemented your own copy of it.. why?
- why dont you use structs? I heard you dont like them, but they would make your code look nicer (since most of your code is array spamming)
- Was the design your idea? J4Ls BuffStruct has a somewhat similar approach, but (apart from some bugs) it seems a little more defined and powerful..

1. Back when I first started using bonusmods I didn't really understand how they work and thus, I was unable to gauge whether the public systems are well written or not. Then when I had already written my own I grew accustomed to it, because it can not have any bugs that I don't know of. My bonusmod is not perfect(no preloading yet. Morphs will break bonuses), but I'm adding features as needed, instead of having to trim down a public system to not do unnecessary stuff.
2. Structs make the code incomprehensible to me by hiding away how the system works. Also they are not needed at all, because their only purpose appears to be indexing. Structs to me are simply unreadable.
Furthermore, since libraries can't require structs, then it would make the system less extendable.
3. At first I just had stat bonuses and they were handled by the system. It's unnecessary though. Now it's possible to do everything that the usual buff system in warcraft does, but with more control.
The system should not know what a buff does, because that's defined by whoever uses it. Thus, it allows for way more.
Stacking is supported by default. Buff icons are easy to do considering how this works.
Unique buffs(1 per target) are easy to do, because it's not even necessary to check the list of buffs, you can check for a specific one directly.
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
Ok no offense, but your reply tells me that you dont understand structs at all.

Furthermore, since libraries can't require structs, then it would make the system less extendable.
Structs and libraries are 2 completely different things, so a library requireing a struct doesnt make sense at all, that would be a like a library that requires a function o_O

Structs make the code incomprehensible to me by hiding away how the system works.
Structs dont hide anything. Structs do exactly these 3 things:
1. Structs members are used when you normally would use parallel arrays. So instead of:
JASS:
set bla[i] = ...
set blub[i] = ...
set foo[i] = ...
you can just use
JASS:
set i.bla = ...
set i.blub = ...
set i.foo = ...
just with the additional comfort that you can drop the "i." when used inside the struct.
2. Struct methods are simply normal functions (which operate on your parallel arrays) but the argument which refers to the instance (the index of your parallel arrays) is passed differently:
Instead of
JASS:
call myFun(i, penguin, 42)
you do:
JASS:
call i.myFun(penguin, 42)
with the additional comfort that you can drop the "i." when used inside the struct.
3. They can create new instances (indexes for your parallel arrays) automatically, its the exact same thing you would manually do.

You should probably try to learn how structs work and give them a chance, it seems like you are scared of them because you dont understand them. Sure, there are many ways to abuse struct syntax, which is a horrible thing to do. But structs being used how they are intended to are great. Structs arent compicated (the way they are compiled into normal jass), and when you understand them there is absolutely nothing magical about them and they dont hide anything. (And im not even a vJass fan, Wurst FTW!!)
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Writing things differently isn't really a gain for me. It's also not painful for me to specify the exact variable name when changing it.

Structs create extra methods unless they are overridden, in which case I'll have to define everything myself like I have anyway.
The difference between my code and a struct-laden one is that I don't use a completely different syntax for no apparent gain.

Notice how I have a doubly linked list? This is not the default behaviour of structs as far as I am aware. Thus, I would again have to define it myself anyway.
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
Structs create extra methods unless they are overridden, in which case I'll have to define everything myself like I have anyway.
They dont. They create an allocator and an deallocator, the same thing you wrote yourself (your code which fetches a new index for your arrays). Its a myth that structs generate useless code, sometimes the allocator does a little bit more than it would have to, but its a difference you cant possible notice in performance. However it saves you tons of code.

Everyone hates structs, everyone loves the "extends array" shit, but i doubt that most of those ppl can actually explain why. Structs are great, use them.

Notice how I have a doubly linked list? This is not the default behaviour of structs as far as I am aware. Thus, I would again have to define it myself anyway.
JASS:
struct Buff
    Buff prev
    Buff next

    method create takes Buff prev, Buff next returns Buff
        local Buff b = Buff.allocate() // Fetches a free index
        set prev.next = b
        set next.prev = b
        set b.next = next
        set b.prev = prev
        return b
    endmethod
endstruct
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
I get your point. Structs are easier.
I still have a disliking for them though, which I will have to sort out myself.

The reason why buffs are added at the head of the list is that this makes buffs organized by when they were placed. It's a minor gain, but it can be used for some stuff, like "Remove last placed buff".
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
Oh, then i got this wrong. I thought you sort the buffs by type (like grouping buffs of same type). If you just insert then on the head then passing prev/next obv. doesnt make sense.

€:
. Each unit can be queried to know the full list of its perks, with all associated data,
including stacks of each perk type and the total amount of perks on unit.
Oh i got this wrong too :D
If you apply "inner fire" to the same unit 3 times you dont store 3 independant instances of the buff, but just add it once and set "stacks" to 3?
But that means that the 3 instances cant have different values... right?
Like.. lets say you have an "inner fire" which adds 2 times the casters intelligence as armor and 4 times the casters intelligence as damage. If you have 2 different casters casting inner fire on the same target unit the bonuses of those two inner fire instances should be different... thats not possible with ur system, right?
 
Status
Not open for further replies.
Top