• 🏆 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] Buff System - Turn Based Game

Status
Not open for further replies.
Level 19
Joined
Oct 12, 2007
Messages
1,821
Hey there,

I'm currently a bit stuck on a matter for my map.
I'm making a turn based game, so durations of buffs and events don't pend on time, but on the amount of turns that pass by.

Whenever I use the 'end turn' ability, an integer called 'TURN' will be set to 'TURN + 1'. Simple as that.

Now the problem is that I want to have a good buff system in the map where dispelling is possible. I looked around and found a great system made by Rising_Dusk (on wc3c), but this system is (obviously) using 'time' to check when a buff expires.

I'm currently thinking of editing this system, or remaking a new one, so that I can set the 'time to expire' as an integer, which means if that value is 3, it will take 3 TURNS to go by until the buff expires.

Now I just don't know what to do here, since I'm not a very great system coder. A thing that is on my mind is 'how can I easily add -1 to every buff's turns left and see if the buff should be expiring or not'.

The current buff system, obviously, uses timers to track the elapsed time of a buff. But I need some kind of tracking to whenever a player uses the 'end turn' ability, and whenever that happens it has to modify the buff durations by reducing it by 1. If then its at 0 it should run the specific buff removal function, which means that sometimes it also has to remove more than just the buff ID, since some buffs add +2 agility (per example) so removing extra abilities with BonusMods is also required.

Is there anyone here that has an idea on how to make this doable? Or even better, would someone like to give it a go for me? :D


JASS:
library IntuitiveBuffSystem initializer Init requires Stack, TimerUtils, AutoIndex, AbilityPreload
//******************************************************************************
//* BY: Rising_Dusk
//*     (Intuitive) Buff System 1.05
//* 
//* This library is a simple and easy to understand implementation of custom
//* buff handling in JASS. I call it the Intuitive Buff System because anyone
//* should be able to pick it up and use it right away. It uses a callback
//* driven style where the user specifies which callbacks run either
//* periodically over a buff's duration or when the buff is removed. This covers
//* all possible types of buffs that one might want to create.
//* 
//******************************************************************************
//* 
//* In order to make a buff work, one must first create a custom aura ability in
//* the Object Editor and a custom buff for that aura. The raw ids for these
//* objects need to be supplied to the IBS via the DefineBuffType function.
//* 
//*     function DefineBuffType takes integer AbilId, integer BuffId, ...
//*     ... real period, boolean isPeriodic, boolean isPositive, callback onAdd, ...
//*     ... callback onPeriodic, callback onEnd returns integer
//* 
//* The arguments for this function are as follows:
//*     [integer]  AbilId               Owner of the buff being applied
//*     [integer]  BuffId               Target for the buff to be applied to
//*     [real]     period               Type of the buff from DefineBuffType
//*     [boolean]  isPeriodic           Whether the buff is periodic or not
//*     [boolean]  isPositive           Whether the buff is positive or not
//*     [callback] onAdd                Callback to run when a buff is added
//*     [callback] onPeriodic           Callback to run every buff iteration
//*     [callback] onEnd                Callback to run when a buff ends
//* 
//* NOTE: All callbacks must follow the below structural convention
//*     function MyCallback takes nothing returns nothing
//* 
//* The callback functions may be private, public, or whatever. When using them
//* in the DefineBuffType function call, simply put the function's name for the
//* respective argument (no function keyword). If you do not want any function
//* to be called for one of the callbacks, simply use 0 for that argument.
//* 
//* Once the bufftype is registered with the system, you are free to use it
//* whenever you want to. It returns an integer, which you should set to a
//* global integer in a scope initializer or some initialization function. To
//* apply a buff to a unit, simple call the UnitAddBuff function. All of these
//* values are effectively constant for any buff created of the given bufftype.
//* 
//* The period argument tells the system the interval for running the
//* onPeriodic callback function. The period and onPeriodic arguments only
//* matter if the isPeriodic argument is true. The onEnd argument runs when a
//* buff is removed either manually, by it expiring, or by the unit it is on
//* dying. The onAdd argument runs when a buff is either applied or refreshed.
//* Please do not destroy the dbuff struct inside your callback functions; that
//* will cause doublefrees. The system automatically cleans them up.
//* 
//* Inside the callbacks, you get one event response:
//*     function GetEventBuff takes nothing returns dbuff
//* 
//* Usage:
//*     local dbuff db = GetEventBuff()
//* 
//* Within the above struct, the following information can be retrieved (or
//* modified, but I recommend not changing most of the parameters). At any given
//* time during the buff's existence, these values can also be retrieved with a
//* call to GetUnitBuff, GetRandomBuff, or GetRandomBuffSigned on the unit.
//* 
//*     [unit]    .source               Owning unit of the buff
//*     [unit]    .target               Unit the buff is applied to
//*     [real]    .duration             Total duration for buff expiration
//*     [real]    .timeElapsed()        Total elapsed time of the buff
//*     [real]    .timeRemaining()      Remaining time before buff expiration
//*               .setDuration(NewDur)  Sets the current buff duration to NewDur
//*               .addDuration(ModDur)  Adds ModDur to the current buff duration
//*     [integer] .btype.period         Periodicity for the buff
//*     [integer] .level                Level of the buff used on the unit
//*     [integer] .data                 An integer the user can store data to
//*     [boolean] .isPeriodic           Whether the buff is periodic or not
//*     [boolean] .isExpired            Whether the buff expired naturally or not
//*                                     Can only return true in the 'OnEnd' callback
//*     [boolean] .isRemoved            Whether the buff was purged or removed by death or not
//*                                     Can only return true in the 'OnEnd' callback
//*     [boolean] .isRefreshed          Whether the buff was refreshed or not
//*                                     Can only return true in the 'OnAdd' callback
//* 
//******************************************************************************
//* 
//*     function UnitAddBuff takes unit source, unit target, integer btype, ...
//*     ... real dur, integer lvl returns dbuff
//* 
//* The arguments for this function are as follows:
//*     [unit]    source                The 'owner' of the buff being applied
//*     [unit]    target                The target for the buff to be applied to
//*     [integer] btype                 The type of the buff from DefineBuffType
//*     [real]    dur                   The total duration of the buff
//*     [integer] lvl                   The 'level' of the buff
//* 
//* Constants usable in this function call:
//*     [real]    BUFF_PERMANENT        This is the value you should use for the
//*                                     duration argument if you want a buff
//*                                     that never ends.
//* 
//* Additional options of note:
//*     set mybuff.data = SomeInteger   This allows you to attach an integer to
//*                                     a buff that you can get in the event
//*                                     callbacks.
//* 
//* The lvl argument is there for reapplication of buff purposes. A buff being
//* applied of a higher level than the old buff will always overwrite the old
//* one, it doesn't matter if duration is higher or not. This way, if you want a
//* buff to get shorter per level, the system will support that.
//* 
//******************************************************************************
//* 
//* Available system functions:
//* 
//*    > function GetUnitBuff takes unit target, integer whichType returns dbuff
//* 
//* This function allows the user to get a specific buff on a unit at any point
//* if the user knows the bufftype they want. Will return 0 if the buff does not
//* exist.
//* 
//*    > function GetRandomBuff takes unit target returns dbuff
//* 
//* This function allows the user to get a random buff on a unit at any point.
//* Getting a random buff is most useful for stealing or moving of arbitrary
//* buffs on a unit. Will return 0 if the unit has no buffs.
//* 
//*    > function GetRandomBuffSigned takes unit target, boolean isPos returns dbuff
//* 
//* This function, like GetRandomBuff, returns a random buff on a unit given a
//* 'sign'. The isPos argument lets you specify to either get a random positive
//* buff if true or a random negative buff if false. Will return 0 if the unit
//* has no buffs of the desired alignment.
//* 
//*    > function GetLastAppliedBuff takes unit target returns dbuff
//* 
//* This function returns the most recently applied buff on a unit. Will return
//* 0 if the unit has no buffs.
//* 
//*    > function GetFirstAppliedBuff takes unit target returns dbuff
//* 
//* This function returns the first buff in a user's bufflist that is still
//* active. Will return 0 if the unit has no buffs.
//* 
//*    > function UnitHasBuff takes unit target, integer whichType returns boolean
//* 
//* This function will return true if the given unit has the specified buff.
//* 
//*    > function UnitHasAnyBuff takes unit target returns boolean
//* 
//* This function will return true if the given unit has any buff managed by
//* the system.
//* 
//*    > function UnitRemoveBuff takes unit target, integer whichType returns boolean
//* 
//* This function removes a buff of a specified type from a given unit. Will
//* return false if the buff did not exist to be removed.
//* 
//*    > function UnitRemoveAllBuffs takes unit target returns nothing
//* 
//* This function removes all buffs of any type from a given unit.
//* 
//******************************************************************************
//* 
//* Finally, there is the inherent limit that you can only have 8192 buffs and
//* 8192 bufftypes at once. If you ever get this high, your map will have worse
//* problems than this system working improperly.
//* 
//* I hope you find this library as useful as I do. Thanks for reading the
//* documentation. If you have any questions for me about the IBS, how it works,
//* or if you have suggestions for it, please contact me at WC3C by private
//* message. This system is only authorized for release at Warcraft 3 Campaigns.
//* 
//* Enjoy!
//* 

globals
    //Constant for use with infinite duration buffs
    constant real    BUFF_PERMANENT     = 0x800000
endglobals

private keyword TimeOut
private keyword TempBuff
private function interface callback takes nothing returns nothing

globals
    private hashtable     ht    = InitHashtable() //Hashtable for all data attachment
    private integer array lists
endglobals

private struct bufftype
    //All readonly so the user doesn't mess with them
    integer  abilid
    integer  buffid
    real     period
    boolean  isPeriodic
    boolean  isPositive
    
    //All of the callback behavior for the bufftype
    callback onAdd
    callback onPeriodic
    callback onEnd
    static method create takes integer abilid, integer buffid, real period, boolean isPeriodic, boolean isPositive, callback onAdd, callback onPeriodic, callback onEnd returns bufftype
        local bufftype bt = bufftype.allocate()
        set bt.abilid     = abilid
        set bt.buffid     = buffid
        set bt.period     = period
        set bt.isPeriodic = isPeriodic
        set bt.onAdd      = onAdd
        set bt.onPeriodic = onPeriodic
        set bt.onEnd      = onEnd
        return bt
    endmethod
endstruct

private struct bufflist
    Stack sta
    unit  target
    
    static method create takes unit target returns bufflist
        local bufflist bl = bufflist.allocate()
        set bl.target     = target
        set bl.sta        = Stack.create()
        //Attach it to the unit
        set lists[GetUnitId(target)] = integer(bl)
        return bl
    endmethod
    private method onDestroy takes nothing returns nothing
        call .sta.destroy()               //Destroy the stack
        set lists[GetUnitId(.target)] = 0 //Remove array value
    endmethod
endstruct

struct dbuff
    unit     source
    unit     target
    real     elapsed     = 0.
    real     duration
    integer  level
    integer  data        = 0
    bufftype btype
    timer    tmr
    
    //Behavior variables inside the struct
    boolean  isPeriodic
    boolean  isExpired   = false
    boolean  isRemoved   = false
    boolean  isRefreshed = false
    
    //Handy methods that save the user math they don't want to do
    method timeElapsed takes nothing returns real
        if .btype.isPeriodic then
            return .elapsed+TimerGetElapsed(.tmr)
        else
            return TimerGetElapsed(.tmr)
        endif
    endmethod
    method timeRemaining takes nothing returns real
        if .btype.isPeriodic then
            return .duration-.elapsed-TimerGetElapsed(.tmr) 
        else
            return TimerGetRemaining(.tmr)
        endif
    endmethod
    method setDuration takes real newDur returns nothing
        local real oldDur = .duration
        if .timeElapsed() >= newDur then
            //This is equivalent to force removing the buff, so kill it
            set .isExpired   = false
            set .isRemoved   = true
            set .isRefreshed = false
            call .destroy() //Timer gets recycled in here!
        else
            if .isPeriodic then
                if .timeElapsed() + .btype.period > newDur then
                    //Update timeout to cover last segment of buff duration
                    call TimerStart(.tmr, newDur-.timeElapsed(), false, TimeOut)
                    //Since it won't clear a full period, we don't let it run onPeriodic
                    set .isPeriodic = false
                endif
            else
                //Run for however long you have to so total duration is the new duration
                call TimerStart(.tmr, newDur-.timeElapsed()  , false, TimeOut)
            endif
            set .duration = newDur
        endif
    endmethod
    method addDuration takes real modDur returns nothing
        //Just calls the setDuration method
        call .setDuration(.duration+modDur)
    endmethod
    //Some callback running methods
    method runAdd takes nothing returns nothing
        set .isExpired   = false
        set .isRemoved   = false
        set .isRefreshed = false
        call .btype.onAdd.execute()
    endmethod
    method runRefreshed takes nothing returns nothing
        set .isRefreshed = true
        set .isExpired   = false
        set .isRemoved   = false
        call .btype.onAdd.execute()
    endmethod
    method runPeriodic takes nothing returns nothing
        set .isExpired   = false
        set .isRemoved   = false
        set .isRefreshed = false
        call .btype.onPeriodic.execute()
    endmethod
    method runExpired takes nothing returns nothing
        //This destroys the dbuff
        set .isExpired   = true
        set .isRemoved   = false
        set .isRefreshed = false
        call .destroy() //Timer gets recycled in here!
    endmethod
    method runRemoved takes nothing returns nothing
        //This destroys the dbuff
        set .isExpired   = false
        set .isRemoved   = true
        set .isRefreshed = false
        call .destroy() //Timer gets recycled in here!
    endmethod
    static method create takes unit source, unit target, bufftype btype, real dur, integer lvl returns dbuff
        local dbuff   db  = dbuff.allocate()
        local integer id  = GetUnitId(target)
        set db.source     = source
        set db.target     = target
        set db.duration   = dur
        set db.btype      = btype
        set db.isPeriodic = btype.isPeriodic
        set db.level      = lvl
        set db.tmr        = NewTimer()
        //Hook the dbuff to the timer
        call SetTimerData(db.tmr, integer(db))
        //Add ability to the target of the buff
        call UnitAddAbility(target, db.btype.abilid)
        call UnitMakeAbilityPermanent(target, true, db.btype.abilid)
        //Load to table for future referencing
        call SaveInteger(ht, btype, id, integer(db))
        //Push the buff into the bufflist stack on the target
        call bufflist(lists[id]).sta.add(integer(db))
        return db
    endmethod
    private method onDestroy takes nothing returns nothing
        local integer id = GetUnitId(.target)
        //Clear the table value so the system knows it's done
        //Done before the onRemove call to prevent potential double frees with death trigger
        call RemoveSavedInteger(ht, .btype, id)
        
        //Remove the buff from the bufflist stack on the target
        call bufflist(lists[id]).sta.remove(integer(this))
        
        //Set up and run the onRemove callback
        set TempBuff = this
        call .btype.onEnd.execute()
        
        //Clear stuff inside the struct and on the unit
        call UnitRemoveAbility(.target, .btype.abilid)
        call UnitRemoveAbility(.target, .btype.buffid)
        call ReleaseTimer(.tmr)
    endmethod
endstruct

globals
    private code           TimeOut       = null //Code callback for buff timeouts
    private integer        BuffTypeCount = 0    //Counter for how many buff types exist
    private bufftype array BuffTypes            //Array for all defined buff types
    private dbuff          TempBuff      = 0    //Temp buff for callback data
endglobals

private function BuffTimeout takes nothing returns nothing
    local timer t  = GetExpiredTimer()
    local dbuff db = dbuff(GetTimerData(t))
    
    //Different behavior for periodic buffs than one-timers
    if db.isPeriodic then
        //Run the onPeriod callback no matter what
        set TempBuff = db
        call db.runPeriodic()
        //Check if this iteration kills the buff
        set db.elapsed = db.elapsed + db.btype.period
        if db.elapsed >= db.duration and not db.isRemoved then
            //Kill the buff completely if it hasn't been cleared elsewhere
            call db.runExpired()
        elseif db.elapsed + db.btype.period > db.duration and not db.isRemoved then
            //Update timeout to cover last segment of buff duration
            call TimerStart(db.tmr, db.duration-db.elapsed, false, function BuffTimeout)
            //Since it won't clear a full period, we don't let it run onPeriodic
            set db.isPeriodic = false
        elseif TimerGetElapsed(db.tmr) < db.btype.period then
            //Update period of timer to normal value
            call TimerStart(db.tmr, db.btype.period, true, function BuffTimeout)
        endif
    elseif not db.isRemoved then
        //Kill the buff completely, set it to expired naturally
        //Shouldn't run if it was force removed by the on-death trigger
        call db.runExpired()
    endif
    
    set t = null
endfunction

//******************************************************************************

function GetEventBuff takes nothing returns dbuff
    return TempBuff
endfunction

//******************************************************************************

function UnitHasBuff takes unit target, integer whichType returns boolean
    return HaveSavedInteger(ht, whichType, GetUnitId(target))
endfunction

function UnitHasAnyBuff takes unit target returns boolean
    return bufflist(lists[GetUnitId(target)]).sta.size > 0
endfunction

function UnitRemoveBuff takes unit target, integer whichType returns boolean
    local dbuff db = 0
    if HaveSavedInteger(ht, whichType, GetUnitId(target)) then
        set db = dbuff(LoadInteger(ht, whichType, GetUnitId(target)))
        //Buff exists, clear it
        call db.runRemoved()
        return true
    endif
    return false
endfunction

private function RemoveAllBuffsEnum takes integer value returns nothing
    call dbuff(value).runRemoved()
endfunction
function UnitRemoveAllBuffs takes unit target returns nothing
    call bufflist(lists[GetUnitId(target)]).sta.enum(RemoveAllBuffsEnum, true)
endfunction

function GetUnitBuff takes unit target, integer whichType returns dbuff
    return dbuff(LoadInteger(ht, whichType, GetUnitId(target)))
endfunction

function GetRandomBuff takes unit target returns dbuff
    local bufflist bl = bufflist(lists[GetUnitId(target)])
    if bl.sta.size > 0 then
        return dbuff(bl.sta.random)
    endif
    return 0 //No buff to return
endfunction

function GetRandomBuffSigned takes unit target, boolean isPos returns dbuff
    local bufflist bl = bufflist(lists[GetUnitId(target)])
    local Stack    s1 = 0
    local Stack    s2 = 0
    local integer  v  = 0
    
    //Only do this stuff if the unit has any buffs at all
    if bl.sta.size > 0 then
        //Build the needed stacks
        set s1 = bl.sta.copy()
        set s2 = Stack.create()
        //Loop through and build a new stack of buffs of desired type
        loop
            exitwhen s1.size == 0
            set v = s1.first
            if dbuff(v).btype.isPositive == isPos then
                //Found one, add to #2 stack
                call s2.add(v)
            endif
            call s1.remove(v)
        endloop
        if s2.size > 0 then
            //Get random member of generated stack
            set v = s2.random
        else
            set v = 0
        endif
        //Destroy the generated stacks
        call s1.destroy()
        call s2.destroy()
        //Return our buff or 0
        if v > 0 then
            return dbuff(v) //Return our buff
        endif
    endif
    return 0 //No buff to return
endfunction

function GetLastAppliedBuff takes unit target returns dbuff
    local bufflist bl = bufflist(lists[GetUnitId(target)])
    if bl.sta.size > 0 then
        return dbuff(bl.sta.first)
    endif
    return 0 //No buff to return
endfunction

function GetFirstAppliedBuff takes unit target returns dbuff
    local bufflist bl = bufflist(lists[GetUnitId(target)])
    local Stack    s  = 0
    local integer  v  = 0
    
    //Only do this stuff if the unit has any buffs at all
    if bl.sta.size > 0 then
        //Build the stack we need
        set s = bl.sta.copy()
        //Loop through until you find the bottom of the stack
        loop
            exitwhen s.size == 0
            set v = s.first
            call s.remove(v)
        endloop
        return dbuff(v)
    endif
    return 0 //No buff to return
endfunction

function UnitAddBuff takes unit source, unit target, integer btype, real dur, integer lvl returns dbuff
    local timer    t  = null
    local integer  id = GetUnitId(target)
    local bufftype bt = bufftype(btype)
    local integer  d  = 0
    local dbuff    db = 0
    
    //Standard debugging procedure
    if source == null or target == null or btype < 0 or lvl < 0 or dur <= 0. then
        debug call BJDebugMsg(SCOPE_PREFIX+"Error: Invalid buff creation parameters")
        return 0 
    endif
    
    //Find if this previous instance exists in the Table
    if HaveSavedInteger(ht, btype, id) then
        //Exists, use its data
        set db = dbuff(LoadInteger(ht, btype, id))
        if (lvl == db.level and db.timeRemaining() < dur) or lvl > db.level then
            //Update all applicable values to the newly supplied ones if new instance overwrites
            //Elapsed goes to 0 because it's as if a new buff has been cast, run timer again
            set db.source   = source
            set db.target   = target
            set db.elapsed  = 0.
            set db.duration = dur
            set db.level    = lvl
            if not db.isPeriodic and db.btype.isPeriodic then
                //If it was on the last segment of a periodic timer, reset periodicity
                set db.isPeriodic = true
            endif
            //Run the onAdd callback
            set TempBuff = db
            //It becomes a refreshed buff now
            call db.runRefreshed()
            if db.isPeriodic then
                call TimerStart(db.tmr, db.btype.period-TimerGetElapsed(db.tmr), true , function BuffTimeout)
            else
                call TimerStart(db.tmr, dur                                    , false, function BuffTimeout)
            endif
        endif
    else
        //Doesn't exist, create it
        set db = dbuff.create(source, target, bt, dur, lvl)
        //Run the onAdd callback
        set TempBuff = db
        call db.runAdd()
        if db.isPeriodic then
            call TimerStart(db.tmr, db.btype.period, true , function BuffTimeout)
        else
            call TimerStart(db.tmr, dur            , false, function BuffTimeout)
        endif
    endif
    return db
endfunction

function DefineBuffType takes integer AbilId, integer BuffId, real period, boolean isPeriodic, boolean isPositive, callback onAdd, callback onPeriodic, callback onEnd returns integer
    local bufftype bt = bufftype.create(AbilId, BuffId, period, isPeriodic, isPositive, onAdd, onPeriodic, onEnd)
    //Preload abilities to prevent first-buff lag
    call AbilityPreload(AbilId)
    set BuffTypeCount = BuffTypeCount + 1
    set BuffTypes[BuffTypeCount] = bt
    return integer(bt)
endfunction

//******************************************************************************

private function DeathActions takes nothing returns nothing
    call UnitRemoveAllBuffs(GetDyingUnit())
endfunction

//******************************************************************************

private function UnitEntersMap takes unit u returns nothing
    call bufflist.create(u)
endfunction
private function UnitLeavesMap takes unit u returns nothing
    if lists[GetUnitId(u)] != 0 then
        call bufflist(lists[GetUnitId(u)]).destroy()
    endif
endfunction

private function Init takes nothing returns nothing
    local trigger trg = CreateTrigger()
    
    //Trigger for removing buffs on death
    call TriggerAddAction(trg, function DeathActions)
    call TriggerRegisterAnyUnitEventBJ(trg, EVENT_PLAYER_UNIT_DEATH)
    
    //Indexing callback to create bufflists for indexed units
    call OnUnitIndexed(UnitEntersMap)
    
    //Removal callback to remove bufflists for indexed units
    call OnUnitDeindexed(UnitLeavesMap)
    
    //Initialize the callback code var
    set TimeOut = function BuffTimeout
endfunction
endlibrary
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
Thanks guys.
@DEE-BOO: I think the system is not complex enough. There are a few things I need to have that are present in the current system I use.

@Wietlol: That would be great! What does your system support right now? I need to be able to customize buffs to classify them as positive/negative and dispellable/undispellable. I currently use an aura ability based on the tornado aura + a buff for each 'buff'. So adding the buffs is done by this aura ability.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Well... I actually use the exact same method as you do...
I dont know exactly how you do it but I do it like this:

JASS:
library Buff uses TimerUtils, LLBuff //, Unit Indexer
    
    //! textmacro CREATE_BUFF_ABILITY takes RAWCODE, NAME, ICON, DESCRIPTION
        //! external ObjectMerger w3a Aasl A$RAWCODE$ abuf 1 B$RAWCODE$ Slo1 1 0 aare 1 0 atar 1 "Self,Invulnerable,Vulnerable"
        //! external ObjectMerger w3h Basl B$RAWCODE$ ftip "$NAME$" fube "$DESCRIPTION$" fart "$ICON$"
    //! endtextmacro
    /*function GetTriggerBuff takes nothing returns integer
        return buff_CurrentIndex
    endfunction*/
    
    globals
        
        integer         STACKING_TYPE_PARALLEL                          = 0
        integer         STACKING_TYPE_CUSTOM                            = 1
        
        integer         buff_NewBuffType                                = 0
        integer         buff_NewIndex                                   = 0
        integer         buff_TriggeredBuff                              = 0
        
        real            buff_Event_Applied                              = 0
        unit            buff_EventResponse_Target                       = null
        unit            buff_EventResponse_Source                       = null
        integer         buff_EventResponse_Bufftype                     = 0
        real            buff_EventResponse_Duration                     = 0.
        
        boolean array   buff_IsOccupied
        unit array      buff_Target
        unit array      buff_Source
        integer array   buff_BuffType
        timer array     buff_Timer
        real array      buff_Duration
        
        real array      bufftype_Interval
        integer array   bufftype_Ability
        integer array   bufftype_Buff
        integer array   bufftype_StackingType
        trigger array   bufftype_TriggerApply
        trigger array   bufftype_TriggerStack
        trigger array   bufftype_TriggerInterval
        trigger array   bufftype_TriggerRemove
        trigger array   bufftype_TriggerExpire
        trigger array   bufftype_TriggerDispel
        //User defined:
        //
        //
        //
        
    endglobals
    
    function Buff_CreateBuffType takes nothing returns integer
        set buff_NewBuffType = buff_NewBuffType +1
        
        set bufftype_TriggerApply[buff_NewBuffType] = CreateTrigger()
        set bufftype_TriggerStack[buff_NewBuffType] = CreateTrigger()
        set bufftype_TriggerInterval[buff_NewBuffType] = CreateTrigger()
        set bufftype_TriggerRemove[buff_NewBuffType] = CreateTrigger()
        set bufftype_TriggerExpire[buff_NewBuffType] = CreateTrigger()
        set bufftype_TriggerDispel[buff_NewBuffType] = CreateTrigger()
        
        return buff_NewBuffType
    endfunction
    
    function BuffType_SetBuff takes integer bufftype, integer buffAbility, integer buffBuff returns nothing
        set bufftype_Ability[bufftype] = buffAbility
        set bufftype_Buff[bufftype] = buffBuff
    endfunction
    
    //Get the id of the first buff on the given unit of the given bufftype.
    function Buff_GetBuffIndex takes unit target, integer buffType returns integer
        local integer index = LL_Buff_FirstIndex[GetUnitUserData(target)]
        loop
            exitwhen index == 0
            if buff_BuffType[index] == buffType then
                return index
            endif
            set index = LL_Buff_NextIndex[index]
        endloop
        return 0
    endfunction

    //Count all buffs on the given unit of the specified bufftype (only returns >1 for Parralel buffs ofcourse).
    function Buff_CountBuffsOfType takes unit target, integer buffType returns integer
        local integer index = LL_Buff_FirstIndex[GetUnitUserData(target)]
        local integer count = 0
        loop
            exitwhen index == 0
            if buff_BuffType[index] == buffType then
                set count = count +1
            endif
            set index = LL_Buff_NextIndex[index]
        endloop
        return count
    endfunction
    
    //Remove the buff from the unit.
    function Buff_Destroy takes integer id returns nothing
        set buff_TriggeredBuff = id
        call TriggerEvaluate(bufftype_TriggerRemove[buff_BuffType[id]])
        
        if bufftype_Ability[buff_BuffType[id]] != 0 then
            call UnitRemoveAbility(buff_Target[id], bufftype_Ability[buff_BuffType[id]])
            call UnitRemoveAbility(buff_Target[id], bufftype_Buff[buff_BuffType[id]])
        endif
        
        call ReleaseTimer(buff_Timer[id])
        call Buff_RemoveIndex(id)
        
        set buff_IsOccupied[id] = false
        set buff_Target[id] = null
        set buff_Source[id] = null
        set buff_BuffType[id] = 0
        set buff_Timer[id] = null
        set buff_Duration[id] = 0.
    endfunction
    
    //Dispel the buff.
    function Buff_Dispell takes integer id returns nothing
        set buff_TriggeredBuff = id
        call TriggerEvaluate(bufftype_TriggerDispel[buff_BuffType[id]])
        
        call Buff_Destroy(id)
    endfunction
    
    //System functions.
    function Buff_Expire takes nothing returns nothing
        local integer id = GetTimerData(GetExpiredTimer())
        
        set buff_TriggeredBuff = id
        call TriggerEvaluate(bufftype_TriggerExpire[buff_BuffType[id]])
        call Buff_Destroy(id)
    endfunction
    function Buff_Interval takes nothing returns nothing
        local integer id = GetTimerData(GetExpiredTimer())
        
        set buff_TriggeredBuff = id
        call TriggerEvaluate(bufftype_TriggerInterval[buff_BuffType[id]])
        
        set buff_Duration[id] = buff_Duration[id] - bufftype_Interval[buff_BuffType[id]]
        if buff_Duration[id] > bufftype_Interval[buff_BuffType[id]] then
            call TimerStart(buff_Timer[id], buff_Duration[id], false, function Buff_Expire)
        endif
    endfunction
    
    //System function.
    function Buff_Create takes unit source, unit target, integer buffType, real duration returns integer
        local integer id = Buff_CreateIndex(GetUnitUserData(target))
        set buff_Source[id] = source
        set buff_Target[id] = target
        set buff_BuffType[id] = buffType
        set buff_Timer[id] = NewTimerEx(id)
        set buff_Duration[id] = duration
        
        if bufftype_Interval[buffType] > 0. then
            call TimerStart(buff_Timer[id], bufftype_Interval[buffType], true, function Buff_Interval)
        else
            call TimerStart(buff_Timer[id], duration, false, function Buff_Expire)
        endif
        
        if bufftype_Ability[buffType] != 0 then
            call UnitAddAbility(buff_Target[id], bufftype_Ability[buffType])
            call UnitMakeAbilityPermanent(buff_Target[id], true, bufftype_Ability[buffType])
        endif
        
        set buff_TriggeredBuff = id
        call TriggerEvaluate(bufftype_TriggerApply[buffType])
        
        //Events still used to block certain classified buffs or change duration/strength.
        set buff_Event_Applied = 1
        set buff_Event_Applied = 0
        
        return id
    endfunction
    
    //Apply a buff on a unit.
    function Buff_Apply takes unit source, unit target, integer buffType, real duration returns integer
        local integer id = Buff_GetBuffIndex(target, buffType)
        
        if id > 0 or bufftype_StackingType[buffType] == STACKING_TYPE_PARALLEL then
            return Buff_Create(source, target, buffType, duration)
            
        elseif bufftype_StackingType[buffType] == STACKING_TYPE_CUSTOM then
            set buff_TriggeredBuff = id
            set buff_EventResponse_Target = target
            set buff_EventResponse_Source = source
            set buff_EventResponse_Bufftype = buffType
            set buff_EventResponse_Duration = duration
            if TriggerEvaluate(bufftype_TriggerStack[buffType]) then
                return Buff_Create(source, target, buffType, duration)
            endif
        endif
        
        return 0
    endfunction
    
    //Loop through all buffs on a given unit.
    function ForBuff takes unit target, trigger callback returns nothing
        local integer i = LL_Buff_FirstIndex[GetUnitUserData(target)]
        local integer nextIndex
        loop
            exitwhen i == 0
            set nextIndex = LL_Buff_NextIndex[i]
            set buff_TriggeredBuff = i
            call TriggerEvaluate(callback)
            set i = nextIndex
        endloop
    endfunction
    
endlibrary
//! runtextmacro CREATE_LINKED_LIST("Buff", "false", "8191")

(This might not be 100% working cause I fiddled with it just now :D)

Anyway here is a testmap of the system and the required libraries.
I also attached a second map where the integer interval is implemented.
All you have to do is call ApplyDurationInterval(whichUnit) for each unit every time when your round ends.
I decided not to do it for all buffs at once because you might only want it to do for the units of the player that just finished his round or something else.
Anyway, you can pick all units on the map and do this.
It wont matter if that unit does not have any buff at all.
The example however is not really working on this version any more but it is good to watch how the buff is made (Object Merger).

You can add classifications like Positive, Negative, Magical, Physical, Level, Dispellable, Undispellable, etc yourself.
Just make boolean/integer variables of type array for each of the classifications.
You can set those to true or >0 to change them to whatever you want.

Dispelling buffs on a unit is done using ForBuff() where you can call Buff_Dispell(id) for each buff where Undispellable is false.
I guess you figured that one out already though :D

The buffs display a red name by default, however if you know how to add colored text in the object editor, you can make them green or whatever color you want anyway.
In the rage example, you could try this for example:
//! runtextmacro CREATE_BUFF_ABILITY("rag", "|cff00ff00Rage|r", "ReplaceableTextures\CommandButtons\BTNAncestralSpirit.blp", "This unit is affected by Rage.")

Anyway, if there are any questions, just ask.
(TimerUtils can be removed from the second map ofc.)
 

Attachments

  • Buff.w3x
    31.5 KB · Views: 80
  • Buff with integer.w3x
    30.9 KB · Views: 77
Level 19
Joined
Oct 12, 2007
Messages
1,821
Wow, that's a very good looking system Wietlol. I've been looking at it for a while and I'm impressed by how you work. There are just a few things I have to get used to when I would use it, and I (since I'm not an experienced coder) have to work and study it a bit to understand how I can do specific things.

Let's say I create a buff that will give a unit +2 agility (added with BonusMod system) and will increase his level of 'Devotion Aura' by 1. (Just a random example).
What would be a good way to store this stuff then, for when the buff will be removed. I suppose I need an 'onAdd' function and 'onRemove' function that will run when a unit gets the buff or when it will be removed.

Or hmm..
Would you maybe like to take a look at my map and just give your opinion on what you think would be smart to add and what not?
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Well, those things are pretty easy.

The system is provided with a few triggers that are automatically executed when a certain thing happens.

These are those triggers:
JASS:
        trigger array   bufftype_TriggerApply
        trigger array   bufftype_TriggerStack
        trigger array   bufftype_TriggerRemove
        trigger array   bufftype_TriggerExpire
        trigger array   bufftype_TriggerDispel

These triggers are unique for each bufftype.
You add an action to such triggers using call TriggerAddCondition(t, Filter(function f))
t should be "bufftype_TriggerApply[id]" or "bufftype_TriggerRemove[id]" or any other one depending on what you need
f should be the function name that you want to run when that trigger is ran.
Do note that conditions are used because they are faster but they require the function to return a boolean.

To sum things up:
JASS:
globals
    
    integer     BuffType_DevotionAuraUpgrade
    integer     DevotionAuraUpgrade_Ability = 'Abuf'
    integer     DevotionAuraUpgrade_Buff = 'Bbuf'
    
endglobals

//! runtextmacro CREATE_BUFF_ABILITY("buf", "|cff00ff00Devotion Aura Upgrade|r", "ReplaceableTextures\CommandButtons\BTNDevotion.blp", "This unit's Devotion Aura is more effective.")
//  call Buff_Apply(source, target, BuffType_DevotionAuraUpgrade, 5)

function DevotionAuraUpgrade_OnApply takes nothing returns boolean
    
    //Increase level of Devotion Aura for buff_Target[buff_TriggeredBuff]
    call IncUnitAbilityLevel(buff_Target[buff_TriggeredBuff], 'AHad')
    
    //Add 2 agility to buff_Target[buff_TriggeredBuff]
    call DoSomething()
    
    return false
endfunction
function DevotionAuraUpgrade_OnRemove takes nothing returns boolean
    
    //Increase level of Devotion Aura for buff_Target[buff_TriggeredBuff]
    call DecUnitAbilityLevel(buff_Target[buff_TriggeredBuff], 'AHad')
    
    //Remove 2 agility to buff_Target[buff_TriggeredBuff]
    call DoSomething()
    
    return false
endfunction

//===========================================================================
function InitTrig_DevotionAuraUpgrade takes nothing returns nothing
    set BuffType_DevotionAuraUpgrade = Buff_CreateBuffType()
    
    set bufftype_Classification_IsPositive[BuffType_DevotionAuraUpgrade] = true
    set bufftype_Classification_IsDispellable[BuffType_DevotionAuraUpgrade] = true
    
    //If you remove this, it will not show a buff but the effect will remain.
    call BuffType_SetBuff(BuffType_DevotionAuraUpgrade, DevotionAuraUpgrade_Ability, DevotionAuraUpgrade_Buff)
    
    //Register buff actions:
    call TriggerAddCondition(bufftype_TriggerApply[BuffType_DevotionAuraUpgrade], Filter(function DevotionAuraUpgrade_OnApply))
    call TriggerAddCondition(bufftype_TriggerRemove[BuffType_DevotionAuraUpgrade], Filter(function DevotionAuraUpgrade_OnRemove))
endfunction

You call call Buff_Apply(source, target, BuffType_DevotionAuraUpgrade, 5) to add the devotion aura upgrade to <target> for <5> rounds giving <source> the credits (just because you might need it for some reason).

Be aware that this system has no in-built stacking options.
By default, buffs will stack parralel so a unit can have multiple buffs of one type.
So a unit can have multiple Devotion Aura Upgrade buffs at once causing his agility and devotion aura to become more powerfull each time.
To avoid this, you can create your own stacking algorithm by adding a condition to the trigger "bufftype_TriggerStack[]".
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
Ah man, amazing Wietlol.
I'm going to try to work with your system and delve into it to try to understand all. I'll make an example spell myself to see if I get the hang of it. Can I send you my example once I'm done, just to check if I get everything right? :)
You seriously made my day!
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
You can use Muzzel's http://www.hiveworkshop.com/forums/jass-resources-412/system-buffhandler-242121/ system and just add a "counter" as a custom integer parameter to your buffs.

Then, just cycle through all buffs every round and decrease the counter variable by 1 and if it reaches 0, destroy the buff.

Muzzel's system is vJass, though. It requires a bit of extra programming knowledge to use it (basicly, your have to extend all your custom buffs from the basic struct like struct MyNewbuff extends Buff, but it's a lot cleaner than Wietlol's. I recommend looking into it.

Here's a tutorial on struct extension you might want to read:
http://www.hiveworkshop.com/forums/...83/vjass-meet-vjass-extending-structs-267719/
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
Well, Wietlol's system works fine for me now.
I suppose everything could always be done cleaner/faster/better, but at some point you just gotta go with what you've got. I think it's fine for my needs. :)
But thanks anyway!
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Well, Wietlol's system works fine for me now.
I suppose everything could always be done cleaner/faster/better, but at some point you just gotta go with what you've got. I think it's fine for my needs. :)
But thanks anyway!
Remember that cleaner code usually also means code is less prone to bugs. But yeah, if it works for you, no worries.
 
I haven't really read the thread yet, but my suggestion is that instead of decreasing the "turn" value for each buff, you start off by setting an expiration turn for the buff, which is [current turn] + [buff duration]. Then, you can just compare if [expiration turn] == [current turn], and remove it if so. Here's a simple example on how i would create this system:

JASS:
library BuffSystem requires ListModule 
  //link to ListModule: [url]http://www.wc3c.net/showthread.php?t=105486[/url]

  globals
    private hashtable Hash = InitHashtable()
  endglobals

  //I am not sure if you need to save buff count (for stacking buffs), but i added the functionality anyway.

  function GetBuffCount takes unit whichunit, integer whichbuff returns integer
    if HaveSavedInteger(Hash, GetHandleId(whichunit), whichbuff) then
      return LoadInteger(Hash, GetHandleId(whichunit), whichbuff)
    endif
    return 0
  endfunction

  function SetBuffCount takes unit whichunit, integer whichbuff, integer count returns nothing
    if count < 0 then
      call SaveInteger(Hash, GetHandleId(whichunit), whichbuff, 0)
    else
      call SaveInteger(Hash, GetHandleId(whichunit), whichbuff, count)
    endif
  endfunction

  function UnitHasBuff takes unit whichunit, integer whichbuff returns boolean
    //You might think this function is redundant, but i figured it might be handy.
    return GetBuffCount(whichunit, whichbuff) > 0 
  endfunction

  struct Buff

    implement List //I use ListModule by Grim001 for this.

    integer expires_at
    integer buff_id
    boolean stacking = false
    unit target

    static method create takes unit whichunit, integer whichbuff, integer duration, boolean stacking returns thistype
      local thistype this = thistype.allocate()
      local integer buffcount = GetBuffCount(whichunit, buffid)+1

      call .listAdd() //Add the struct instance to the list.

      set .target 	= whichunit
      set .buff_id 	= whichbuff
      set .expires_at 	= TURN + duration
      set .stacking	= stacking

      call UnitAddAbility(whichunit, whichbuff)
      if stacking then
          call SetUnitAbilityLevel(whichunit, whichbuff, buffcount)
      endif
      call SetBuffCount(whichunit, whichbuff, buffcount)

      return this
    endmethod

    static method unitRemoveAll takes unit whichunit returns nothing
      //Clears all buffs from a unit. Remember to do this when a unit dies.
      local thistype this = .first
      local thistype temp

      loop
          exitwhen this == 0
          set temp = .next

          if .target == whichunit then
	      call .destroy()
          endif

          set this == temp
      endloop
    endmethod

    method onDestroy takes nothing returns nothing
      local integer count

      call .listRemove() //Remove the buff from the list.

      if .target != null then
          set count = GetBuffCount(.target, .buff_id)

          if count > 0 then
              call SetBuffCount(.target, .buff_id, count-1)
          endif

          if .stacking then
              call SetUnitAbilityLevel(.target, count-1)
          else
              call UnitRemoveAbility(.target, .buff_id)
          endif
      endif
    endmethod

    static method update takes nothing returns nothing
      //Run this method at the start of every turn.
      local thistype this = .first
      local thistype temp

      loop
          exitwhen this == 0
          set temp = .next

          if .expires_at = TURN then
              call .destroy()
          endif

          //Here, it is also easy to add different buff events that would fire at the start of the turn.
          //Let me know if you need this. 

          set this = temp
      endloop
    endmethod

  endstruct
endlibrary

Let's say for instance that you want to create a non-stacking buff of type 'A000' and lasting 3 turns for the triggering unit:

JASS:
call Buff.create(GetTriggerUnit(), 'A000', 3, false)

Simple as that. Every turn, you update the buffs using:

JASS:
call Buff.update()

You can also remove all buffs from a unit using:
JASS:
call Buff.removeAll(GetTriggerUnit())

I haven't tried the script yet, and for the moment it is pretty basic. For instance, i haven't added a function for removing a specific buff from a unit, (right now they only expire) but this is very simple. If you are interested, i can modify it to whatever needs you have.

EDIT: Wow, the browser really fucked up my indentation. I hope it is still readable, might fix it later.
 
Last edited:
Level 24
Joined
Aug 1, 2013
Messages
4,657
@Zwiebelchen
I never said that you said it's bad, didn't I?

@Fingolfin
That method is really nice indeed and I use it for many things with actual gametime but for this case, it doesnt really matter that much.
You have to check every round if the buff expires anyway.
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
So far the system worked fine for me. I didn't encounter problems (yet).
I also just didn't start on making a dispel function yet, but I'll get into that later.

I just changed one small thing:
If the duration of a buff is set to 0, it will be permanent. If it becomes 0 after going from 1 to 0 (done by the end turn stuff) it will just be removed.
Works fine so far. :)
 
That method is really nice indeed and I use it for many things with actual gametime but for this case, it doesnt really matter that much.
You have to check every round if the buff expires anyway.

Well, with the solution Sephalo first suggested, you'd need to both reduce the expiration variable of each buff every turn, aswell as compare it to zero. Now you just need to do the comparision, so at least it is a bit more elegant.

So far the system worked fine for me. I didn't encounter problems (yet).
I also just didn't start on making a dispel function yet, but I'll get into that later.

I just changed one small thing:
If the duration of a buff is set to 0, it will be permanent. If it becomes 0 after going from 1 to 0 (done by the end turn stuff) it will just be removed.
Works fine so far. :)

Are you talking about my script now? In that case, glad that it works. If you want to make a method for removing specific buffs, the principle is the same as when removing all buffs, except you add an extra check for ".buff_id == whichbuff". Here are some other things that can be done to improve the system:

  • It could be nice to create a separate struct for the buff type, containing stuff like duration, ability id, whether it stacks, etc. That way, you could save buff type structs in global variables on initialization, and use them to simplify the input of the Buff.create method. Might be necessary if you want to add a lot of extra data to the buff. If you want, i can write you an example of this.
  • If you want to add a sort of event-like response to each turn, or for when the buff expires, you could use function interfaces (you can read about these in the jasshelper manual). Basically, you are able to save functions in member variables of the struct, and call them, provided that the function has the same parameters/returns as specified in the interface. If you create different functions for different buff types, you can save them as data and call them on the target whenever you wish.
 
Status
Not open for further replies.
Top