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

[vJASS] Custom Unit Stat Factory

Sales Pitch:


This library abstracts away all of the tedious implementation details of custom stats while still leaving you, the user, with the ability to define the overall behavior for each custom stat. Want to make a Lifesteal stat? Sure thing, just hook it up to the system and handle the
rest (damage event from an attack). What about Healing Effects? Bonus Attack Damage? Flat Movement Speed? Bonus Movement Speed? This system can handle all of those handily. All you have to do is define their behavior.

JASS:
library CStatFactory requires /*
    -----------------------
    */  optional Init    /*
    -----------------------
        You can fetch a ready-made module initializer library from the link below:
        - "https://github.com/SinisterVLuffy/NestharusJASS/blob/master/jass/Systems/Init/script.j"
    --------------
    */  Alloc   /*
    --------------
        Written by AGD, this would do well for the allocation of effectively unlimited instances.
        - "https://www.hiveworkshop.com/threads/global-alloc.324937/"
    --------------
    */  ListT   /*
    --------------
        The one by Bannar is most preferred:
        - https://www.hiveworkshop.com/threads/containers-list-t.249011/#post-3271599
    ------------------
    */  UnitDex     /*
    ------------------
        Written by TriggerHappy. Make sure that the Detect Leave ability is copied into your map
        - https://www.hiveworkshop.com/threads/system-unitdex-unit-indexer.248209/
    --------------
    */  Table   /*
    --------------
        Written by Bribe. A staple for a lot of vJASS resources.
        - https://www.hiveworkshop.com/threads/lua-vjass-new-table.188084/page-8#post-3448830
        
    ---------------------------
    */  optional Bitwise     /*
    ---------------------------
        Written by Bannar, with significant contributions from Nestharus and Magtheridon.
        Due to this system's use of bitwise operations which older versions (1.30 and below)
        of wc3 do not offer directly, this optional dependency is needed to bridge that gap
        in features.
        - "https://www.hiveworkshop.com/threads/snippet-bitwise.249223/"

     ---------------------------------------------------------------------------
    |
    |       Custom Unit Stat Factory:
    |           v.1.0
    |           MyPad
    |
    |---------------------------------------------------------------------------
    |
    |   This library offers a straightforward implementation of custom unit stats, as
    |   well as any other bonuses (bonii) offered. While initially designed for
    |   float-based custom stats, this version offers int-based custom stats as well.
    |   
    |   This library approaches the problem of dealing with multiple modifier instances
    |   in a manner that treats them as struct instances themselves. Thus, applying and revoking
    |   bonuses for custom unit stats comes as naturally as .create() and .destroy() does
    |   for structs.
    |
    |   The removal of modifier instances is handled automatically via
    |   the event OnUnitDeindex (provided by UnitDex). Hence, there's little to no need
    |   to worry about removing them manually upon the removal of units affected by
    |   said modifier instances.
    |
    |---------------------------------------------------------------------------
    |
    |   External API:
    |
    |       class CStat
    |       {
    |           method operator real=       -> real
    |           method operator amount=     -> real
    |               - Returns the float value of the modifier instance.
    |               - Also permits the changing of the float value to your desired value.
    |
    |           method operator int=        -> integer
    |               - The numerator value (default: 0)
    |           method operator divisor=    -> integer
    |               - The denominator value (default: 1)
    |               - Note: The equivalent set operation never allows a value of 0.
    |
    |           method operator owner       -> unit
    |               - The unit the bonus instance acts upon.
    |           method operator modClass    -> integer
    |               - The class ID of the instance.
    |               - Trying to get the class ID of the instance via .typeid will only return the
    |                 class ID of CStat.
    |           method operator statType    -> integer
    |               - Describes whether the modifier instance is based on a float-based custom stat
    |                 or an int-based custom stat.
    |               - Enum Values:
    |                   - CSTAT_TYPE_FLOAT  : 1 (Default)
    |                   - CSTAT_TYPE_INT    : 2
    |
    |           method isActive()           -> boolean
    |               - Returns true if the modifier instance is active.
    |           method activate(boolean flag)
    |               - Increments or decrements the active counter of the modifier instance based on
    |                 the flag value. (True activates, False deactivates)
    |       }
    |
    |       modules:
    |           
    |           When implementing these modules, it's imperative to define methods predicated
    |           with the "virtual" keyword. Those prefixed with "stub" are optionally defined.
    |
    |           module CUnitStatFactoryHeader
    |           {
    |               ==============================================
    |                               Overrideable
    |               ==============================================
    |               virtual static constant method STAT_TYPE() -> int
    |                   - This determines whether the custom stat is float-based or int-based/
    |                   - Enum values:
    |                       - CSTAT_TYPE_FLOAT (Default)
    |                       - CSTAT_TYPE_INT
    |               
    |               stub method onApply(unit whichUnit, real amount)
    |                   - Runs whenever a modifier instance's (real/float) value is modified
    |                     via .real= or .amount=
    |
    |               stub method onApplyInt(unit whichUnit, integer amount)
    |                   - Runs whenever a modifier instance's (integer) value is modified
    |                     via .int= or .divisor=
    |
    |               stub method onBonusCalc(unit whichUnit, real base, real sum,
    |                                       real product, boolean zeroMultiple) -> real
    |                   - Returns the final value of the specified custom stat
    |                     for the unit (after calculations).
    |                   - Used immediately in the method .onApply()
    |
    |               stub method onBonusCalcInt(unit whichUnit, integer base, integer sum,
    |                                       integer product, boolean zeroMultiple) -> integer
    |                   - Same as .onBonusCalc() but for integers.
    |
    |               ==============================================
    |                               Non-overrideable
    |               ==============================================
    |               static method apply(unit whichUnit, real value, integer modType) -> CStat
    |                   - The function which defines this system.
    |                   - Applies a modifier instance onto a unit. The class ID of
    |                     the instance depends on the class/struct calling this method.
    |                   - Fails when the unit isn't registered.
    |                       - Units are registered automatically to the system by default.
    |                         Hence, the scenario above wouldn't likely occur.
    |                   - Valid values for modType:
    |                       - STAT_ADD
    |                       - STAT_MULT
    |
    |               static method applyInt(unit whichUnit, int value, integer modType) -> CStat
    |                   - Same as apply for int-based custom stats.
    |                   - Valid values for modType:
    |                       - STAT_ADD
    |                       - STAT_MULT
    |                   
    |               static method getProduct(unit whichUnit) -> real
    |                   - Returns the aggregated product of all nonzero multiplicative modifiers of
    |                     the unit's custom stat.
    |
    |               static method getSum(unit whichUnit) -> real
    |                   - Returns the aggregated sum of all additive modifiers of
    |                     the unit's custom stat.
    |
    |               static method getBaseValue(unit whichUnit) -> real
    |                   - Returns the unit's base custom stat.
    |
    |               static method getTotalValue(unit whichUnit) -> real
    |                   - Returns the unit's custom stat value after modifiers.
    |
    |               static method setBaseValueEx(unit whichUnit, real value, boolean silentUpdate)
    |                   - Updates the unit's base custom stat to the specified value.
    |                   - If silentUpdate is true, this will not update the unit's current
    |                     custom stat value.
    |
    |               static method setBaseValue(unit whichUnit, real value)
    |                   - A wrapper method for setBaseValueEx(whichUnit, value, silentUpdate)
    |                     with false as the parameter for "silentUpdate"
    |
    |               static method getIntProduct(unit whichUnit) -> integer
    |               static method getIntSum(unit whichUnit) -> integer
    |               static method getIntBaseValue(unit whichUnit) -> integer
    |               static method getIntTotalValue(unit whichUnit) -> integer
    |               static method setIntBaseValueEx(unit whichUnit, integer value, boolean silentUpdate)
    |               static method setIntBaseValue(unit whichUnit, integer value)
    |                   - All integer equivalents of the above operations.
    |           }
    |
    |           module CUnitStatFactoryFooter
    |           {
    |               ==============================================
    |                               Defineable
    |               ==============================================
    |               stub static method onRegister(unit whichUnit)
    |                   - Runs whenever a unit has been registered to the system.
    |                   - By default, this runs whenever a unit is created.
    |                   - You can specify which units to register via .onFilterRegister(whichUnit).
    |
    |               stub static method onUnregister(unit whichUnit)
    |                   - Runs whenever a registered unit has left the game
    |                     (either through decay, RemoveUnit or ExlodeUnit).
    |
    |               stub static method onFilterRegister(unit whichUnit) -> boolean
    |                   - Runs whenever a unit is created.
    |                   - Return true to register the unit, or false to ignore it.
    |           }
    |
    |           module CUnitStatFactory
    |           {
    |               module CUnitStatFactoryHeader
    |               module CUnitStatFactoryFooter
    |           }
    |
     ---------------------------------------------------------------------------
*/
globals
    private integer requestFlag                     = 0
    private trigger onEnterHandler                  = CreateTrigger()
    private trigger onLeaveHandler                  = CreateTrigger()
    //  Stat Type
    constant integer CSTAT_TYPE_FLOAT               = 1
    constant integer CSTAT_TYPE_INT                 = 2
    //  Modifier types
    constant integer STAT_ADD                       = 1
    constant integer STAT_MULT                      = 2
    //  Request types
    public constant integer REQUEST_DIRECT_AMOUNT   = 1
    public constant integer REQUEST_MODIFY_ATTR     = 2
    public constant integer REQUEST_CHANGE_COUNTER  = 4
    //  Event types
    private constant integer EVENT_STAT_MODIFY      = 1
    private constant integer EVENT_STAT_ACTIVATE    = 2
    private constant integer EVENT_STAT_DEACTIVATE  = 4
endglobals
static if not LIBRARY_Init then
private module Init
    private static method onInit takes nothing returns nothing
        call thistype.init()
    endmethod
endmodule
endif
//  ================================================================
//              Bitwise Interface functions
//  ================================================================
private function BitAnd takes integer a, integer b returns integer
    static if not LIBRARY_Bitwise then
        return BlzBitAnd(a, b)
    else
        return Bitwise.AND(a, b)
    endif
endfunction
private function BitOr takes integer a, integer b returns integer
    static if not LIBRARY_Bitwise then
        return BlzBitOr(a, b)
    else
        return Bitwise.OR(a, b)
    endif
endfunction
//  ================================================================
private struct CUnitStatHandler extends array
    //  This is defined through the generated module below.
    static trigger array modifyResponse
    static trigger array activationResponse
    static trigger array deactivationResponse
    //  Modify event values
    readonly static integer eventIndex          = 0
    readonly static integer array eventType
    readonly static unit array curUnit
    readonly static real array prevAmt
    readonly static real array curAmt
    readonly static integer array prevIntAmt
    readonly static integer array prevIntDivAmt
    readonly static integer array curIntAmt
    readonly static integer array curIntDivAmt
    readonly static CStat array curStat
    readonly static integer array curStatType
    //  Activation and deactivation event values
    readonly static boolean array isActivating
    //  Called by the operator CStat:real=, CStat:amount=
    static method prepModifyHandler takes unit whichUnit, real prev, real cur, CStat stat returns nothing
        set eventIndex              = eventIndex + 1
        set eventType[eventIndex]   = EVENT_STAT_MODIFY
        set curUnit[eventIndex]     = whichUnit
        set prevAmt[eventIndex]     = prev
        set curAmt[eventIndex]      = cur
        set curStat[eventIndex]     = stat
        set curStatType[eventIndex] = CSTAT_TYPE_FLOAT
    endmethod
    //  Called by the operator CStat:int=, CStat:divisor=
    static method prepModifyIntHandler takes unit whichUnit, integer prevNum, integer curNum, integer prevDiv, integer curDiv, CStat stat returns nothing
        set eventIndex                  = eventIndex + 1
        set eventType[eventIndex]       = EVENT_STAT_MODIFY
        set curUnit[eventIndex]         = whichUnit
        set prevIntAmt[eventIndex]      = prevNum
        set curIntAmt[eventIndex]       = curNum
        set prevIntDivAmt[eventIndex]   = prevDiv
        set curIntDivAmt[eventIndex]    = curDiv
        set curStat[eventIndex]         = stat
        set curStatType[eventIndex]     = CSTAT_TYPE_INT
    endmethod
    //  Called by the method incActiveCounter and decActiveCounter.
    static method prepActivationHandler takes unit whichUnit, boolean state, CStat stat returns nothing
        set eventIndex                  = eventIndex + 1
        set isActivating[eventIndex]    = state
        set curUnit[eventIndex]         = whichUnit
        set curStat[eventIndex]         = stat
        if (state) then
            set eventType[eventIndex]   = EVENT_STAT_ACTIVATE
        else
            set eventType[eventIndex]   = EVENT_STAT_DEACTIVATE
        endif
    endmethod
    static method callModifyHandler takes integer modClass returns nothing
        call TriggerEvaluate(modifyResponse[modClass])
    endmethod
    static method callActivationHandler takes integer modClass returns nothing
        call TriggerEvaluate(activationResponse[modClass])
    endmethod
    static method callDeactivationHandler takes integer modClass returns nothing
        call TriggerEvaluate(deactivationResponse[modClass])
    endmethod
    static method releaseHandler takes nothing returns nothing
        set eventIndex  = IMaxBJ(eventIndex - 1, 0)
    endmethod
    private static method onEnter takes nothing returns nothing
        call TriggerEvaluate(onEnterHandler)
    endmethod
    private static method onLeave takes nothing returns nothing
        call TriggerEvaluate(onLeaveHandler)
    endmethod
    private static method init takes nothing returns nothing
        set onEnterHandler  = CreateTrigger()
        set onLeaveHandler  = CreateTrigger()
        call OnUnitIndex(function thistype.onEnter)
        call OnUnitDeindex(function thistype.onLeave)
    endmethod
    implement Init
endstruct
//  ================================================================
private function IsFlagRequested takes integer flag returns boolean
    return BitAnd(requestFlag, flag) != 0
endfunction
//  ================================================================
//  While technically public functions, these should not be used
//  manually at any point, since the system requires that these
//  functions be accessible from the module side, which cannot be
//  possible when such functions are declared private.
//  ================================================================
public function AssignModifuncToClass takes integer classID, code callback returns nothing
    set CUnitStatHandler.modifyResponse[classID]        = CreateTrigger()
    call TriggerAddCondition(CUnitStatHandler.modifyResponse[classID], Condition(callback))
endfunction
public function AssignActifuncToClass takes integer classID, code callback returns nothing
    set CUnitStatHandler.activationResponse[classID]    = CreateTrigger()
    call TriggerAddCondition(CUnitStatHandler.activationResponse[classID], Condition(callback))
endfunction
public function AssignDeactifuncToClass takes integer classID, code callback returns nothing
    set CUnitStatHandler.deactivationResponse[classID]  = CreateTrigger()
    call TriggerAddCondition(CUnitStatHandler.deactivationResponse[classID], Condition(callback))
endfunction
//  ================================================================
//  These public functions are 
//  ================================================================
public function GetCurrentUnit takes nothing returns unit
    return CUnitStatHandler.curUnit[CUnitStatHandler.eventIndex]
endfunction
public function GetPrevAmount takes nothing returns real
    return CUnitStatHandler.prevAmt[CUnitStatHandler.eventIndex]
endfunction
public function GetCurrentAmount takes nothing returns real
    return CUnitStatHandler.curAmt[CUnitStatHandler.eventIndex]
endfunction
public function GetPrevIntAmount takes nothing returns integer
    return CUnitStatHandler.prevIntAmt[CUnitStatHandler.eventIndex]
endfunction
public function GetPrevDivisorAmount takes nothing returns integer
    return CUnitStatHandler.prevIntDivAmt[CUnitStatHandler.eventIndex]
endfunction
public function GetCurrentIntAmount takes nothing returns integer
    return CUnitStatHandler.curIntAmt[CUnitStatHandler.eventIndex]
endfunction
public function GetCurrentDivisorAmount takes nothing returns integer
    return CUnitStatHandler.curIntDivAmt[CUnitStatHandler.eventIndex]
endfunction
public function GetCurrentStatType takes nothing returns integer
    return CUnitStatHandler.curStatType[CUnitStatHandler.eventIndex]
endfunction
public function GetCurrentStat takes nothing returns CStat
    return CUnitStatHandler.curStat[CUnitStatHandler.eventIndex]
endfunction
public function GetStatActivationState takes nothing returns boolean
    return CUnitStatHandler.isActivating[CUnitStatHandler.eventIndex]
endfunction
public function SetRequestFlag takes integer flag returns nothing
    set requestFlag = BitOr(requestFlag, flag)
endfunction
public function ResetRequestFlag takes integer flag returns nothing
    set requestFlag = BitAnd(requestFlag, -flag - 1)
endfunction
public function RegisterEnterHandler takes code func returns nothing
    call TriggerAddCondition(onEnterHandler, Condition(func))
endfunction
public function RegisterLeaveHandler takes code func returns nothing
    call TriggerAddCondition(onLeaveHandler, Condition(func))
endfunction
//  ================================================================
//  ================================================================
//! runtextmacro DEFINE_LIST("", "StatList", "integer")
struct CStat extends array
    implement optional GlobalAlloc
    implement optional AllocT
    private static TableArray hashMap   = 0
    //  =========================================== //
    //              Instance Values                 //
    //  =========================================== //
    method operator real takes nothing returns real
        if (hashMap[3].integer[this] > 0) then
            return 0.0
        endif
        return hashMap[0].real[this]
    endmethod
    method operator int takes nothing returns integer
        if (hashMap[3].integer[this] > 0) then
            return 0
        endif
        return hashMap[7].integer[this]
    endmethod
    method operator divisor takes nothing returns integer
        if (hashMap[3].integer[this] > 0) then
            return 1
        endif
        return hashMap[8].integer[this]
    endmethod
    method operator amount takes nothing returns real
        return this.real
    endmethod
    
    method operator modType takes nothing returns integer
        return hashMap[1].integer[this]
    endmethod
    private method operator activeCounter takes nothing returns integer
        return hashMap[2].integer[this]
    endmethod
    private method operator zeroCounter takes nothing returns integer
        return hashMap[3].integer[this]
    endmethod
    method operator owner takes nothing returns unit
        return hashMap[4].unit[this]
    endmethod
    method operator listPtr takes nothing returns StatListItem
        return hashMap[5].integer[this]
    endmethod
    method operator modClass takes nothing returns integer
        return hashMap[6].integer[this]
    endmethod
    method operator statType takes nothing returns integer
        return hashMap[9].integer[this]
    endmethod
    //  =========================================== //
    //              Instance Values                 //
    //  =========================================== //
    //! textmacro CSTAT_SETTER_HEADER takes OPNAME, TYPE, HASHINDEX
    method operator $OPNAME$= takes $TYPE$ value returns nothing
        if (value == this.$OPNAME$) then
            return
        endif
        //  If REQUEST_DIRECT_AMOUNT is flagged, that means
        //  the handlers that observe any changes will not
        //  be notified.
        //  Otherwise, if the modifier is suppressed via
        //  activate(false), any attempts at changing
        //  the value will not reflect on the affected unit,
        //  but will still be considered upon reactivation.
        if ((IsFlagRequested(REQUEST_DIRECT_AMOUNT)) or /*
        */  (hashMap[2].integer[this] <= 0)) then
            set hashMap[$HASHINDEX$].real[this]       = value
            return
        endif
    //! endtextmacro
    //! textmacro CSTAT_SETTER_FOOTER takes OPNAME, TYPE, HASHINDEX
        set hashMap[$HASHINDEX$].$TYPE$[this]       = value
        call CUnitStatHandler.callModifyHandler(hashMap[6].integer[this])
        call CUnitStatHandler.releaseHandler()
    endmethod
    //! endtextmacro
    //! runtextmacro CSTAT_SETTER_HEADER("real", "real", "0")
        call CUnitStatHandler.prepModifyHandler(hashMap[4].unit[this], hashMap[0].real[this], value, this)
    //! runtextmacro CSTAT_SETTER_FOOTER("real", "real", "0")
    //! runtextmacro CSTAT_SETTER_HEADER("int", "integer", "7")
        call CUnitStatHandler.prepModifyIntHandler(hashMap[4].unit[this], hashMap[7].integer[this], value, this.divisor, this.divisor, this)
    //! runtextmacro CSTAT_SETTER_FOOTER("int", "integer", "7")
    
    //! runtextmacro CSTAT_SETTER_HEADER("divisor", "integer", "8")
        //  Don't allow division by zero at all costs.
        if (value == 0) then
            set value = 1
        endif
        call CUnitStatHandler.prepModifyIntHandler(hashMap[4].unit[this], this.int, this.int, hashMap[8].integer[this], value, this)
    //! runtextmacro CSTAT_SETTER_FOOTER("divisor", "integer", "8")
    method operator amount= takes real newValue returns nothing
        set this.real = newValue
    endmethod
    
    method operator modType= takes integer value returns nothing
        if (not IsFlagRequested(REQUEST_MODIFY_ATTR)) then
            return
        endif
        set hashMap[1].integer[this]    = value
    endmethod
    method operator activeCounter= takes integer value returns nothing
        if (not IsFlagRequested(REQUEST_MODIFY_ATTR)) then
            return
        endif
        set hashMap[2].integer[this]    = value
    endmethod
    method operator zeroCounter= takes integer value returns nothing
        if (not IsFlagRequested(REQUEST_CHANGE_COUNTER)) then
            return
        endif
        set hashMap[3].integer[this]    = value
    endmethod
    method operator owner= takes unit value returns nothing
        if (not IsFlagRequested(REQUEST_MODIFY_ATTR)) then
            return
        endif
        set hashMap[4].unit[this]       = value
    endmethod
    method operator listPtr= takes StatListItem value returns nothing
        if (not IsFlagRequested(REQUEST_MODIFY_ATTR)) then
            return
        endif
        set hashMap[5].integer[this]    = value
    endmethod
    method operator modClass= takes integer whichClass returns nothing
        if (not IsFlagRequested(REQUEST_MODIFY_ATTR)) then
            return
        endif
        set hashMap[6].integer[this]    = whichClass
    endmethod
    method operator statType= takes integer statType returns nothing
        if (not IsFlagRequested(REQUEST_MODIFY_ATTR)) then
            return
        endif
        set hashMap[9].integer[this]    = statType
    endmethod
    private method incActiveCounter takes nothing returns nothing
        local integer value             = hashMap[2].integer[this] + 1
        set hashMap[2].integer[this]    = value
        if (value == 1) then
            call CUnitStatHandler.prepActivationHandler(hashMap[4].unit[this], true, this)
            call CUnitStatHandler.callActivationHandler(hashMap[6].integer[this])
            call CUnitStatHandler.releaseHandler()
        endif
    endmethod
    private method decActiveCounter takes nothing returns nothing
        local integer value             = hashMap[2].integer[this] - 1
        set hashMap[2].integer[this]    = value
        //  Call activation and deactivation handlers.
        if (value == 0) then
            call CUnitStatHandler.prepActivationHandler(hashMap[4].unit[this], false, this)
            call CUnitStatHandler.callDeactivationHandler(hashMap[6].integer[this])
            call CUnitStatHandler.releaseHandler()
        endif
    endmethod
    method nullifiesProduct takes nothing returns boolean
        return this.zeroCounter > 0
    endmethod
    method isActive takes nothing returns boolean
        return this.activeCounter > 0
    endmethod
    method activate takes boolean flag returns nothing
        if (flag) then
            call this.incActiveCounter()
        else
            call this.decActiveCounter()
        endif
    endmethod
    method nullify takes boolean flag returns nothing
        if (not IsFlagRequested(REQUEST_CHANGE_COUNTER)) then
            return
        endif
        if (flag) then
            set hashMap[3].integer[this]    = hashMap[3].integer[this] + 1
        else
            set hashMap[3].integer[this]    = hashMap[3].integer[this] - 1
        endif
    endmethod
    method destroy takes nothing returns nothing
        if (this.modType == 0) then
            return
        endif
        call SetRequestFlag(REQUEST_MODIFY_ATTR)
        /*
        if (this.modType == STAT_ADD) then
            set this.amount = 0.0
        else
            set this.amount = 1.0
        endif
        */
        //  if (this.activeCounter > 0)
        if (hashMap[2].integer[this] > 0) then
            set hashMap[2].integer[this]  = 1
            call this.decActiveCounter()
        endif
        //  Addition: (For integer-based stats)
        call hashMap[9].integer.remove(this)
        call hashMap[8].integer.remove(this)
        call hashMap[7].integer.remove(this)
        //  Clear out all mapped data.
        call hashMap[6].integer.remove(this)
        call hashMap[5].integer.remove(this)
        call hashMap[4].unit.remove(this)
        call hashMap[3].integer.remove(this)
        call hashMap[2].integer.remove(this)
        call hashMap[1].integer.remove(this)
        call hashMap[0].real.remove(this)
        call this.deallocate()
        call ResetRequestFlag(REQUEST_MODIFY_ATTR)
    endmethod
    static method create takes unit whichUnit, integer manifest returns thistype
        local thistype this     = thistype.allocate()
        call SetRequestFlag(REQUEST_DIRECT_AMOUNT + REQUEST_MODIFY_ATTR)
        if (manifest == STAT_ADD) then
            set this.real       = 0.0
            set this.int        = 0
        // else if (manifest == STAT_MULT)
        else
            set this.real       = 1.0
            set this.int        = 1
        endif
        set this.divisor        = 1
        set this.modType        = manifest
        set this.activeCounter  = 1
        set this.zeroCounter    = 0
        set this.owner          = whichUnit
        set this.modClass       = 0
        call ResetRequestFlag(REQUEST_DIRECT_AMOUNT + REQUEST_MODIFY_ATTR)
        return this
    endmethod
    private static method init takes nothing returns nothing
        set hashMap = TableArray[10]
    endmethod
    implement Init
endstruct
//  ================================================================
module CUnitStatFactoryHeader
    boolean registered
    StatList addList
    StatList multList
    StatList zeroList
    real     pSum
    real     pProduct
    real     pBaseValue
    integer  pIntSum
    integer  pIntProduct
    integer  pIntBaseValue
    //  @Stub
    static if not thistype.STAT_TYPE.exists then
    static constant method STAT_TYPE takes nothing returns integer
        return CSTAT_TYPE_FLOAT
    endmethod
    endif
    static method apply takes unit whichUnit, real value, integer modType returns CStat
        local CStat newStat = 0
        local thistype this = thistype(GetUnitId(whichUnit))
        if ((not this.registered) or (this == 0)) or /*
        */ ((modType != STAT_ADD) and (modType != STAT_MULT)) then
            return CStat(0)
        endif
        set newStat             = CStat.create(whichUnit, modType)
        call CStatFactory_SetRequestFlag(REQUEST_MODIFY_ATTR)
        set newStat.modClass    = thistype.typeid
        set newStat.statType    = thistype.STAT_TYPE()
        call CStatFactory_ResetRequestFlag(REQUEST_MODIFY_ATTR)
        set newStat.amount      = value
        return newStat
    endmethod
    static method applyInt takes unit whichUnit, integer value, integer modType returns CStat
        local CStat newStat     = 0
        local thistype this = thistype(GetUnitId(whichUnit))
        if ((not this.registered) or (this == 0)) or /*
        */ ((modType != STAT_ADD) and (modType != STAT_MULT)) then
            return CStat(0)
        endif
        set newStat             = CStat.create(whichUnit, modType)
        call CStatFactory_SetRequestFlag(REQUEST_MODIFY_ATTR)
        set newStat.modClass    = thistype.typeid
        set newStat.statType    = thistype.STAT_TYPE()
        call CStatFactory_ResetRequestFlag(REQUEST_MODIFY_ATTR)
        set newStat.int         = value
        return newStat
    endmethod
    //  @stub
    static if (not thistype.onApply.exists) then
    method onApply takes unit whichUnit, real amount returns nothing
    endmethod
    endif
    //  @stub
    static if (not thistype.onApplyInt.exists) then
    method onApplyInt takes unit whichUnit, integer amount returns nothing
    endmethod
    endif
    //  @stub
    static if not thistype.onBonusCalc.exists then 
    method onBonusCalc takes unit whichUnit, real base, real sum, real product, boolean zeroMultiple returns real
        if (zeroMultiple) then
            return sum
        endif
        return base*product + sum
    endmethod
    endif
    //  @stub
    static if not thistype.onBonusCalcInt.exists then 
    method onBonusCalcInt takes unit whichUnit, integer base, integer sum, integer product, boolean zeroMultiple returns integer
        if (zeroMultiple) then
            return sum
        endif
        return base*product + sum
    endmethod
    endif
    method bonusCalc takes unit whichUnit returns nothing
        static if thistype.onApply.exists then
        if thistype.STAT_TYPE() == CSTAT_TYPE_FLOAT then
            call this.onApply(whichUnit, this.onBonusCalc(whichUnit, this.pBaseValue, this.pSum, this.pProduct, not this.zeroList.empty()))
        endif
        endif
        static if thistype.onApplyInt.exists then
        if thistype.STAT_TYPE() == CSTAT_TYPE_INT then
            call this.onApplyInt(whichUnit, this.onBonusCalcInt(whichUnit, this.pIntBaseValue, this.pIntSum, this.pIntProduct, not this.zeroList.empty()))
        endif
        endif
    endmethod
    static method getProduct takes unit whichUnit returns real
        return thistype(GetUnitId(whichUnit)).pProduct
    endmethod
    static method getSum takes unit whichUnit returns real
        return thistype(GetUnitId(whichUnit)).pSum
    endmethod
    static method getBaseValue takes unit whichUnit returns real
        return thistype(GetUnitId(whichUnit)).pBaseValue
    endmethod
    static method setBaseValueEx takes unit whichUnit, real value, boolean silentUpdate returns nothing
        local thistype this = thistype(GetUnitId(whichUnit))
        set this.pBaseValue = value
        if (not silentUpdate) then
            call this.bonusCalc(whichUnit)
        endif
    endmethod
    static method setBaseValue takes unit whichUnit, real value returns nothing
        call thistype.setBaseValueEx(whichUnit, value, false)
    endmethod
    static method getTotalValue takes unit whichUnit returns real
        local thistype this = thistype(GetUnitId(whichUnit))
        return this.onBonusCalc(whichUnit, this.pBaseValue, this.pSum, this.pProduct, not this.zeroList.empty())
    endmethod
    static method getIntProduct takes unit whichUnit returns integer
        return thistype(GetUnitId(whichUnit)).pIntProduct
    endmethod
    static method getIntSum takes unit whichUnit returns integer
        return thistype(GetUnitId(whichUnit)).pIntSum
    endmethod
    static method getIntBaseValue takes unit whichUnit returns integer
        return thistype(GetUnitId(whichUnit)).pIntBaseValue
    endmethod
    static method setIntBaseValueEx takes unit whichUnit, integer value, boolean silentUpdate returns nothing
        local thistype this     = thistype(GetUnitId(whichUnit))
        set this.pIntBaseValue  = value
        if (not silentUpdate) then
            call this.bonusCalc(whichUnit)
        endif
    endmethod
    static method setIntBaseValue takes unit whichUnit, integer value returns nothing
        call thistype.setIntBaseValueEx(whichUnit, value, false)
    endmethod
    static method getIntTotalValue takes unit whichUnit returns integer
        local thistype this = thistype(GetUnitId(whichUnit))
        return this.onBonusCalcInt(whichUnit, this.pIntBaseValue, this.pIntSum, this.pIntProduct, not this.zeroList.empty())
    endmethod
endmodule
module CUnitStatFactoryFooter
    static method register takes unit whichUnit returns boolean
        local thistype this = thistype(GetUnitId(whichUnit))
        if ((this.registered) or (this == 0)) then
            return false
        endif
        set this.registered     = true
        set this.pBaseValue     = 0.0
        set this.pSum           = 0.0
        set this.pProduct       = 1.0
        set this.pIntBaseValue  = 0
        set this.pIntSum        = 0
        set this.pIntProduct    = 1
        set this.addList        = StatList.create()
        set this.multList       = StatList.create()
        set this.zeroList       = StatList.create()
        //  Perhaps the base value is overridden here, eh?
        static if thistype.onRegister.exists then
            call thistype.onRegister(whichUnit)
        endif
        return true
    endmethod
    private static method onModifyInt takes unit whichUnit, integer pastNum, integer curNum, integer pastDiv, integer curDiv, CStat curStat returns nothing
        local thistype this     = thistype(GetUnitId(whichUnit))
        if (not curStat.isActive()) then
            return
        endif
        if (curStat.modType == STAT_ADD) then
            call CStatFactory_SetRequestFlag(REQUEST_MODIFY_ATTR)
            if (curStat.listPtr == 0) then
                set curStat.listPtr = this.addList.push(curStat).last
                set this.pIntSum    = this.pIntSum + (curNum / curDiv)
            else
                set this.pIntSum    = this.pIntSum - (pastNum / pastDiv) + (curNum / curDiv)
            endif
            call CStatFactory_ResetRequestFlag(REQUEST_MODIFY_ATTR)
            call this.bonusCalc(whichUnit)
            return
        endif
        if (curNum == 0) and (pastNum != 0) then
            call CStatFactory_SetRequestFlag(REQUEST_CHANGE_COUNTER)
            if (not curStat.nullifiesProduct()) then
                call CStatFactory_SetRequestFlag(REQUEST_MODIFY_ATTR)
                call this.multList.erase(curStat.listPtr)
                set curStat.listPtr = this.zeroList.push(curStat).last
                call CStatFactory_ResetRequestFlag(REQUEST_MODIFY_ATTR)
                //  set this.pIntProduct    = this.pIntProduct / pastValue
                set this.pIntProduct        = this.pIntProduct * (pastDiv / pastNum)
                call curStat.nullify(true)
            endif
            call CStatFactory_ResetRequestFlag(REQUEST_CHANGE_COUNTER)
        elseif (pastNum == 0) and (curNum != 0) then
            call CStatFactory_SetRequestFlag(REQUEST_CHANGE_COUNTER)
            if (curStat.nullifiesProduct()) then
                call CStatFactory_SetRequestFlag(REQUEST_MODIFY_ATTR)
                call this.zeroList.erase(curStat.listPtr)
                set curStat.listPtr = this.multList.push(curStat).last
                call CStatFactory_ResetRequestFlag(REQUEST_MODIFY_ATTR)
                set this.pIntProduct        = this.pIntProduct * (curNum / curDiv)
                call curStat.nullify(false)
            endif
            call CStatFactory_ResetRequestFlag(REQUEST_CHANGE_COUNTER)
        else
            set this.pIntProduct            = this.pIntProduct * (pastDiv / pastNum) * (curNum / curDiv)
        endif
        call this.bonusCalc(whichUnit)
    endmethod
    private static method onModify takes unit whichUnit, real pastValue, real curValue, CStat curStat returns nothing
        local thistype this     = thistype(GetUnitId(whichUnit))
        if (not curStat.isActive()) then
            return
        endif
        if (curStat.modType == STAT_ADD) then
            call CStatFactory_SetRequestFlag(REQUEST_MODIFY_ATTR)
            if (curStat.listPtr == 0) then
                set curStat.listPtr = this.addList.push(curStat).last
                set this.pSum       = this.pSum + curValue
            else
                set this.pSum       = this.pSum - pastValue + curValue
            endif
            call CStatFactory_ResetRequestFlag(REQUEST_MODIFY_ATTR)
            call this.bonusCalc(whichUnit)
            return
        endif
        if (curValue == 0.0) and (pastValue != 0.0) then
            call CStatFactory_SetRequestFlag(REQUEST_CHANGE_COUNTER)
            if (not curStat.nullifiesProduct()) then
                call CStatFactory_SetRequestFlag(REQUEST_MODIFY_ATTR)
                call this.multList.erase(curStat.listPtr)
                set curStat.listPtr = this.zeroList.push(curStat).last
                call CStatFactory_ResetRequestFlag(REQUEST_MODIFY_ATTR)
                set this.pProduct   = this.pProduct / pastValue
                call curStat.nullify(true)
            endif
            call CStatFactory_ResetRequestFlag(REQUEST_CHANGE_COUNTER)
        elseif (pastValue == 0.0) and (curValue != 0.0) then
            call CStatFactory_SetRequestFlag(REQUEST_CHANGE_COUNTER)
            if (curStat.nullifiesProduct()) then
                call CStatFactory_SetRequestFlag(REQUEST_MODIFY_ATTR)
                call this.zeroList.erase(curStat.listPtr)
                set curStat.listPtr = this.multList.push(curStat).last
                call CStatFactory_ResetRequestFlag(REQUEST_MODIFY_ATTR)
                set this.pProduct   = this.pProduct * curValue
                call curStat.nullify(false)
            endif
            call CStatFactory_ResetRequestFlag(REQUEST_CHANGE_COUNTER)
        else
            set this.pProduct   = this.pProduct / pastValue * curValue
        endif
        call this.bonusCalc(whichUnit)
    endmethod
    private static method modify takes nothing returns nothing
        if CStatFactory_GetCurrentStatType() == CSTAT_TYPE_FLOAT then
            call thistype.onModify(CStatFactory_GetCurrentUnit(), /*
                                */ CStatFactory_GetPrevAmount(), /*
                                */ CStatFactory_GetCurrentAmount(), /*
                                */ CStatFactory_GetCurrentStat())
        else
            call thistype.onModifyInt(CStatFactory_GetCurrentUnit(), /*
                                */ CStatFactory_GetPrevIntAmount(), /*
                                */ CStatFactory_GetCurrentIntAmount(), /*
                                */ CStatFactory_GetPrevDivisorAmount(), /*
                                */ CStatFactory_GetCurrentDivisorAmount(), /*
                                */ CStatFactory_GetCurrentStat())
        endif
    endmethod
    private static method onActivate takes unit whichUnit, CStat curStat, thistype this returns nothing
        call CStatFactory_SetRequestFlag(CStatFactory_REQUEST_MODIFY_ATTR)
        if (curStat.statType == CSTAT_TYPE_FLOAT) then
            if (curStat.modType == STAT_ADD) then
                set curStat.listPtr = this.addList.push(curStat).last
                set this.pSum       = this.pSum + curStat.amount
            else
                if (curStat.nullifiesProduct()) then
                    set curStat.listPtr = this.zeroList.push(curStat).last
                else
                    set curStat.listPtr = this.multList.push(curStat).last
                    set this.pProduct   = this.pProduct * curStat.amount
                endif
            endif
        endif
        if (curStat.statType == CSTAT_TYPE_INT) then
            if (curStat.modType == STAT_ADD) then
                set curStat.listPtr = this.addList.push(curStat).last
                set this.pIntSum    = this.pIntSum + curStat.int / curStat.divisor
            else
                if (curStat.nullifiesProduct()) then
                    set curStat.listPtr     = this.zeroList.push(curStat).last
                else
                    set curStat.listPtr     = this.multList.push(curStat).last
                    set this.pIntProduct    = this.pIntProduct * curStat.int / curStat.divisor
                endif
            endif
        endif
        call this.bonusCalc(whichUnit)
        set curStat.listPtr = 0
        call CStatFactory_ResetRequestFlag(CStatFactory_REQUEST_MODIFY_ATTR)
    endmethod
    private static method onDeactivate takes unit whichUnit, CStat curStat, thistype this returns nothing
        local StatListItem ptr  = curStat.listPtr
        call CStatFactory_SetRequestFlag(CStatFactory_REQUEST_MODIFY_ATTR)
        if (curStat.statType == CSTAT_TYPE_FLOAT) then
            if (curStat.modType == STAT_ADD) then
                call this.addList.erase(curStat.listPtr)
                set this.pSum           = this.pSum - curStat.amount
            else
                // if curStat.zeroCounter > 0
                if (curStat.nullifiesProduct()) then
                    call this.zeroList.erase(curStat.listPtr)
                else
                    call this.multList.erase(curStat.listPtr)
                    set this.pProduct   = this.pProduct / curStat.amount
                endif
            endif
        endif
        if (curStat.statType == CSTAT_TYPE_INT) then
            if (curStat.modType == STAT_ADD) then
                call this.addList.erase(curStat.listPtr)
                set this.pIntSum        = this.pIntSum - curStat.int / curStat.divisor
            else
                // if curStat.zeroCounter > 0
                if (curStat.nullifiesProduct()) then
                    call this.zeroList.erase(curStat.listPtr)
                else
                    call this.multList.erase(curStat.listPtr)
                    set this.pIntProduct    = this.pIntProduct * curStat.divisor / curStat.int
                endif
            endif
        endif
        call this.bonusCalc(whichUnit)
        set curStat.listPtr     = 0
        call CStatFactory_ResetRequestFlag(CStatFactory_REQUEST_MODIFY_ATTR)
    endmethod
    private static method activate takes nothing returns nothing
        local unit whichUnit    = CStatFactory_GetCurrentUnit()
        local CStat curStat     = CStatFactory_GetCurrentStat()
        local boolean state     = CStatFactory_GetStatActivationState()
        local thistype this     = thistype(GetUnitId(whichUnit))
        if (state) then
            call thistype.onActivate(whichUnit, curStat, this)
        else
            call thistype.onDeactivate(whichUnit, curStat, this)
        endif
        set whichUnit   = null
    endmethod
    private static method onUnitEnter takes nothing returns nothing
        static if thistype.onFilterRegister.exists then
        if not thistype.onFilterRegister(GetIndexedUnit()) then
            return
        endif
        endif
        call thistype.register(GetIndexedUnit())
    endmethod
    private static method onUnitLeave takes nothing returns nothing
        local unit whichUnit        = GetIndexedUnit()
        local thistype this         = thistype(GetIndexedUnitId())
        local boolean wasRegistered = this.registered
        local CStat curStat
        //  Empty all the nodes from each list
        //  ====================================================
        loop
            exitwhen not wasRegistered
            loop
                exitwhen this.addList.empty()
                set curStat = CStat(this.addList.first.data)
                call curStat.destroy()
            endloop
            loop
                exitwhen this.multList.empty()
                set curStat = CStat(this.multList.first.data)
                call curStat.destroy()
            endloop
            loop
                exitwhen this.zeroList.empty()
                set curStat = CStat(this.zeroList.first.data)
                call curStat.destroy()
            endloop
            call this.zeroList.destroy()
            call this.multList.destroy()
            call this.addList.destroy()
            exitwhen true
        endloop
        //  ====================================================
        set this.registered     = false
        set this.pBaseValue     = 0.0
        set this.pSum           = 0.0
        set this.pProduct       = 0.0
        set this.pIntBaseValue  = 0
        set this.pIntSum        = 0
        set this.pIntProduct    = 0
        set this.addList        = 0
        set this.multList       = 0
        set this.zeroList       = 0
        static if thistype.onUnregister.exists then
        if wasRegistered then
            call thistype.onUnregister(whichUnit)
        endif
        endif
        set whichUnit   = null
    endmethod
    private static method onInit takes nothing returns nothing
        call CStatFactory_AssignModifuncToClass(thistype.typeid, function thistype.modify)
        call CStatFactory_AssignActifuncToClass(thistype.typeid, function thistype.activate)
        call CStatFactory_AssignDeactifuncToClass(thistype.typeid, function thistype.activate)
        call CStatFactory_RegisterEnterHandler(function thistype.onUnitEnter)
        call CStatFactory_RegisterLeaveHandler(function thistype.onUnitLeave)
    endmethod
endmodule
module CUnitStatFactory
    implement CUnitStatFactoryHeader
    implement CUnitStatFactoryFooter
endmodule
endlibrary

//  An older version of the library that only offers custom stats for real values (not explicitly integers).
//  Also the former name of the library.
library CustomUnitStatFactory requires CStatFactory
endlibrary

That's a lot of code to digest. May I present some of the examples mentioned above:

Healing Effect

Lifesteal

Attack Damage



JASS:
library HealEffect requires CStatFactory

native UnitAlive takes unit id returns boolean

struct HealEffect extends array
    implement CUnitStatFactory
   
    static method heal takes unit whichUnit, real amount returns boolean
        if not UnitAlive(whichUnit) then
            return false
        endif
        set amount = amount*HealEffect.getProduct(whichUnit) + HealEffect.getSum(whichUnit)
        call SetWidgetLife(whichUnit, GetWidgetLife(whichUnit) + amount)
        return true
    endmethod
   
    static method healTarget takes unit source, unit target, real amount returns boolean
        if not UnitAlive(target) then
            return false
        elseif (source == target) then
            return HealEffect.heal(target, amount)
        endif
        set amount = (amount*HealEffect.getProduct(source) + HealEffect.getSum(source))*HealEffect.getProduct(target)
        call SetWidgetLife(target, GetWidgetLife(target) + amount)
        return true
    endmethod
endstruct

endlibrary


Warning: Example code not optimized.

JASS:
library Lifesteal requires CustomUnitStatFactory, optional HealEffect

struct Lifesteal extends array
    implement CUnitStatFactory

    private static method onHeal takes nothing returns nothing
        local real lifesteal = Lifesteal.getTotalValue(GetEventDamageSource())*GetEventDamage()
        if IsUnitEnemy(GetTriggerUnit(), GetOwningPlayer(GetEventDamageSource())) and BlzGetEventDamageType() == DAMAGE_TYPE_NORMAL then
            static if not LIBRARY_HealEffect then
            call SetWidgetLife(GetEventDamageSource(), GetWidgetLife(GetEventDamageSource()) + lifesteal)
            else
            // Make it play well with the Healing Effect attribute
            call HealEffect.heal(GetEventDamageSource(), lifesteal)
            endif
        endif
    endmethod
    private static method init takes nothing returns nothing
        local trigger trig = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DAMAGED)
        call TriggerAddCondition(trig, Condition(function Lifesteal.onHeal))
    endmethod
    implement Init
endstruct

endlibrary

Warning: Does not compile! (Missing constants)

JASS:
library AttackDamage requires CustomUnitStatFactory

struct AttackDamage extends array
    static constant method STAT_TYPE takes nothing returns integer
        return CSTAT_TYPE_INT
    endmethod

    static method onApplyInt takes unit whichUnit, integer amount returns nothing
        call IncUnitAbilityLevel(whichUnit, ATTACK_STAT_ABIL)
        call BlzSetUnitAbilityIntegerLevelField(BlzGetUnitAbility(whichUnit, ATTACK_STAT_ABIL), ATK_STAT_FIELD, 0, amount)
        call DecUnitAbilityLevel(whichUnit, ATTACK_STAT_ABIL)
    endmethod

    static method onRegister takes unit whichUnit returns nothing
        call UnitAddAbility(whichUnit, ATTACK_STAT_ABIL)
        call UnitMakeAbilityPermanent(whichUnit, true, ATTACK_STAT_ABIL)
    endmethod

    implement CUnitStatFactory   
endstruct

endlibrary

 
Top