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

RPG Threat System v3.2c

The system provides various behaviors to creeps, like:
- Sleeping
- Wandering
- Ambushing
- Helping
- Retreating
- etc.

JASS:
library RPGTS initializer onInit uses GetClosestWidget, Table, UnitIndexer, optional TimerUtils

    /************************************************************************************
    *                                                                                   *
    *                           RPG Threat System v3.2c                                 *
    *                                         *****                                     *
    *                                       by: Quilnez                                 *
    *                                                                                   *
    *  This system generates special behavior to creep units. Gives some brains to        *
    *  those shrimp-head creeps. They can flee, they have artificial sight area            *
    *  (you can sneak behind them), they are able to help each other, they become        *
    *  aggressive or not aggressive as well. And many more!                             *
    *                                                                                   *
    *  External dependencies:                                                           *
    *   (required)                                                                      *
    *     - Table                                                                       *
    *     - UnitIndexer                                                                 *
    *     - GetClosestWidget                                                            *
    *     - Any DDS                                                                     *
    *   (optional)                                                                      *
    *     - TimerUtils                                                                  *
    *                                                                                   *
    *  Implementation:                                                                  *
    *     - Copy paste RPG Threat System folder into your map                            *
    *     - Give two player slots for passive and aggresive player, better just         *
    *       leave'em as empty player slot                                               *
    *     - Configure the system & start defining your creep behavior                    *
    *     - Done.                                                                       *
    *                                                                                   *
    *  Credits:                                                                         *
    *     - GDD by Weep                                                                 *
    *     - GetClosestWidget by Bannar                                                  *
    *     - UnitIndexer by Nestharus                                                    *
    *     - TimerUtils by Vexorian                                                      *
    *     - Table by Bribe                                                              *
    *                                                                                   *
    *   (You can read more info at READ ME trigger)                                     *
    *                                                                                   *
    *************************************************************************************
    *                                                                                   *
    *                                CONFIGURATION                                      */
    globals
        // Owner of passive creeps
        private constant    player      PASSIVE             = Player(10)
     
        // Owner of active creeps
        private constant    player      AGGRESSIVE          = Player(11)
     
        // If true, creeps will use PASSIVE's player color
        private constant    boolean     COLOR               = true
     
        // Behavior generation area (distance from generator units)
        private constant    real        DISTANCE            = 2400.0
     
        // Created sfx when creeps are sleeping
        private constant    string      SLEEP_SFX           = "Abilities\\Spells\\Undead\\Sleep\\SleepTarget.mdl"
        private constant    string      SLEEP_SFX_PT        = "overhead"
     
        // Order Ids
        private constant    integer     ATTACK_ORDER_ID     = 851983
        private constant    integer     MOVE_ORDER_ID       = 851986
        private constant    integer     STOP_ORDER_ID       = 851972
     
        // Total seconds in a day (can be found in gameplay constants)
        private constant    real        SECONDS_IN_A_DAY    = 480
     
        // If true, all units owned by PASSIVE and AGGRESSIVE will be
        // registered automatically on unit indexing event
        private constant    boolean     AUTO_REGISTER       = true
     
        // Just leave it alone
        private constant    real        INTERVAL            = 0.03125
     
        // Don't touch this
        private group CreepGroup = CreateGroup()
    endglobals
 
    // Damage event of your DDS
    private function addEvent takes trigger t returns nothing
        call TriggerRegisterVariableEvent(t, "udg_GDD_Event", EQUAL, 0.0)
    endfunction
 
    // Damage source of your DDS
    private constant function getSource takes nothing returns unit
        return udg_GDD_DamageSource
    endfunction
 
    // Damage target of your DDS
    private constant function getTarget takes nothing returns unit
        return udg_GDD_DamagedUnit
    endfunction
 
    // Set classifications of affected creeps
    private function creepFilter takes unit u returns boolean
 
        // For safety, just keep this format
        if IsUnitType(u, UNIT_TYPE_HERO) or IsUnitType(u, UNIT_TYPE_STRUCTURE) then
            return false
        endif
     
    /*                                END OF CONFIGURATION                              *
    *                                                                                   *
    ************************************************************************************/
 
        if IsUnitInGroup(u, CreepGroup) then
            return false
        endif
        call GroupAddUnit(CreepGroup, u)
     
        return true
    endfunction
 
    struct SCDataLib
        private static thistype Dex
        private static Table LibIndex
     
        //! textmacro CREEP_DATA_TYPE takes VAR_NAME,VAL_TYPE
        readonly $VAL_TYPE$ $VAR_NAME$Var
        static method operator $VAR_NAME$= takes $VAL_TYPE$ value returns nothing
            set Dex.$VAR_NAME$Var = value
        endmethod
     
        //! endtextmacro
        //! runtextmacro CREEP_DATA_TYPE( "maxHelp",         "integer" )
        //! runtextmacro CREEP_DATA_TYPE( "helpType",        "integer" )
        //! runtextmacro CREEP_DATA_TYPE( "resetTimer",      "boolean" )
        //! runtextmacro CREEP_DATA_TYPE( "canFlee",         "boolean" )
        //! runtextmacro CREEP_DATA_TYPE( "isTamed",         "boolean" )
        //! runtextmacro CREEP_DATA_TYPE( "seekHelp",        "boolean" )
        //! runtextmacro CREEP_DATA_TYPE( "callHelp",        "boolean" )
        //! runtextmacro CREEP_DATA_TYPE( "seekRadius",      "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "callRadius",      "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "sightRadius",     "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "sleepTime",       "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "sleepDur",        "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "lowHealth",       "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "acquisitionRange","real"    )
        //! runtextmacro CREEP_DATA_TYPE( "ambushDelay",     "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "combatDuration",  "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "wanderDelay",     "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "wanderVariant",   "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "wanderDist",      "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "fleeDist",        "real"    )
        //! runtextmacro CREEP_DATA_TYPE( "nestArea",        "real"    )
     
        static method getIndex takes integer unitId returns integer
            return LibIndex.integer[unitId]
        endmethod
     
        static method add takes integer unitId returns nothing
            if LibIndex.integer[unitId] == 0 then
                set Dex = allocate()
                set LibIndex.integer[unitId] = Dex
            endif
        endmethod
     
        static method end takes nothing returns nothing
            set Dex = 0
        endmethod
     
        static method onInit takes nothing returns nothing
            set LibIndex = Table.create()
        endmethod
    endstruct
 
    // Container for creep data
    private struct CreepData
 
        boolean isSleep
        integer helpCount
        effect  sleepSfx
     
        unit    target
        timer   sleepTimer
        real    wanderDelay
     
        real    nestX
        real    nestY
        real    combatDur
     
        real    combatDly
        real    unitX
        real    unitY
     
    endstruct
 
    globals
        private boolean Move          = false
        private group PickGroup       = CreateGroup()
        private group TempGroup       = CreateGroup()
        private integer DummyType
        private integer Total         = -1
        private integer GenTotal      = 0
        private integer array GenDex
        private real array Real
        private timer Timer           = CreateTimer()
        private trigger array Handler
        private unit TempUnit
        private unit EventUnit        = null
        private unit EventThreat      = null
        private unit array GenUnit
        private CreepData array Data
     
        // Real array indexes
        private constant integer R_X             = 0
        private constant integer R_Y             = 1
        private constant integer R_TOD           = 2
        private constant integer R_WAKE          = 3
        private constant integer R_DISTANCE      = 4
        private constant integer R_FACING        = 5
        private constant integer R_DURATION      = 6
     
        // Event constants
        constant integer EVENT_CREEP_SLEEP       = 0
        constant integer EVENT_CREEP_AWAKE       = 1
        constant integer EVENT_CREEP_WANDER      = 2
        constant integer EVENT_CREEP_FLEE        = 3
        constant integer EVENT_CREEP_THREAT      = 4
        constant integer EVENT_CREEP_UNTHREAT    = 5
        constant integer EVENT_CREEP_ATTACK      = 6
        constant integer EVENT_CREEP_CALL_HELP   = 7
        constant integer EVENT_CREEP_SEEK_HELP   = 8
        constant integer EVENT_CREEP_GIVE_HELP   = 9
        constant integer EVENT_CREEP_LOW_HEALTH  = 10
        constant integer EVENT_CREEP_DEAL_DAMAGE = 11
        constant integer EVENT_CREEP_TAKE_DAMAGE = 12
        constant integer EVENT_CREEP_UNATTACK    = 13
    endglobals
 
    native UnitAlive takes unit id returns boolean
 
    // API functions
    function SetGenUnit takes unit u, boolean b returns nothing
 
        local integer uDex = GetUnitUserData(u)
 
        if b then
            if GenDex[uDex] == 0 then
                set GenTotal = GenTotal + 1
                set GenUnit[GenTotal] = u
                set GenDex[uDex] = GenTotal
            endif
        elseif GenDex[uDex] > 0 then
            set GenDex[GetUnitUserData(GenUnit[GenTotal])] = GenDex[uDex]
            set GenUnit[GenDex[uDex]] = GenUnit[GenTotal]
            set GenUnit[GenTotal] = null
            set GenTotal = GenTotal - 1
            set GenDex[uDex] = 0
        endif
     
    endfunction
 
    function AddCreepEventHandler takes integer whichEvent, boolexpr func returns nothing
        call TriggerAddCondition(Handler[whichEvent], func)
    endfunction
 
    function GetTriggerCreep takes nothing returns unit
        return EventUnit
    endfunction
 
    function GetTriggerThreat takes nothing returns unit
        return EventThreat
    endfunction
 
    function GetCreepNestX takes unit u returns real
        return Data[GetUnitUserData(u)].nestX
    endfunction
 
    function GetCreepNestY takes unit u returns real
        return Data[GetUnitUserData(u)].nestY
    endfunction
 
    function IsCreepSleeping takes unit u returns boolean
        return Data[GetUnitUserData(u)].isSleep
    endfunction
 
    function IsCreepMoving takes unit u returns boolean
        local integer uDex = GetUnitUserData(u)
        return GetUnitCurrentOrder(u) == MOVE_ORDER_ID or GetUnitX(u) != Data[uDex].unitX or GetUnitY(u) != Data[uDex].unitY
    endfunction
 
    // Get distance between points
    private function getDistance takes real x, real y, real xt, real yt returns real
        return SquareRoot((xt-x)*(xt-x)+(yt-y)*(yt-y))
    endfunction
 
    // Get angle between points
    private function getAngle takes real x, real y, real xt, real yt returns real
        return Atan2(yt-y, xt-x) * bj_RADTODEG
    endfunction
 
    // Get circular difference between two angles
    private function circularDifference takes real a, real b returns real
 
        local real r = RAbsBJ(a-b)
     
        if r <= 180 then
            return r
        else
            return 180 - r
        endif
     
    endfunction
 
    // Filter enemy units around creep
    private function targetFilter takes nothing returns boolean
 
        local unit u       = GetFilterUnit()
        local player p     = GetOwningPlayer(u)
        local SCDataLib lDex = SCDataLib.getIndex(GetUnitTypeId(TempUnit))
        local real a       = getAngle(GetUnitX(TempUnit), GetUnitY(TempUnit), GetUnitX(u), GetUnitY(u))
        local boolean b    = circularDifference(GetUnitFacing(TempUnit), a) < lDex.sightRadiusVar/4
     
        set b = b and (p != PASSIVE and p != AGGRESSIVE and UnitAlive(u))
        set b = b and (IsUnitVisible(u, PASSIVE) or IsUnitVisible(u, AGGRESSIVE))
        set u = null
     
        return b
    endfunction
 
    // Filter possible helper for creep
    private function helperFilter takes nothing returns boolean
 
        local unit    u    = GetFilterUnit()
        local integer id   = GetUnitTypeId(u)
        local integer uDex = GetUnitUserData(u)
        local SCDataLib lDex = SCDataLib.getIndex(id)
        local boolean b    = (lDex.helpTypeVar != 1 or id == DummyType) and lDex.helpTypeVar >= 1
     
        set b = b and (GetOwningPlayer(u) == PASSIVE and not lDex.isTamedVar)
        set b = b and not IsCreepSleeping(u)
        set u = null
     
        return b
    endfunction
 
    private function fireEvent takes integer whichEvent, unit u, unit t returns nothing
     
        local unit tu   = EventUnit
        local unit tt   = EventThreat
     
        set EventUnit   = u
        set EventThreat = t
     
        call TriggerEvaluate(Handler[whichEvent])
     
        set EventUnit   = tu
        set EventThreat = tt
     
        set tu = null
        set tt = null
     
    endfunction
 
    // Seek for nearby helpers
    private function pickHelper takes unit u, unit t, integer uDex, SCDataLib lDex returns nothing
     
        local integer i = 0
        local SCDataLib lDex2
        local integer uDex2
        local unit h
     
        loop
            exitwhen Data[uDex].helpCount == lDex.maxHelpVar
            set h = GetClosestUnitInRange(Data[uDex].unitX, Data[uDex].unitY, lDex.callRadiusVar, Filter(function helperFilter))
            exitwhen h == null
         
            call SetUnitOwner(h, AGGRESSIVE, not COLOR)
            set uDex2 = GetUnitUserData(h)
            set lDex2 = SCDataLib.getIndex(GetUnitTypeId(h))
         
            set Data[uDex2].combatDur = lDex2.combatDurationVar
            set Data[uDex2].target    = t
         
            call IssueTargetOrderById(h, ATTACK_ORDER_ID, t)
            call fireEvent(EVENT_CREEP_GIVE_HELP, h, null)
            set Data[uDex].helpCount = Data[uDex].helpCount + 1
        endloop
        set h = null
     
    endfunction
 
    // Filter creeps around generators
    private function pickFilter takes nothing returns boolean
     
        local unit u = GetFilterUnit()
     
        if not IsUnitInGroup(u, PickGroup) and IsUnitInGroup(u, CreepGroup) then
            call GroupAddUnit(PickGroup, u)
        endif
        set u = null
     
        return false
    endfunction
 
    private function onLoop takes nothing returns nothing
 
        local unit u
        local unit t
        local SCDataLib lDex
        local integer uDex
        local integer i = 1
     
        // Collect creeps around generator units' positions
        loop
            exitwhen i > GenTotal
            call GroupEnumUnitsInRange(TempGroup, GetUnitX(GenUnit[i]), GetUnitY(GenUnit[i]), DISTANCE, function pickFilter)
            set i = i + 1
        endloop
     
        loop
            set u = FirstOfGroup(PickGroup)
            exitwhen u == null
            call GroupRemoveUnit(PickGroup, u)
         
            set uDex = GetUnitUserData(u)
            if GetUnitTypeId(u) != 0 then
                if UnitAlive(u) then
                    set lDex = SCDataLib.getIndex(GetUnitTypeId(u))
                    // Determine whether the creep is moving atm or not
                    set Move = IsCreepMoving(u)
                    if Move then
                        set Data[uDex].unitX = GetUnitX(u)
                        set Data[uDex].unitY = GetUnitY(u)
                    endif
                 
                    if GetOwningPlayer(u) == PASSIVE then
                        if Data[uDex].combatDly < 0 then
                            // If the creep is able to sleep
                            if lDex.sleepDurVar > 0 then
                                // Gather sleep time datas
                                set Real[R_TOD]  = GetFloatGameState(GAME_STATE_TIME_OF_DAY)
                                set Real[R_WAKE] = lDex.sleepTimeVar + lDex.sleepDurVar
                                if  Real[R_TOD] <= Real[R_WAKE] - 24 then
                                    set Real[R_TOD] = Real[R_TOD] + 24
                                endif
                             
                                // Check sleep time
                                if Real[R_TOD] >= lDex.sleepTimeVar and Real[R_TOD] < Real[R_WAKE] and not Data[uDex].isSleep then
                                    if not Move and not(IsUnitType(u, UNIT_TYPE_STUNNED) or IsUnitPaused(u)) then
                                        set Data[uDex].isSleep  = true
                                        set Data[uDex].sleepSfx = AddSpecialEffectTarget(SLEEP_SFX, u, SLEEP_SFX_PT)
                                        if Data[uDex].sleepTimer == null then
                                            static if LIBRARY_TimerUtils then
                                                set Data[uDex].sleepTimer = NewTimer()
                                            else
                                                set Data[uDex].sleepTimer = CreateTimer()
                                            endif
                                            // Calculate sleep duration
                                            set Real[R_DURATION] = (SECONDS_IN_A_DAY/86400)*((lDex.sleepDurVar-(Real[R_TOD]-lDex.sleepTimeVar))*3600)/GetTimeOfDayScale()
                                            call TimerStart(Data[uDex].sleepTimer, Real[R_DURATION], false, null)
                                        endif
                                        call fireEvent(EVENT_CREEP_SLEEP, u, null)
                                    endif
                                endif
                            endif
                         
                            if Data[uDex].isSleep then
                                // Wake up time
                                if TimerGetRemaining(Data[uDex].sleepTimer) <= 0 then
                                    call DestroyEffect(Data[uDex].sleepSfx)
                                    static if LIBRARY_TimerUtils then
                                        call ReleaseTimer(Data[uDex].sleepTimer)
                                    else
                                        call DestroyTimer(Data[uDex].sleepTimer)
                                    endif
                                 
                                    set Data[uDex].isSleep    = false
                                    set Data[uDex].sleepTimer = null
                                    set Data[uDex].sleepSfx   = null
                                 
                                    call fireEvent(EVENT_CREEP_AWAKE, u, null)
                                endif
                            else
                                // Check if wandering or not
                                if lDex.wanderDelayVar > 0 then
                                    if Data[uDex].wanderDelay > INTERVAL then
                                        set Data[uDex].wanderDelay = Data[uDex].wanderDelay - INTERVAL
                                    else
                                        set Real[R_DISTANCE] = GetRandomReal(lDex.wanderDistVar, lDex.nestAreaVar)
                                        set Real[R_FACING]   = GetRandomReal(0,bj_PI*2)
                                     
                                        // Reset wander delay
                                        set Data[uDex].wanderDelay = lDex.wanderDelayVar + GetRandomReal(-lDex.wanderVariantVar, lDex.wanderVariantVar)/2
                                     
                                        set Real[R_X] = Data[uDex].nestX + Real[R_DISTANCE] * Cos(Real[R_FACING])
                                        set Real[R_Y] = Data[uDex].nestY + Real[R_DISTANCE] * Sin(Real[R_FACING])
                                     
                                        call IssuePointOrderById(u, MOVE_ORDER_ID, Real[R_X], Real[R_Y])
                                        call fireEvent(EVENT_CREEP_WANDER, u, null)
                                    endif
                                endif
                             
                                // If the creep is aggressive
                                if lDex.acquisitionRangeVar > 0 then
                                    // If is around it's nest area
                                    if getDistance(Data[uDex].unitX, Data[uDex].unitY, Data[uDex].nestX, Data[uDex].nestY) <= lDex.nestAreaVar then
                                        set TempUnit = u
                                        set t = GetClosestUnitInRange(Data[uDex].unitX, Data[uDex].unitY, lDex.acquisitionRangeVar, Filter(function targetFilter))
                                        if t != null then
                                            set Data[uDex].target    = t
                                            set Data[uDex].combatDly = lDex.ambushDelayVar
                                            call IssueImmediateOrderById(u, STOP_ORDER_ID)
                                            call fireEvent(EVENT_CREEP_THREAT, u, t)
                                            set t = null
                                        endif
                                    endif
                                endif
                            endif
                        else
                            set Real[R_X] = GetUnitX(Data[uDex].target)
                            set Real[R_Y] = GetUnitY(Data[uDex].target)
                            call SetUnitFacing(u, getAngle(Data[uDex].unitX, Data[uDex].unitY, Real[R_X], Real[R_Y]))
                         
                            // If target has fleed
                            if getDistance(Data[uDex].unitX, Data[uDex].unitY, Real[R_X], Real[R_Y]) > lDex.acquisitionRangeVar then
                                set Data[uDex].combatDly = -1
                                set Data[uDex].target    = null
                                call fireEvent(EVENT_CREEP_UNTHREAT, u, Data[uDex].target)
                            else
                                if Data[uDex].combatDly > INTERVAL then
                                    set Data[uDex].combatDly = Data[uDex].combatDly - INTERVAL
                                else
                                    set Data[uDex].combatDur = lDex.combatDurationVar
                                    call SetUnitOwner(u, AGGRESSIVE, not COLOR)
                                    call IssueTargetOrderById(u, ATTACK_ORDER_ID, Data[uDex].target)
                                    call fireEvent(EVENT_CREEP_ATTACK, u, Data[uDex].target)
                                endif
                            endif
                        endif
                    else
                        // Calm down if the combat timer is up or if the target has died
                        if Data[uDex].combatDur > INTERVAL and UnitAlive(Data[uDex].target) then
                            set Data[uDex].combatDur = Data[uDex].combatDur - INTERVAL
                        else
                            call SetUnitOwner(u, PASSIVE, not COLOR)
                            set Data[uDex].helpCount = 0
                         
                            // Order to move away
                            set Real[R_DISTANCE] = GetRandomReal(0, lDex.nestAreaVar)
                            set Real[R_FACING]   = GetRandomReal(0, bj_PI*2)
                         
                            set Real[R_X] = Data[uDex].nestX + Real[R_DISTANCE] * Cos(Real[R_FACING])
                            set Real[R_Y] = Data[uDex].nestY + Real[R_DISTANCE] * Sin(Real[R_FACING])
                         
                            call IssuePointOrderById(u, MOVE_ORDER_ID, Real[R_X], Real[R_Y])
                            call fireEvent(EVENT_CREEP_UNATTACK, u, Data[uDex].target)
                            set Data[uDex].target = null
                        endif
                    endif
                endif
            else
                call Data[uDex].destroy()
                set  Data[uDex].sleepTimer = null
                set  Data[uDex].sleepSfx   = null
                set  Data[uDex].target     = null
     
                set Total = Total - 1
                if Total < 0 then
                    call PauseTimer(Timer)
                endif
            endif
        endloop
     
    endfunction

    private function onHit takes nothing returns boolean
 
        local SCDataLib lDex
        local integer uDex
        local integer i
        local integer ix
        local unit t = getTarget()
        local unit s = getSource()
        local unit h
     
        if GetOwningPlayer(s) == AGGRESSIVE and IsUnitInGroup(s, CreepGroup) then
            set DummyType = GetUnitTypeId(s)
            set uDex = GetUnitUserData(s)
            set lDex = SCDataLib.getIndex(DummyType)
            call fireEvent(EVENT_CREEP_DEAL_DAMAGE, s, t)
         
            if lDex.resetTimerVar then
                if getDistance(Data[uDex].unitX, Data[uDex].unitY, Data[uDex].nestX, Data[uDex].nestY) <= lDex.nestAreaVar then
                    set Data[uDex].combatDur = lDex.combatDurationVar
                endif
            endif
         
            // If call for help when attacking
            if lDex.maxHelpVar > 0 and lDex.callHelpVar then
                call pickHelper(s, t, uDex, lDex)
                call fireEvent(EVENT_CREEP_CALL_HELP, s, null)
            endif
         
        elseif IsUnitInGroup(t, CreepGroup) then
            set DummyType = GetUnitTypeId(t)
            set uDex = GetUnitUserData(t)
            set lDex = SCDataLib.getIndex(DummyType)
            set Data[uDex].combatDur = lDex.combatDurationVar
            call fireEvent(EVENT_CREEP_TAKE_DAMAGE, t, s)
         
            // If the creep is currently sleeping
            if Data[uDex].isSleep then
                call DestroyEffect(Data[uDex].sleepSfx)
                set Data[uDex].sleepSfx = null
                set Data[uDex].isSleep  = false
                call fireEvent(EVENT_CREEP_AWAKE, t, null)
            endif
         
            if lDex.canFleeVar and lDex.fleeDistVar > 0 then
                // If still around it's nest area
                if getDistance(Data[uDex].unitX, Data[uDex].unitY, Data[uDex].nestX, Data[uDex].nestY) < lDex.nestAreaVar then
                    set Real[R_FACING] = getAngle(GetUnitX(s), GetUnitY(s), Data[uDex].unitX, Data[uDex].unitY) * bj_DEGTORAD
                    set Real[R_X] = Data[uDex].unitX + lDex.fleeDistVar * Cos(Real[R_FACING])
                    set Real[R_Y] = Data[uDex].unitY + lDex.fleeDistVar * Sin(Real[R_FACING])
                else
                    set Real[R_FACING] = GetRandomReal(0, bj_PI*2)
                    set Real[R_X] = Data[uDex].nestX + lDex.fleeDistVar * Cos(Real[R_FACING])
                    set Real[R_Y] = Data[uDex].nestY + lDex.fleeDistVar * Sin(Real[R_FACING])
                endif
                call IssuePointOrderById(t, MOVE_ORDER_ID, Real[R_X], Real[R_Y])
                call fireEvent(EVENT_CREEP_FLEE, t, null)
            endif
         
            if GetOwningPlayer(t) == PASSIVE then
                set Data[uDex].target = s
                // If not tamed
                if not lDex.isTamedVar then
                    call SetUnitOwner(t, AGGRESSIVE, not COLOR)
                    call IssueTargetOrderById(t, ATTACK_ORDER_ID, s)
                endif
             
                if lDex.maxHelpVar > 0 then
                    call pickHelper(t, s, uDex, lDex)
                    call fireEvent(EVENT_CREEP_CALL_HELP, t, s)
                endif
            endif
         
            if GetWidgetLife(t) <= lDex.lowHealthVar then
                call fireEvent(EVENT_CREEP_LOW_HEALTH, t, null)
                // If able to seek help
                if lDex.seekHelpVar and lDex.maxHelpVar > 0 then
                    set h = GetClosestUnitInRange(Data[uDex].unitX,Data[uDex].unitY,lDex.seekRadiusVar,Filter(function helperFilter))
                    // Order to move to helper's position
                    if h != null then
                        call IssuePointOrderById(t, MOVE_ORDER_ID, GetUnitX(h), GetUnitY(h))
                        set h = null
                    endif
                    call fireEvent(EVENT_CREEP_SEEK_HELP, t, null)
                endif
            endif
        endif
        set t = null
        set s = null
     
        return false
    endfunction
 
    private function onDeath takes nothing returns boolean
     
        local integer uDex
        local SCDataLib lDex
        local player  p = GetTriggerPlayer()
        local unit u
     
        if p == PASSIVE or p == AGGRESSIVE then
            set u    = GetTriggerUnit()
            set uDex = GetUnitUserData(u)
            set lDex = SCDataLib.getIndex(GetUnitTypeId(u))
         
            call SetUnitOwner(u, PASSIVE, not COLOR)
            set Data[uDex].wanderDelay = lDex.wanderDelayVar + GetRandomReal(-lDex.wanderVariantVar, lDex.wanderVariantVar)/2
            set Data[uDex].combatDur   = -1
         
            set Data[uDex].combatDly   = -1
            set Data[uDex].helpCount   = 0
            set Data[uDex].isSleep     = false
         
            set u = null
        endif
     
        return false
    endfunction

    function EnableCreepBehavior takes unit u, boolean b returns nothing
 
        local SCDataLib lDex
        local integer uDex
        local player p = GetOwningPlayer(u)
     
        if b then
            if p == PASSIVE or p == AGGRESSIVE then
                if p == AGGRESSIVE then
                    call SetUnitOwner(u, PASSIVE, false)
                    static if COLOR then
                        call SetUnitColor(u, GetPlayerColor(PASSIVE))
                    endif
                else
                    static if not COLOR then
                        call SetUnitColor(u, GetPlayerColor(AGGRESSIVE))
                    endif
                endif
             
                if creepFilter(u) then
                    set Total = Total + 1
                    set uDex  = GetUnitUserData(u)
                    set lDex  = SCDataLib.getIndex(GetUnitTypeId(u))
                 
                    set Data[uDex] = CreepData.create()
                    set Data[uDex].helpCount = 0
                    set Data[uDex].isSleep   = false
                 
                    set Data[uDex].wanderDelay = GetRandomReal(0, lDex.wanderDelayVar)
                    set Data[uDex].nestX = GetUnitX(u)
                    set Data[uDex].nestY = GetUnitY(u)
                 
                    set Data[uDex].combatDly = -1
                    set Data[uDex].unitX = 0
                    set Data[uDex].unitY = 0
                 
                    if Total == 0 then
                        call TimerStart(Timer, INTERVAL, true, function onLoop)
                    endif
                endif
            endif
        elseif IsUnitInGroup(u, CreepGroup) then
            call GroupRemoveUnit(CreepGroup, u)
        endif
     
    endfunction

    private function onIndex takes nothing returns boolean
        call EnableCreepBehavior(GetIndexedUnit(), true)
        return false
    endfunction

    private function setAlliance takes nothing returns nothing
 
        local player p = GetEnumPlayer()
     
        if p != PASSIVE and p != AGGRESSIVE then
            // Aggressive player threats others as enemies
            call SetPlayerAlliance(AGGRESSIVE, p, ALLIANCE_PASSIVE, false)
            // Passive player threats other players as allies
            call SetPlayerAlliance(PASSIVE, p, ALLIANCE_PASSIVE, true)
        endif
     
    endfunction

    private function onInit takes nothing returns nothing
 
        local player  p  = GetLocalPlayer()
        local trigger t1 = CreateTrigger()
        local trigger t2 = CreateTrigger()
     
        static if AUTO_REGISTER then
            call RegisterUnitIndexEvent(Condition(function onIndex), UnitIndexer.INDEX)
        endif
     
        call addEvent(t1)
        call TriggerAddCondition(t1, Condition(function onHit))
     
        call TriggerRegisterAnyUnitEventBJ(t2, EVENT_PLAYER_UNIT_DEATH)
        call TriggerAddCondition(t2, Condition(function onDeath))
     
        // Alliance setting
        call SetPlayerAlliance(PASSIVE, AGGRESSIVE, ALLIANCE_PASSIVE, true)
        call SetPlayerAlliance(AGGRESSIVE, PASSIVE, ALLIANCE_PASSIVE, true)
        call ForForce(bj_FORCE_ALL_PLAYERS, function setAlliance)
     
        // Give global sight for both player
        if p == AGGRESSIVE or p == PASSIVE then
            call FogEnable(false)
            call FogMaskEnable(false)
        endif
     
        set t1 = null
        set t2 = null
     
    endfunction
 
endlibrary

Credits
  • TimerUtils by Vexorian
  • GetClosestWidget by Spinnaker
  • UnitIndexer by Nestharus
  • GDD by Weep
  • Table by Bribe

Keywords:
rpg, creep, monster, behavior
Contents

RPG Threat System (Map)

Reviews
IcemanBo: - date: 3th March 2015 - submission: Special Creep Engine v3.2b The system allows to define unique unit-type behaviours for creeps to improve the enemy's AI creep controle. All in all the code is efficient, leakless and works fine...
Long-awaited review:
  • In filter2:
    not((a2 <= a1 - rad or a2 >= a1 + rad) and (a2 <= a1 - rad - 360. or a2 >= a1 + rad - 360.))
    These statements become a tad confusing as you have more "not"s and parenthesis. Let's cut it down a bit.

    not((A or B) and (C or D))
    (not (A or B)) or (not (C or D))
    (not A and not B) or (not C and not D)
    (see De Morgan's Laws)

    Then you just evaluate the nots in this case, and you'll end up with:
    (a2 > a1 - rad and a2 < a1 + rad) or (a2 > a1 - rad - 360 and a2 < a1 + rad - 360)
    (if my logic was correct)
    It might be convenient to write a function for that, but it is up to you.
  • Using similar logic reduction, this:
    set b = not((p == PASSIVE or p == AGGRESSIVE) or not UnitAlive(u))
    Can become:
    set b = (p != PASSIVE and p != AGGRESSIVE) and UnitAlive(u)
  • Likewise:
    set b = not(not IsUnitVisible(u, PASSIVE) and not IsUnitVisible(u, AGGRESSIVE))
    Can become:
    set b = (IsUnitVisible(u, PASSIVE) or IsUnitVisible(u, AGGRESSIVE))
    Useful, huh? But take caution, it is easy to make mistakes. Hell, I might've even made a mistake. So keep a copy of your original code to compare with.
  • If I didn't make any mistakes, this should be an equivalent function to "functionHelper". I won't explain these ones since it takes time, but try it out for yourself and see if you get the same reduction!
    JASS:
        private function filterHelper takes nothing returns boolean
        
            local unit u = GetFilterUnit()
            local integer ut = GetUnitTypeId(u)
            local integer dex = LoadInteger(Hashtable, 1911, ut)
            local integer ht = LoadInteger(Hashtable, 7550, dex)
            local integer hand = GetHandleId(u)
            local boolean b = true
            
            set b = (ht != 1 or ut == DummyType) and ht >= 1
            if b then
                set b = (GetOwningPlayer(u) == PASSIVE and not LoadBoolean(Hashtable, 2343, dex))
            endif
            if b then
                set b = not LoadBoolean(Hashtable, hand, 7) or LoadEffectHandle(Hashtable, hand, 8) == null
            endif
            set u = null
            
            return b
        endfunction
  • This doesn't make much sense to me:
    JASS:
            loop
                exitwhen i > TotalCharacter
                call GroupEnumUnitsInRange(PickGroup, GetUnitX(MainCharacter[i]), GetUnitY(MainCharacter[i]), DISTANCE, function pickFilter)
                set i = i + 1
            endloop

    GroupEnumUnitsInRange actually clears units in the group each time you enumerate. So that loop is equivalent to:
    call GroupEnumUnitsInRange(PickGroup, GetUnitX(MainCharacter[TotalCharacter], GetUnitY(MainCharacter[TotalCharacter]), DISTANCE, function pickFilter)

    If I'm not mistaken. Although, I don't know if that is the intended result...
  • (Optional) It is difficult to read through the onLoop because of the Real[] array. Personally, I prefer locals, even if they bloat the code–they have readability since you can change the names. But that is up to you.
  • Make a subroutine for firing the event. Since you repeat this a lot:
    JASS:
    set EventUnit = u
    set EventTrigger = EVENT_CREEP_SLEEP
    set EventTrigger = -1
    set EventUnit = null
    Make a function that does it for you!
    JASS:
    private function FireEvent takes unit u, integer ev returns nothing
        local unit temp = EventUnit
        //.. add other event params, e.g. distance, etc.
        set EventUnit = u
        set EventTrigger = ev
        set EventTrigger = -1
        set EventUnit = temp
        set temp = null
    endfunction
    Add all the parameters that you might need to edit, and all the variables. If you do this, it will cut down on A LOT of code. That is the beauty of subroutines.

    Also, I noticed you were setting the variables, and then setting them back to null or 0. That isn't the proper way to implement a recursive event. You have to use a temporary variable, because it isn't guaranteed that the former value is 0 or null. ;)
  • This:
    set Real[3] = GetRandomReal(0., 359.9) * bj_DEGTORAD
    ->
    set Real[3] = GetRandomReal(0., 2*bj_PI)
  • This:
    JASS:
    if COLOR then
        call SetUnitOwner(u, AGGRESSIVE, false)
    else
        call SetUnitOwner(u, AGGRESSIVE, true)
    endif
    Can be:
    JASS:
    call SetUnitOwner(u, AGGRESSIVE, not COLOR)
    (same for the other one)
  • About this:
    JASS:
    // Evade critical bug (everlasting combat duration where it's not supposed to)
    if Real[0] == 0. then
        set Real[0] = -1.
    endif
    This won't really be needed if you change the exit condition appropriately:
    JASS:
    if Real[0] <= 0. or not UnitAlive(LoadUnitHandle(Hashtable, hand, 6)) then
    (I changed Real[0] < 0. to Real[0] <= 0.)
  • JASS:
    loop
        // Continue the loop ala total help hasn't reach the max
        if ht < ix then
        // ... a bunch of code
        endif
    endloop
    You have two of those. They look exactly the same to me, so you should put it in a separate function. It'll cut down on a lot of code as well. DRY principle.
  • In "setAlliance":
    JASS:
    if p != PASSIVE then
        if p != AGGRESSIVE then
            // ... code
        endif
    endif
    That is equivalent to: if p != PASSIVE and p != AGGRESSIVE then
  • You have a few leaks in "ini":
    GetWorldBounds()
    That actually generates a new rect each time. Set a global to WorldBounds(), and then use it over and over. Or you can start a timer to fire after 0 seconds, and use bj_mapInitialPlayableArea instead (if that works for your fog modifiers).
  • Why public?
    public function initPick takes nothing returns nothing
  • (Optional) The code is difficult to read with the random keys. I recommend using global constants for the keys so that the code will be comprehensible.

Overall, this system performs the job it needs to do. The main things that need improvement: repeated code, reduction, readability, and comprehensibility. But don't be worried! Your coding is constantly getting better. And I know it is a lot to read/fix, but hey, the code was a lot to review in itself! :D
 
Some people have been requesting well-defined reviews. I'll try my best to accommodate
for that. The format, calibration, and scaling is still WIP. But overall I'm pretty happy
with it. Note: the scores are not final, so don't freak out! A lot of the deductions
are correctable. PM me or VM me for any questions, comments, or responses.

General

Coding

Concept

Design

Misc

Docs

Required Changes

Purgeandfire :: 29 October 2014

Criteria (Systems):
  • Coding (20): efficiency and potential improvement.
  • Concept (10): originality and features.
  • Design (20): modularity, importability, ease of use, and approach.
  • Misc (10): test map, image, hive submission, misc.
  • Docs (10): documentation

Score:
  • Coding: 10 / 20
  • Concept: 7 / 10
  • Design: 14 / 20
  • Misc: 8 / 10
  • Docs: 5 / 10
  • Total Score: 44 / 70

Needs Fix :: see Required Changes for approval.

Current Rating
3 / 5
  • 65 - 70 becomes 5 / 5
  • 55 - 65 becomes 4 / 5
  • 40 - 55 becomes 3 / 5
  • 30 - 40 becomes 2 / 5
  • 0 - 30 becmes 1 / 5




  1. GetWorldBounds() leaks. You need to store it in a variable and then
    remove it manually (2 occurrences). [-3]
  2. Just a note, adding [13] does not do anything for the following line:
    private trigger array Handler[13]
    It does not change anything unless you use a size > 8192.
  3. filter2 should have a better name. [-1]
  4. In pickHelper, if I'm not mistaken, it seems like you are trying to
    choose the closest units and order them to help. In this case, it may
    be better to take advantage of Spinnaker's system and use:
    GetClosestNUnitsInRange
    It is designed to do what you need to do, and it will result in a
    significantly lighter computation. Note: You will have to change your
    entire loop to adapt to this change. [-2]
  5. This:
    set Real[R_TEMP_FACE] = GetRandomReal(0, 359.9) * bj_DEGTORAD
    Is equivalent to:
    sett Real[R_TEMP_FACE] = GetRandomReal(0, 2*bj_PI)
    Or you can use 6.28319 instead of 2*bj_PI.
  6. In onHit, you can move the assignment of 'dex' outside the blocks.
    (saves 1 line)
  7. In onDeath, you need to set u to null. [-1]
  8. If you really wanted to be extreme, you could redo this entire
    system using structs instead of hashtables. >:) [-2]
  9. Why use a trigger action for initUnit? (use a condition) [-1]

Score: 10 / 20

  1. Cool concept! I love the diverse features! It really shows effort.
  2. Creep engines are quite common though.
  3. It could be cool to have creep-specific modularity, e.g. creep
    specific sleep durations, so forth.
Score: 8 / 10


  1. The module design is not very good. Generally, the best design is where
    a user does not have to touch the system at all, and only needs to know
    its functions. Generally, it is better to separate example code from the
    library itself (usually in a different trigger).
  2. A second design flaw is that the data is all expected upon initialization.
  3. The setup functions have too generic names, e.g. ResetTimer or LowBound
  4. To fix issues 1-3, I recommend translating your setup over to a struct
    and adding a .register() method so the user can invoke registering a creep.
    JASS:
            struct SpecialCreep extends array
                private static group   cg  = CreateGroup()
                private static integer uid = 0
    
                method operator tamed= takes boolean tame returns nothing
                    call SaveBoolean(Hashtable, P_TAMED_BOOL, this, tame)
                endmethod
    
                method operator sleepDuration= takes real duration returns nothing
                    call SaveReal(Hashtable, P_SLEEP_DUR, this, duration)
                endmethod
    
                // etc...
    
                private static method registerCreeps takes nothing returns boolean
                    local unit f = GetFilterUnit()
                    if GetUnitTypeId(f) == thistype.uid then
                        // ... actions to register creeps ... 
    
                        // uid -> rawcode of creep being registered
                        // f -> creep being registered
                    endif  
                    set f = null 
                    return false
                endmethod
    
                method update takes nothing returns nothing
                    local rect r = GetWorldBounds()
                    set uid = this
                    call GroupEnumUnitsInRect(cg, r, Filter(function thistype.registerCreeps))
                    call RemoveRect(r)
                    set r = null
                endmethod
    
                static method get takes integer raw returns thistype
                    return raw
                endmethod
            endstruct
    This may be a little advanced. In this situation, we are treating
    the raw ID as the struct itself. With this, we do not even need to
    map the rawcodes to integers 1, 2, 3... etc. We can just use the
    rawcode for everything! In case this was confusing, maybe this will
    help:
    JASS:
                local SpecialCreep murloc = SpecialCreep.get('nmrl')
                set murloc.tamed = false
                set murloc.sleepDuration = 7.25
                set murloc.sleepTime = 21.0 
                // etc. 
                call murloc.update()
    Note: my code above would change a lot of the system. I recommend
    making these changes only if you understand the code 100%. But the
    advantage is it would allow you to update unit stats at any time by
    using .update()! That is awesome modularity.
  5. When you order a unit to patrol (via the MOVE order), won't it ignore
    enemy targets?
Score: 14 / 20

  1. The test map should have instructions on what to do, or some
    form of debriefing. It can either be in a message, a quest, etc. [-2]
Score: 8 / 10


  1. Needs documentation. It is preferrable to have it at the top of
    the code, and have a list of the functions that should be used.
    Ideally, a user should not have to look through your system at all
    in order to use your system.

    Add some descriptions to functions and such. See the JASS section
    for examples. It will make your code a lot more user-friendly. [-5]
Score: 5 / 10

  • Coding: #1, #4 (or explain if I read your code wrong), #7, #9
  • Design: #5 (need a response)
  • Misc: #1
 

Deleted member 219079

D

Deleted member 219079

"When you order a unit to patrol (via the MOVE order), won't it ignore
enemy targets?" Yeah it will, that might have been intentional too?

That effort and amount of bbcode for a review though :D

But please don't stress yourself, I would feel quilty for it :/

Edit:
If you really wanted to be extreme, you could redo this entire
system using structs instead of hashtables. >:) [-2]

I totally agree with this, if you try to avoid the 8190 instance limit of structs, how could there be so many creeps :p

Also this is a nice system overall, +rep.
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
I totally agree with this, if you try to avoid the 8190 instance limit of structs, how could there be so many creeps :p
Yup. I'm working on it. Cz I want to be extreme >:D

Anyway, well-defined doesn't mean your review should be heavily BB-coded. Just give a clean explanation about every point and I think it's good enough.

"When you order a unit to patrol (via the MOVE order), won't it ignore
enemy targets?" Yeah it will, that might have been intentional too?
I don't clearly understand that part. Do you mean ordering player units or creep units?
 
Lines like this:
JASS:
call IssuePointOrderById(u, MOVE_ORDER_ID, tx, ty)

For wandering creeps. If creeps are ordered to move (for wandering), won't they ignore targets approaching?

Anyway, well-defined doesn't mean your review should be heavily BB-coded

Yep, it doesn't have to be. But I think it leads to a better organization than one massive review. This way, it is a lot less overwhelming. And it doesn't take very long to make once you have a base template. I just write python scripts to automate the process, paste my feedback in, and the script takes care of the formatting.
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Updated. May I jump to v3.0? :3
Well, the sleep thingy was so nasty to handle. But finally it seems to work correctly now.

Okay, now it's time to answer your reviews :p

Lines like this:
JASS:
call IssuePointOrderById(u, MOVE_ORDER_ID, tx, ty)

For wandering creeps. If creeps are ordered to move (for wandering), won't they ignore targets approaching?
No, it's still acquiring targets even while wandering.

In pickHelper, if I'm not mistaken, it seems like you are trying to
choose the closest units and order them to help. In this case, it may
be better to take advantage of Spinnaker's system and use
I don't know how to use it, so I will just stay with my way.

Creep engines are quite common though.
But I haven't seen any :/ If you meant AI system, yes, there are plenty of them

Needs documentation. It is preferrable to have it at the top of
the code, and have a list of the functions that should be used.
Ideally, a user should not have to look through your system at all
in order to use your system.

Add some descriptions to functions and such. See the JASS section
for examples. It will make your code a lot more user-friendly.
Don't worry, there is README thingy there
 
Last edited:
onHit?:
JASS:
// If the creep is currently sleeping
if Data[uDex].isSleep then
    call DestroyEffect(Data[uDex].sleepSfx)
    set Data[uDex].sleepSfx = null
    set Data[uDex].isSleep  = false
    call fireEvent(EVENT_CREEP_AWAKE, t, null)
endif
The sleep timer needs to be destroyed/released, if the creep is currently sleeping onHit.


No need to null h in function pickhelper.

JASS:
// If true, creeps will use PASSIVE's player color
private constant    boolean     COLOR               = true
^That's a bad name for a boolean.

//"sleepDur" "real" Sleep duration (in hour)
Making it possible to depent it on real time would probably be useful, too.

//"nestArea" "real" Nest area AoE
Should have better explaination.
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
The sleep timer also needs to be destroyed/released, if the creep is currently sleeping onHit.
no
^Where does TempUnit come from? :eek: ... I know it's set before you call the function, but why don't you use a local and take it as argument?
Global boolean Move is not needed. It's used to make such things with locals.
Global integer DumyType is not needed. It's used to make such things with locals.
I use global because the function can't take arguments/parameters.

I don't care about the other ones.

And please, don't touch my stuffs. You have no idea how hard I worked on them. Don't ruin it.
 
Level 1
Joined
May 1, 2015
Messages
2
Do i need to change something after pasting the triggers in my map? aside from changing the units in the Demolib.
 
Level 17
Joined
Dec 11, 2014
Messages
2,004
Whatever I do the creeps don't start wandering (Yes I've registered them and set the method calls correctly). Also after I attack the creeps, and I run/die/whatever, the creeps don't change from AGGRESSIVE to PASSIVE (they do change from PASSIVE to AGGRESSIVE).
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Hey, pretty cool system! I want to use this in my project, but I'm using AIDS in it. Is there a way to use this system with a different unit indexer?
Of course, all needed to change is this line:
JASS:
call RegisterUnitIndexEvent(Condition(function onIndex), UnitIndexer.INDEX)
And some parts of onIndex function. If you can link me to your indexer code I can help you to adjust the system.
 
Top