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

StrikeForce v1.6

  • Like
Reactions: MatiasPalacios
StrikeForce v1.6
- This is a sequel of my EnageSystem
- Documentation is inside the code

Features

- Creates an attack group (for AI)
- Waits for lost units (optional) and depends on how much time it waits
- Returns to base when no more targets around
- Attacks closest target when engaging on the main target (optional, StrikeForceEx only)
- The hero and its army will not engage if attack size is not complete (StrikeForce only)
- The army will not engage before the attack delay (StrikeForceEx only)
- If hero is dead, all units will regroup at base
- Retreat to base when life is less is optional
Note

- It is recommended to set your AI to a computer player so that they can cast spells independently
- Never Run melee AI scripts nor Start a custom Melee/Capaign AI scripts coz that will conflict with your hero AI
- Read the IsUnitEngageable library very carefully, else this will not work
JASS:
library StrikeForce /* v1.6
*************************************************************************************
*
*   by mckill2009  
*        Used by AI or user player to group units for attacking purposes
*          
*
*************************************************************************************
*
*   */uses/*
*       
*       */ Table            /* www.hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/  
*       */ GetClosestWidget /* www.hiveworkshop.com/forums/jass-resources-412/snippet-getclosestwidget-204217/
*       */ IsUnitEngageable /* www.hiveworkshop.com/forums/jass-resources-412/isunitengageable-220436/
*       */ IsUnitChanneling /* www.hiveworkshop.com/forums/jass-resources-412/snippet-isunitchanneling-211254/
*
************************************************************************************
*   
*   Installation:
*       - Copy and Paste the above mentioned libraries and the StrikeForce trigger to your map
*       - Follow and read carefully all the instructions in the 'IsUnitEngageable' library
*       - Follow the Demo instructions
*
*************************************************************************************
*
*   Credits:
*       Table by Bribe
*       GetClosestWIdget by Spinnaker
*       IsUnitChanneling by Magtheridon96
*
*************************************************************************************  
*
*                                 StrikeForce API
*
*************************************************************************************
*       static method create takes unit hero, boolean retreat, integer maxAttackSize, real attackDelay returns thistype
*           - Assigns a hero captain, he will lead the attack of the army
*           - Set the retreat to true so that if hero dies all units will return to base (if base is set)
*           - If the amount of added unit is less than maxAttackSize, hero will not engage
*           - maxAttackSize is the size quantity of the group that's engaging
*           - attackDelay is the delay of the next attack           
*            
*       method setHeroTargets takes integer targetID returns nothing
*           - 16 Total Players (0-15), see global constants for refference
*           - If you want the AI to target only player 1 then >>> call structInstanceName.setHeroTargets(0)
*           - If you want the AI to target only heroes then >>> call structInstanceName.setHeroTargets(ATTACK_ENEMY_HEROES_ONLY)
*            
*       method addUnit takes unit follower returns nothing
*           - Adds a unit to the group which increases the maxAttackSize
*           - Must be done manually for the attack to happen
*            
*       method setBase takes real x, real y returns nothing
*           - Sets the base which the AI will retreat if the hero dies or when retreat is true
*            
*       method setMaxAttackSize takes integer maxatksize returns nothing
*           - Changes/Sets the maxAttackSize of the group
*            
*       method setAttackDelay takes real attackDelay returns nothing
*           - Changes/Sets the delay of the next attack
*
*       method heroWaitForToops takes boolean wait, real waitDuration returns nothing
*           - This allows the hero to wait for lost units for (x) seconds
*
*       method removeUnit takes unit u returns nothing
*           - Removes a unit from the engaging group
*            
*       method remove takes nothing returns nothing
*           - Removes all units in the group that's engaging
*            
*************************************************************************************  
*
*                                 StrikeForceEx API
*
*************************************************************************************
*       static method create takes unit captain, real maxDelay, boolean killCaptainWhenGroupIsEmpty returns thistype
*           - Assigns a dummy captain, he will lead the attack but he cant attack
*           - It is advised that the dummy walks and doesnt have a locust ability
*           - Dummy captain must be a non-hero
*           - The dummy can be killed OR not when group is empty
*           - This captain be recycled by the next creation of the force
*        
*       static method createEx takes player p, real xCaptain, real yCaptain, real maxDelay returns thistype
*           - Creates a dummy captain automatically at XY coordinate in the form of a peasant ('hpea')
*           - Take note that this captain will die automatically once the group is empty 
*           - This captain cannot be recycled
*
*       static method setUnitBase takes unit follower, real xNewBase, real yNewBase returns nothing
*            - Sets/changes the retreat/base of individual unit     
*
*       method addUnit takes unit follower, real xReturn, real yReturn returns nothing
*           - Adds a unit to the group and sets the return/retreat/base of the follower  
*           - Must be done manually for the attack to happen        
*            
*       method setCaptainBase takes real xBase, real yBase returns nothing
*           - Sets the group formation at XY of captain when preparing an attack      
*        
*       method delayReset takes real delay returns nothing
*           - Change/Sets the delay of the next attack  
*            
*       method captainWaitForTroops takes boolean wait, real waitDuration returns nothing
*           - This allows the dummy to wait or stop lost units for (x) seconds
*            
*       method setTargets takes integer targetID returns nothing
*           - 16 Total Players (0-15), see global constants for refference
*           - If you want the AI to target only player 2 then >>> set structInstanceName.targets = 2   
*           - If you want the AI to target all enemies then >>> set structInstanceName.targets = ATTACK_ALL_ENEMIES
*       
*        method enableRetreat takes boolean b returns nothing
*           - Enables the retreat option for units
*
*        method remove takes boolean killCaptain returns nothing
*             - Removes all units from the attackgroup and kills the dummy (optional)
*           
**************************************************************************************
*
*   Settings: These Globals are configurables, but the recommended setting
*
**************************************************************************************/
globals
    /*******************************************************************
    *   Used by StrikeForce and StrikeForceEx
    ********************************************************************/    
    //Distance offset of following unit and captain
    private constant real       OFFSET_FROM_CAPTAIN = 250
    
    //Temporary retreats at base if target can't be attacked
    private constant real         TEMPORARY_RETREAT = 5
    
    //Distance of attacking unit and target, if less than indicated, the attacker will engage without waits 
    private constant real             IS_TARGET_FAR = 490000 //squareroot 700, recommended
    
    //Unit will keep on retreating (or go back to captain) until less than indicated
    private constant real             RETURN_OFFSET = 360000 //squareroot 600, recommended
    
    //Used for retreating, retreat must be true in both systems
    private constant real                      LIFE = 0.4 //40%

    /********************************************************************
    *   Used only by StrikeForceEx, by NORMAL UNITS
    ********************************************************************/    
    //The dummy unit that will lead the attack, default footman, this should be non-hero
    private constant integer       DUMMY_CAPTAIN_ID = 'hfoo'
    
    //Allows attacker to target closest unit in CLOSEST_TARGET range    
    private constant boolean  ENABLE_CLOSEST_TARGET = false
    
    //Engages closest target, ENABLE_CLOSEST_TARGET must be true
    private constant real            CLOSEST_TARGET = 600 
    
    /********************************************************************
    *   Global configurables
    ********************************************************************/
    //This setting is recommended and shoudn't be changed
    private constant real                MOVE_DELAY = 3.0
    
    /********************************************************************
    *   You may now use these constants to reffer attack priorities e.g.
    *   For StrikeForce >>> call heroX.setHeroTargets(ATTACK_ALL_ENEMIES) 
    *   For StrikeForceEx >>> set Army.targets = ATTACK_ENEMY_HEROES_ONLY
    *
    *   WARNING: These constants must not be modified!
    ********************************************************************/
    constant integer             ATTACK_ALL_ENEMIES = 16
    constant integer   ATTACK_ENEMY_STRUCTURES_ONLY = 17
    constant integer       ATTACK_ENEMY_FLYING_ONLY = 18
    constant integer       ATTACK_ENEMY_HEROES_ONLY = 19 
endglobals
/**************************************************************************************
*   End of configurable globals
***************************************************************************************/

//=====NON-CONFIGURABLES: NEVER TOUCH THESE GLOBAL BLOCK!=====
globals
    private constant integer                 ATTACK = 851983
    private constant integer                   MOVE = 851986
    private constant integer                  SMART = 851971
    private constant integer                   STOP = 851972
    private constant real                  INTERVAL = 1.0
    private unit TempUnit = null
    private group TempG = CreateGroup()
    private integer countUnit = 0 //counts how many units in a group
    
    //used only by StrikeForce
    private Table chk //used if hero is registered already
    
    //used only by StrikeForceEx
    private TableArray tba //used only by StrikeForceEx
endglobals

//===Simple group recycling
globals
    private group array Grp
    private integer indexGrp = 0
endglobals

private function NGroup takes nothing returns group
    set indexGrp = indexGrp + 1
    if Grp[indexGrp]==null then
        set Grp[indexGrp] = CreateGroup() 
    endif
    return Grp[indexGrp]
endfunction

private function RGroup takes group g returns nothing
    set Grp[indexGrp] = g
    call GroupClear(g)
    set indexGrp = indexGrp - 1  
endfunction
//==========

//! textmacro EngageS takes tID, heroU
    private static method engageSpecificPlayerUnit takes nothing returns boolean
        local thistype this = DATA
        local integer i = 0
        set TempUnit = GetFilterUnit()  
        if UnitAlive(TempUnit) and IsUnitEnemy($heroU$,GetOwningPlayer(TempUnit)) and IsUnitEngageable(TempUnit) then   
            if $tID$ < 16 then
                loop
                    if $tID$==i then
                        return GetOwningPlayer(TempUnit)==Player(i)
                    endif   
                    exitwhen i==$tID$
                    set i = i+1
                endloop
            elseif $tID$==16 then
                return true
            elseif $tID$==17 then
                return IsUnitType(TempUnit,UNIT_TYPE_STRUCTURE)
            elseif $tID$==18 then
                return IsUnitType(TempUnit,UNIT_TYPE_FLYING)
            elseif $tID$==19 then
                return IsUnitType(TempUnit,UNIT_TYPE_HERO)
            endif
        endif
        return false
    endmethod
//! endtextmacro

//! textmacro Targets takes targetID, tar, unitU
    if $tar$ > -1 and $tar$ < 16 then
        if GetOwningPlayer($unitU$)==Player($tar$) or not IsUnitEnemy($unitU$, Player($tar$)) then
            debug call BJDebugMsg("ERROR: can't target this Player!")
        else
            set $targetID$ = $tar$
        endif
    elseif $tar$ > 15 and $tar$ < 20 then
        set $targetID$ = $tar$
    else
        debug call BJDebugMsg("ERROR: Please input only from 0 to 19!")
    endif
//! endtextmacro

private function AttackMoveOrders takes integer order returns boolean
    return (order==ATTACK or order==MOVE)
endfunction

private function UnitAlive takes unit u returns boolean
    return not IsUnitType(u, UNIT_TYPE_DEAD) and u!=null
endfunction
      
private function GetDistance takes real x1, real y1, real x2, real y2 returns real
    return (x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)
endfunction

private function IsTargetFar takes unit u, unit u2 returns boolean
    return (GetDistance(GetUnitX(u), GetUnitY(u), GetUnitX(u2), GetUnitY(u2))) > (IS_TARGET_FAR) //this is 700 range
endfunction

private function AttackNow takes unit u, unit target, real x, real y returns nothing
    if not AttackMoveOrders(GetUnitCurrentOrder(u)) then
        if IsUnitType(u, UNIT_TYPE_SLEEPING) then
            call IssueTargetOrderById(u, ATTACK, target)
        else
            call IssuePointOrderById(u, ATTACK, x, y) 
        endif 
    endif
endfunction

private function EngageCloseTargets takes nothing returns boolean
    return UnitAlive(GetFilterUnit()) and IsUnitEnemy(TempUnit,GetOwningPlayer(GetFilterUnit())) and IsUnitEngageable(GetFilterUnit())
endfunction

private function RandomAngle takes nothing returns real
    return GetRandomReal(0,6.28)
endfunction

private function LifeLessThanMaxLife takes unit u returns boolean
    return GetWidgetLife(u) < (GetUnitState(u, UNIT_STATE_MAX_LIFE) * LIFE)
endfunction

private function MoveToCaptain takes unit u, real x, real y, real offset returns nothing
    call IssuePointOrderById(u, ATTACK, x+offset*Cos(RandomAngle()), y+offset*Sin(RandomAngle()))
endfunction

private function ReturnToBase takes unit u, real xBase, real yBase returns nothing
    if (GetDistance(GetUnitX(u), GetUnitY(u), xBase, yBase)) > (RETURN_OFFSET) then
        call IssuePointOrderById(u, MOVE, xBase, yBase)
    endif
endfunction

private function NoEnemyUnits takes unit u, real radius returns boolean
    local unit first
    call GroupEnumUnitsInRange(TempG, GetUnitX(u), GetUnitY(u), radius, null)
    loop
        set first = FirstOfGroup(TempG)
        exitwhen first==null
        if UnitAlive(first) and IsUnitEnemy(first, GetOwningPlayer(u)) and not UnitIsSleeping(first) then
            return false
        endif
        call GroupRemoveUnit(TempG, first)
    endloop  
    return true
endfunction

private function GroupRetreat takes unit u, real captainXX, real captainYY, real baseXX, real baseYY returns nothing
    if LifeLessThanMaxLife(u) then
        call ReturnToBase(u, baseXX, baseYY)
    else
        call MoveToCaptain(u, captainXX, captainYY, OFFSET_FROM_CAPTAIN)
    endif            
endfunction

private function CountUnits takes nothing returns nothing
    if UnitAlive(GetEnumUnit()) then
        set countUnit = countUnit + 1 
    endif
endfunction

private function UnitsInGroup takes group grp returns integer
    call ForGroup(grp, function CountUnits)
    return countUnit
endfunction

//=====END OF NON-CONFIGURABLE BLOCK=====
private struct init
    private static method onInit takes nothing returns nothing
        set chk = Table.create()
        set tba = TableArray[0x2000]        
    endmethod
endstruct

/*************************************************************************************
*   StrikeForce: used by HEROES, assigns as captain who leads
**************************************************************************************/
struct StrikeForce
    private unit hero
    private unit target
    private unit closeTarget
    private real baseX
    private real baseY
    private real moveDelay
    private real minAttackDelay //starts with 0, max is attackDelay
    private real maxAttackDelay
    private real heroWaitInterval
    private real heroEngageTime //used only if heroWaitsTroop is true
    private real heroWaitTroopDuration
    private real tempRetreat
    private real heroStopGroup
    private integer minAtkSize   
    private integer maxAtkSize   
    private integer handleID
    private integer targetPlayerID
    private boolean retreat
    private boolean onSF
    private boolean heroWaitsTroop
    private boolean heroWaitOneTime
    private group heroGroup
    
    private static thistype DATA
    private static timer t = CreateTimer()
    private static integer instance = 0
    private static integer array insAR
    
    //! runtextmacro EngageS(".targetPlayerID",".hero")
    
    private method destroy takes nothing returns nothing
        call RGroup(.heroGroup)
        set .closeTarget = null
        set .hero = null
        set .target = null
        set .heroGroup = null
        call .deallocate()
    endmethod
    
    //runs every INTERVAL, units return to base when hero is dead or target cannot be attacked
    private static method groupReturnToBase takes nothing returns nothing
        local unit u = GetEnumUnit() 
        local thistype this        
        if UnitAlive(u) then
            set this = DATA
            call ReturnToBase(u, .baseX, .baseY)        
        endif
        set u = null
    endmethod
    
    //runs every INTERVAL, for re-grouping if hero finds a new target or hero waits troops or maxAttackSize is not met
    private static method groupFormation takes nothing returns nothing
        local unit u = GetEnumUnit() 
        local thistype this
        local real hX
        local real hY
        if UnitAlive(u) then
            set this = DATA
            set hX = GetUnitX(.hero)
            set hY = GetUnitY(.hero)
            if .retreat then
                call GroupRetreat(u, hX, hY, .baseX, .baseY)
            else
                call MoveToCaptain(GetEnumUnit(), hX, hY, OFFSET_FROM_CAPTAIN)
            endif        
        endif
        set u = null
    endmethod 
    
    //runs every MOVE_DELAY
    private static method groupAttack takes nothing returns nothing
        local unit u = GetEnumUnit() //u is the follower of the hero
        local thistype this    
        local real hX
        local real hY
        local real xTar
        local real yTar
        local real distHeroFollower
        local real distTargetFollower
        local real xFollower 
        local real yFollower
        if UnitAlive(u) then 
            if not IsUnitChanneling(u) then
                set this = DATA //DATA taken from looper        
                set hX = GetUnitX(.hero)
                set hY = GetUnitY(.hero)
                set xTar = GetUnitX(.target)
                set yTar = GetUnitY(.target)
                set xFollower = GetUnitX(u)
                set yFollower = GetUnitY(u)
                set distTargetFollower = GetDistance(xTar, yTar, xFollower, yFollower)                
                set distHeroFollower = GetDistance(hX, hY, xFollower, yFollower)
                if .retreat then
                    if LifeLessThanMaxLife(u) then
                        call ReturnToBase(u, .baseX, .baseY) 
                    else
                        if distHeroFollower > RETURN_OFFSET then
                            call MoveToCaptain(u, hX, hY, OFFSET_FROM_CAPTAIN)
                        else
                            call AttackNow(u, .target, xTar, yTar)
                        endif                    
                    endif
                else
                    if distHeroFollower > RETURN_OFFSET then
                        call MoveToCaptain(u, hX, hY, OFFSET_FROM_CAPTAIN)
                    else
                        call AttackNow(u, .target, xTar, yTar)
                    endif
                endif
            endif    
         else
            set .minAtkSize = .minAtkSize - 1
            call GroupRemoveUnit(.heroGroup, u)
        endif
        set u = null
    endmethod
    
    //runs every INTERVAL
    private method heroEngaging takes nothing returns nothing
        if not IsUnitChanneling(.hero) then
            //hero attacks
            call AttackNow(.hero, .target, GetUnitX(.target), GetUnitY(.target))
            //hero group attack, not the hero
            call ForGroup(.heroGroup,function thistype.groupAttack)
        endif
    endmethod
    
    //runs every INTERVAL
    private method heroMovement takes nothing returns nothing
        set .moveDelay = .moveDelay + INTERVAL
        if .moveDelay > MOVE_DELAY then
            set .moveDelay = 0
            //movement of hero and group, default every 3 seconds
            //===Hero retreats, this should be true if hero wants to retreat
            if .retreat then
                if LifeLessThanMaxLife(.hero) then
                    call ReturnToBase(.hero, .baseX, .baseY)
                    call ForGroup(.heroGroup, function thistype.groupReturnToBase)
                else
                    call .heroEngaging()
                endif
            else
                call .heroEngaging()
            endif
        endif
    endmethod 
    
    //runs every INTERVAL
    private method findTarget takes nothing returns nothing
        set .target = GetClosestUnit(GetUnitX(.hero), GetUnitY(.hero), Filter(function thistype.engageSpecificPlayerUnit))
        set .minAttackDelay = 0 //resets the delay when engaging the next target
        set .heroWaitInterval = 0
        if .target==null then
            call ReturnToBase(.hero, .baseX, .baseY)
            call ForGroup(.heroGroup, function thistype.groupReturnToBase)
        else
            call ForGroup(.heroGroup, function thistype.groupFormation)
        endif    
    endmethod    
    
    //this is the hero moving, not the units, runs every INTERVAL
    private static method looper takes nothing returns nothing
        local thistype this
        local integer i = 0
        loop
            set i = i+1
            set this = insAR[i]
            if .onSF then
                if UnitAlive(.hero) then
                    if .minAtkSize >= .maxAtkSize then
                        set .minAttackDelay = .minAttackDelay + INTERVAL
                        if .minAttackDelay > .maxAttackDelay then
                            set DATA = this
                            if UnitAlive(.target) then
                                if IsUnitEngageable(.target) then
                                    if .heroWaitsTroop then
                                        set .heroWaitInterval = .heroWaitInterval + INTERVAL
                                        if .heroEngageTime > .heroWaitInterval then
                                            if .heroWaitTroopDuration > .heroWaitInterval then
                                                call .heroMovement()
                                            else
                                                if not IsUnitChanneling(.hero) then
                                                    if IsTargetFar(.hero, .target) and NoEnemyUnits(.hero, 600) then
                                                        if .heroWaitOneTime then
                                                            call IssueImmediateOrderById(.hero, STOP)
                                                            call ForGroup(.heroGroup, function thistype.groupFormation)
                                                            set .heroWaitOneTime = false
                                                        endif                                                        
                                                    endif
                                                endif                                                
                                            endif
                                        else
                                            set .heroWaitOneTime = true
                                            set .heroWaitInterval = 0
                                        endif
                                    else
                                        call .heroMovement()
                                    endif
                                else
                                    //if unit cant be attacked, hero and the group will return to base
                                    call ForGroup(.heroGroup, function thistype.groupReturnToBase)
                                    call ReturnToBase(.hero, .baseX, .baseY)
                                    set .tempRetreat = .tempRetreat + INTERVAL
                                    if .tempRetreat > TEMPORARY_RETREAT then
                                        set .tempRetreat = 0
                                        call .findTarget() //this finds another target    
                                        call ForGroup(.heroGroup, function thistype.groupReturnToBase)
                                    endif                                                                        
                                endif
                            else
                                //if target is dead or null, hero will return to base or find another target
                                //from UnitAlive(.target)
                                call .findTarget()
                            endif
                        endif //no else from .minAttackDelay > .maxAttackDelay
                    else
                        //from .minAtkSize >= .maxAtkSize
                        set .heroStopGroup = .heroStopGroup + INTERVAL
                        if .heroStopGroup > 3 then
                            set .heroStopGroup = 0  
                            if not IsUnitChanneling(.hero) then
                                if IsUnitEngageable(.target) then
                                    call IssuePointOrderById(.hero, ATTACK, GetUnitX(.hero), GetUnitY(.hero))
                                    call ForGroup(.heroGroup, function thistype.groupFormation)                                
                                else
                                    call ReturnToBase(.hero, .baseX, .baseY)
                                    call ForGroup(.heroGroup, function thistype.groupReturnToBase)
                                endif
                            endif                                                      
                        endif 
                    endif
                else
                    //Hero is dead, from UnitAlive(.hero)
                    call ForGroup(.heroGroup, function thistype.groupReturnToBase)
                endif
            else
                call .destroy()
                set insAR[i] = insAR[instance]
                set insAR[instance] = this
                set i = i - 1
                set instance = instance - 1    
                if instance==0 then
                    call PauseTimer(t)
                endif
            endif
            exitwhen i==instance
        endloop
    endmethod
    
    //StrikeForce API:
    static method create takes unit hero, boolean retreat, integer maxAttackSize, real attackDelay returns thistype
        local thistype this
        if chk.has(GetHandleId(hero)) then
             debug call BJDebugMsg("StrikeForce.create ERROR: "+GetUnitName(hero)+" is already registered!")
        else
            if IsUnitType(hero,UNIT_TYPE_HERO) then
                set this = allocate()
                set chk[GetHandleId(hero)] = this
                set .heroGroup = NGroup()
                set .hero = hero
                set .handleID = GetHandleId(hero)
                set .target = null
                set .closeTarget = null
                set .moveDelay = 0                
                set .minAttackDelay = 0
                set .maxAttackDelay = attackDelay
                set .minAtkSize = 0
                set .maxAtkSize = maxAttackSize
                set .retreat = retreat
                set .onSF = true
                set .heroWaitInterval = 0
                set .heroEngageTime = 0
                set .tempRetreat = 0
                set .heroWaitTroopDuration = 0
                set .heroStopGroup = 0
                set .heroWaitsTroop = false
                set .heroWaitOneTime = true
                if instance==0 then
                    call TimerStart(t,INTERVAL,true,function thistype.looper)
                endif
                set instance = instance + 1
                set insAR[instance] = this
                call RemoveGuardPosition(hero)
            else
                debug call BJDebugMsg("StrikeForce.create ERROR: Please register a Hero Type unit!")
            endif
        endif       
        return this
    endmethod
    
    method setHeroTargets takes integer targetID returns nothing
        //! runtextmacro Targets(".targetPlayerID","targetID",".hero")
    endmethod
    
    method addUnit takes unit follower returns nothing
        if follower!=.hero then
            call GroupAddUnit(.heroGroup,follower)
            call RemoveGuardPosition(follower)
            call MoveToCaptain(follower, GetUnitX(.hero), GetUnitY(.hero), OFFSET_FROM_CAPTAIN)
            set .minAtkSize = .minAtkSize + 1
        endif
    endmethod
    
    method setBase takes real x, real y returns nothing
        set .baseX = x
        set .baseY = y
    endmethod  
    
    method setMaxAttackSize takes integer maxatksize returns nothing
        set .maxAtkSize = maxatksize
    endmethod
    
    method setAttackDelay takes real attackDelay returns nothing
        set .maxAttackDelay = attackDelay
    endmethod
    
    method heroWaitForToops takes boolean wait, real waitDuration returns nothing
        set .heroWaitsTroop = wait
        set .heroEngageTime = waitDuration*3
        set .heroWaitTroopDuration = .heroEngageTime-waitDuration
    endmethod
    
    method removeUnit takes unit u returns nothing
        if IsUnitInGroup(u, .heroGroup) then
            call GroupRemoveUnit(.heroGroup, u)
            set .minAtkSize = .minAtkSize - 1
            call IssuePointOrderById(u, MOVE, .baseX, .baseY)
        else
            debug call BJDebugMsg("StrikeForce.removeUnit ERROR: "+GetUnitName(u)+" is not in this group")
        endif
    endmethod
    
    method remove takes nothing returns nothing
        set .onSF = false
        call chk.remove(GetHandleId(.hero))
    endmethod
endstruct

/*************************************************************************************
*   StrikeForceEx: used for NORMAL UNITS, assigns a dummy captain
**************************************************************************************/
struct StrikeForceEx
    private unit captain
    private unit target
    private unit closestTarget
    private real maxDelay
    private real minDelay
    private real xBack
    private real yBack
    private real moveWait //use 3.0
    private real dummyWaitInterval
    private real captainEngageTime
    private real captainWaitTime
    private real tempRetreat
    private integer targetPlayerID
    private group dummyGroup
    private boolean on
    private boolean killCaptain
    private boolean retreat
    private boolean captainWaitsTroop    
    
    private static thistype DATA    
    private static integer index = 0
    private static integer array indexAR
    private static timer t = CreateTimer()
   
    //! runtextmacro EngageS(".targetPlayerID",".captain")
    
    private method destroy takes nothing returns nothing
        if .killCaptain then
            call KillUnit(.captain)
        endif
        call RGroup(.dummyGroup)
        set .captain = null
        set .target = null
        set .closestTarget = null
        set .dummyGroup = null
        call .deallocate()
    endmethod
    
    //this is only used if the group is about to be destroyed
    private static method grpReturn takes nothing returns nothing
        local unit u = GetEnumUnit()
        local real xRet
        local real yRet
        if UnitAlive(u) then
            set xRet = tba[0].real[GetHandleId(u)]
            set yRet = tba[1].real[GetHandleId(u)]
            if GetDistance(xRet, yRet, GetUnitX(u), GetUnitY(u)) > RETURN_OFFSET then
                call IssuePointOrderById(u, MOVE, xRet, yRet)
            endif
        endif
        set u = null
    endmethod
    
    private static method groupFormation takes nothing returns nothing
        local unit u = GetEnumUnit()
        local thistype this         
        local real x
        local real y
        if UnitAlive(u) then
            set this = DATA
            set x = GetUnitX(.captain)
            set y = GetUnitY(.captain)
            if .retreat then
                if LifeLessThanMaxLife(u) then
                    call ReturnToBase(u, tba[0].real[GetHandleId(u)], tba[1].real[GetHandleId(u)])
                else
                    call MoveToCaptain(u, x, y, OFFSET_FROM_CAPTAIN)
                endif
            else                
                call MoveToCaptain(u, x, y, OFFSET_FROM_CAPTAIN)
            endif        
        endif
        set u = null
    endmethod
    
    //runs every MOVE_DELAY
    private static method groupEngagingEx takes unit u, thistype this, real xCaptain, real yCaptain returns nothing  
        local real xFol
        local real yFol
        local real xTar
        local real yTar
        local real distanceFromCaptain
        if not IsUnitChanneling(u) then
            set xFol = GetUnitX(u)
            set yFol = GetUnitY(u)
            set xTar = GetUnitX(.target)
            set yTar = GetUnitY(.target)
            set distanceFromCaptain = GetDistance(xFol, yFol, xCaptain, yCaptain)
            if ENABLE_CLOSEST_TARGET then
                if UnitAlive(.closestTarget) and IsUnitEngageable(.closestTarget) then
                    call AttackNow(u, .closestTarget, GetUnitX(.closestTarget), GetUnitY(.closestTarget))
                else
                    //if captain is far, unit will go near captain, else attack the target
                    if distanceFromCaptain > 600000 then //600
                        call MoveToCaptain(u, xCaptain, yCaptain, OFFSET_FROM_CAPTAIN)
                    else               
                        call AttackNow(u, .target, xTar, yTar)
                    endif 
                endif
            else
                //if captain is far, unit will go near captain, else attack the target
                if distanceFromCaptain > 600000 then //600
                    call MoveToCaptain(u, xCaptain, yCaptain, OFFSET_FROM_CAPTAIN)
                else               
                    call AttackNow(u, .target, xTar, yTar)
                endif  
            endif
        endif  
    endmethod
    
    //runs every MOVE_DELAY
    private static method grpEngaging takes nothing returns nothing
        local unit u = GetEnumUnit()
        local thistype this
        local real x
        local real y
        if UnitAlive(u) then
            set this = DATA
            set x = GetUnitX(.captain)
            set y = GetUnitY(.captain)
            if .retreat then
                if LifeLessThanMaxLife(u) then
                    call ReturnToBase(u, tba[0].real[GetHandleId(u)], tba[1].real[GetHandleId(u)])
                else
                    call thistype.groupEngagingEx(u, this, x, y)
                endif
            else
                call thistype.groupEngagingEx(u, this, x, y)
            endif
        else
            call GroupRemoveUnit(.dummyGroup, u)
        endif        
        set u = null
    endmethod
    
    //runs every INTERVAL
    private method captainEngaging takes nothing returns nothing
        local real x
        local real y
        set .moveWait = .moveWait + INTERVAL
        if .moveWait > MOVE_DELAY then
            set .moveWait = 0
            set x = GetUnitX(.captain)
            set y = GetUnitY(.captain)
            set DATA = this
            if ENABLE_CLOSEST_TARGET then
                if UnitAlive(.closestTarget) and IsUnitEngageable(.closestTarget) then
                    call IssuePointOrderById(.captain, MOVE, GetUnitX(.closestTarget), GetUnitY(.closestTarget))
                else
                    set TempUnit = .captain
                    set .closestTarget = GetClosestUnitInRange(x, y, CLOSEST_TARGET, Filter(function EngageCloseTargets))
                    if .closestTarget==null then
                        call ForGroup(.dummyGroup, function thistype.grpEngaging)
                        call IssuePointOrderById(.captain, MOVE, GetUnitX(.target), GetUnitY(.target))                        
                    endif
                endif 
            else
                call ForGroup(.dummyGroup, function thistype.grpEngaging)
                call IssuePointOrderById(.captain, MOVE, GetUnitX(.target), GetUnitY(.target))                
            endif 
        endif
    endmethod
    
    //runs from looper if target is dead or not engageable
    private method findCaptainNewTarget takes nothing returns nothing
        set .minDelay = 0
        set .target = GetClosestUnit(GetUnitX(.captain), GetUnitY(.captain), Filter(function thistype.engageSpecificPlayerUnit))
        if .target==null then
            //captain goes back to original location
            call IssuePointOrderById(.captain, MOVE, .xBack, .yBack)
            //individual group go back to their respective locations
            call ForGroup(.dummyGroup, function thistype.grpReturn)
        else
            call ForGroup(.dummyGroup, function thistype.groupFormation)
        endif  
    endmethod    
    
    //runs every INTERVAL
    private static method looper takes nothing returns nothing
        local thistype this 
        local integer i = 0
        loop
            set i = i+1
            set this = indexAR[i]
            if .on and UnitAlive(.captain) then
                set .minDelay = .minDelay + INTERVAL
                if .minDelay > .maxDelay then
                    set DATA = this
                    set countUnit = 0
                    if UnitsInGroup(.dummyGroup) > 0 then //checks if the group is empty or not
                        if UnitAlive(.target) then
                            if IsUnitEngageable(.target) then
                                if .captainWaitsTroop then
                                    set .dummyWaitInterval = .dummyWaitInterval + INTERVAL  
                                    if .captainEngageTime > .dummyWaitInterval then
                                        if .captainWaitTime > .dummyWaitInterval then
                                            call .captainEngaging()
                                        else
                                            if IsTargetFar(.captain, .target) then
                                                call IssueImmediateOrderById(.captain, STOP)
                                                call ForGroup(.dummyGroup, function thistype.groupFormation)
                                            endif
                                        endif
                                    else
                                        set .dummyWaitInterval = 0 
                                    endif
                                else
                                    call .captainEngaging()                            
                                endif
                            else
                                call ForGroup(.dummyGroup, function thistype.grpReturn)
                                call IssuePointOrderById(.captain, MOVE, .xBack, .yBack)
                                set .tempRetreat = .tempRetreat + INTERVAL
                                if .tempRetreat > TEMPORARY_RETREAT then
                                    set .tempRetreat = 0
                                    call .findCaptainNewTarget()
                                    call ForGroup(.dummyGroup, function thistype.grpReturn)
                                endif
                            endif
                        else //from UnitAlive(.target)
                            call .findCaptainNewTarget()
                        endif                   
                    else //from UnitsInGroup(.dummyGroup)
                        if .killCaptain then
                            set .on = false
                        else
                            call SetUnitPosition(.captain, .xBack, .yBack)                            
                        endif
                    endif
                endif
            else //from .on and UnitAlive(.captain)
                //this order returns all units in the group to it's base
                set DATA = this
                call ForGroup(.dummyGroup, function thistype.grpReturn)
                //deallocating instance
                call .destroy()
                set indexAR[i] = indexAR[index]
                set indexAR[index] = this
                set index = index - 1
                set i = i-1
                if index==0 then
                    call PauseTimer(t)
                endif            
            endif          
            exitwhen i==index
        endloop
    endmethod
    
    //StrikeForceEx API:
    static method create takes unit captain, real maxDelay, boolean killCaptainWhenGroupIsEmpty returns thistype
        local thistype this
        if IsUnitType(captain, UNIT_TYPE_HERO) then
            debug call BJDebugMsg("StrikeForceEx (create) ERROR: "+GetUnitName(captain)+" must be a non-hero!")
        else
            set this = allocate()
            set .captain = captain
            set .target = null
            set .closestTarget = null
            set .minDelay = 0
            set .maxDelay = maxDelay  
            set .dummyGroup = NGroup() //CreateGroup()
            set .on = true  
            set .killCaptain = killCaptainWhenGroupIsEmpty
            set .moveWait = 0
            set .targetPlayerID = 0
            set .dummyWaitInterval = 0
            set .captainEngageTime = 0
            set .captainWaitTime = 0
            set .tempRetreat = 0
            set .captainWaitsTroop  = false
            set .retreat = false
            call SetUnitUseFood(captain, false)
            if index==0 then
                call TimerStart(t,INTERVAL,true,function thistype.looper)
            endif
            set index = index + 1
            set indexAR[index] = this
            call RemoveGuardPosition(captain)
            call SetUnitInvulnerable(captain,true)
            call ShowUnit(captain,false)
        endif
        return this
    endmethod
    
    static method createEx takes player p, real xCaptain, real yCaptain, real maxDelay returns thistype
        local thistype this
        local unit u = CreateUnit(p, DUMMY_CAPTAIN_ID, xCaptain, yCaptain, 0)
        call UnitAddAbility(u, 'Abun')
        set this = create(u, maxDelay, true)
        set u = null
        return this
    endmethod      
    
    static method setUnitBase takes unit follower, real xNewBase, real yNewBase returns nothing
        set tba[0].real[GetHandleId(follower)] = xNewBase
        set tba[1].real[GetHandleId(follower)] = yNewBase
    endmethod
    
    method addUnit takes unit follower, real xReturn, real yReturn returns nothing
        if follower!=.captain then
            set tba[0].real[GetHandleId(follower)] = xReturn
            set tba[1].real[GetHandleId(follower)] = yReturn
            call GroupAddUnit(.dummyGroup, follower)
            call RemoveGuardPosition(follower)
            call MoveToCaptain(follower, GetUnitX(.captain), GetUnitY(.captain), OFFSET_FROM_CAPTAIN)
        endif
    endmethod
    
    method setCaptainBase takes real xBase, real yBase returns nothing
        set .xBack = xBase
        set .yBack = yBase
    endmethod
    
    method delayReset takes real delay returns nothing
        set .minDelay = 0
        set .maxDelay = delay
    endmethod
    
    method captainWaitForTroops takes boolean wait, real waitDuration returns nothing
        set .captainWaitsTroop = wait
        set .captainEngageTime = waitDuration*3
        set .captainWaitTime = .captainEngageTime-waitDuration
    endmethod
    
    method setTargets takes integer targetID returns nothing
        //! runtextmacro Targets(".targetPlayerID","targetID",".captain")
    endmethod
    
    method enableRetreat takes boolean b returns nothing
        set .retreat = b
    endmethod
    
    method remove takes boolean killCaptain returns nothing
        set .killCaptain = killCaptain
        set .on = false    
    endmethod
endstruct

endlibrary

The global constants for easier reading

JASS:
        constant integer             ATTACK_ALL_ENEMIES = 16
        constant integer   ATTACK_ENEMY_STRUCTURES_ONLY = 17
        constant integer       ATTACK_ENEMY_FLYING_ONLY = 18
        constant integer       ATTACK_ENEMY_HEROES_ONLY = 19
You may now use these constants to reffer attack priorities e.g.
- For StrikeForce call heroX.setHeroTargets(ATTACK_ALL_ENEMIES)
- For StrikeForceEx call Army.setTargets(ATTACK_ENEMY_HEROES_ONLY)


JASS:
scope StrikeForceDemo

globals
    //you may declare this also as a local, but recommended as global
    //coz you may want your hero to be removed from the system
    StrikeForce heroX 
    private constant player P = Player(1) //player 1 in GUI
endglobals

struct StrikeForceDemo
    static method strikeForce takes nothing returns nothing  
        //this block is NOT related, just creating units
        local integer PreplacedUnits = 5
        local integer ArmySize = 10
        local integer i = PreplacedUnits
        local unit bm = CreateUnit(P, 'Hblm', -1632, 1665, 0)
        local unit grunt
        call SetHeroLevel(bm, 30, true)
        //call SetWidgetLife(bm, 2)
        //=============================================================
        //once the hero is available, let's put him in the system
        //create it first >>> unit hero, boolean retreat, integer maxAttackSize, real attackDelay
        //attackDelay means the time delay for the next target
        //maxAttackSize means how many units in the group
        set heroX = heroX.create(bm, false, ArmySize, 5)
        
        //if one or more troop is far from hero the hero may stop and wait for them if the target is still far
        //method heroWaitForToops takes boolean wait, real waitDuration returns nothing
        call heroX.heroWaitForToops(true, 5)
        
        //setting the hero targets, ATTACK_ALL_ENEMIES is inside the system global constants
        call heroX.setHeroTargets(ATTACK_ALL_ENEMIES)
        
        //set the base, when no more enemies the group will return to base
        //this is also used for retreating
        call heroX.setBase(-1632, 1665)
        
        //creating an army for the hero
        loop
            exitwhen i==0
            set grunt = CreateUnit(P, 'ogru', 300, 2500, 0) //grunt
            call SetWidgetLife(grunt, 5)
            
            //creating a grunt and add to the system
            //this increases the attack size
            call heroX.addUnit(grunt)           
            set i = i - 1
        endloop
        
        //Text display only, NOT related to the system
        call DisplayTimedTextFromPlayer(Player(0), 0, 0, 2000, "")
        call DisplayTimedTextFromPlayer(Player(0), 0, 0, 2000, "StrikeForce vJass is ON, this message is NOT related to the system")
        call DisplayTimedTextFromPlayer(Player(0), 0, 0, 2000, "ArmySize is " + I2S(ArmySize))
        call DisplayTimedTextFromPlayer(Player(0), 0, 0, 2000, "Added Unit is " + I2S(PreplacedUnits))
        call DisplayTimedTextFromPlayer(Player(0), 0, 0, 2000, "Train units to be added to the force until AttackSize is met")
        call DisplayTimedTextFromPlayer(Player(0), 0, 0, 2000, "Heal the orcs then they will join the group")       
        
        //NOTE: if you set the maxAttackSize and setAttackDelay to 0, the the hero will keep on engaging
        //changes or sets the maxAttackSize of the group
        //call heroX.setMaxAttackSize(0) //optional since we already created this above
        
        //sets the delay time to engage the next target, I'm not gonna use this ATM
        //call heroX.setAttackDelay(0) //optional since we already created this above
    endmethod
    
    static method train takes nothing returns nothing
        call heroX.addUnit(GetTrainedUnit())
    endmethod   
    
    static method onInit takes nothing returns nothing
        call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_TRAIN_FINISH, function thistype.train)
        call TimerStart(CreateTimer(), 0, false, function thistype.strikeForce) //hero captain
    endmethod
endstruct

endscope



  • StrikeForceEx Demo GUI
    • Events
      • Time - Elapsed game time is 0.00 seconds
    • Conditions
    • Actions
      • -------- let's create the StrikeForceEx first --------
      • -------- you may use a global instance but Im not gonna explain it here, try the vJass StrikeForce --------
      • Custom script: local StrikeForceEx AI
      • -------- creating the dummy captain, setting the player, coordinates and how long the AI will wait to engage the target, in this case 10 seconds --------
      • -------- you may use also the create but createEx creates a captain automatically --------
      • Custom script: set AI = AI.createEx(Player(1), -2331, 1572, 10)
      • -------- setting the targets, the ATTACK_ENEMY_HEROES_ONLY is a constant variable indicated in the srcript --------
      • Custom script: call AI.setTargets(ATTACK_ENEMY_HEROES_ONLY)
      • -------- setting where the dummy captain will return when there'e no more heroes in the map --------
      • -------- sample in the middle of the map --------
      • Custom script: call AI.setCaptainBase(0, 0)
      • -------- retreat function is a NEW method enables the unit to retreat when life is low --------
      • Custom script: call AI.enableRetreat(true)
      • -------- waiting for troops is a new option, allows the captain to stop in x seconds just to group/wait lost units --------
      • Custom script: call AI.captainWaitForTroops(true, 7)
      • -------- udg_PASS_DATA is an optional global integer variable used to pass/preserve the index of the AI data during group itinerations --------
      • Custom script: set udg_PASS_DATA = AI
      • Unit Group - Pick every unit in (Units owned by Player 2 (Blue) matching (((Matching unit) is A Hero) Equal to False)) and do (Actions)
        • Loop - Actions
          • -------- this is GUI version, so this is normal, creating the local again --------
          • Custom script: local StrikeForceEx AI
          • -------- passing the data information to the AI instance --------
          • Custom script: set AI = udg_PASS_DATA
          • -------- now we can add members to the group --------
          • -------- the -1953 and 1299 are x and y of the map, whre individual units return if retreat is on --------
          • Custom script: call AI.addUnit(GetEnumUnit(), -1953, 1299)
          • -------- This is only a test if retreat is working, but this is completely optional --------
          • Custom script: call SetWidgetLife(GetEnumUnit(), 5)




- Table by Bribe
- GetClosestWidget by Spinnaker
- IsUnitChanneling by Magtheridon96
- RegisterPlayerUnitEvent by Magtheridon96

Others:
- PurgeandFire111 for the distance issue


v1.6
- Improved documentation
- Fixed dummy captain that used food
- Some constant globals turned to struct method instance
- Added retreat function for StrikeforceEx
- Added wait function for both

v1.5a
- Improved documentation
- FLYIER changed to FLYING

v1.5
- Added global integer constants to reffer attack priority
- Player numbers is now 0-15 instead of 1-16
- group nulled
- createEx has been simplified

v1.4
- Added new function createEx which enables to automatically creates a dummy captain

v1.3
- Added GroupUtils library by Rising_Dusk
- Fixed distance issues pointed out by PurgeandFire111
- Added more documentation

v1.2a
- Fixed a minor bug from v1.2, StrikeForceEx dummy doesnt wait properly

v1.2
- Added IsUnitChanneling for faster execution of non-movements of AI
- Removed the first changelog from v1.1 coz it bugs in my The Harvest map

v1.1
- Added a function that the attacker can attack ground or air units, else he will return to base
- The wait for hero/dummy function is more accurate now
- TargetID's is more efficient


Keywords:
AI, strike, group, dota, mckill2009, engage, enemy, target, ai, system, fire, wicked, awesome, recommended, high
Contents

StrikeForce (Map)

Reviews
22:38, 16th November 2012 Magtheridon96: Approved. The demos here are a plus.

Moderator

M

Moderator

22:38, 16th November 2012
Magtheridon96: Approved.

The demos here are a plus.
 
JASS:
    private constant real         INTERVAL = 1.0 //recommended
    private constant real       MOVE_DELAY = 3.0 //recommended

But these are configurables, and thus should be in the config section, right? :p

Also, you don't need the UnitNotChanneling function, I have a snippet in the JASS section called IsUnitChanneling that handles any channeling spell done without requiring the user to modify anything.
It's also very fast too :D
 
Level 29
Joined
Mar 10, 2009
Messages
5,016
The purpose of that interval and delay globals is caching and doesnt need to be adjusted coz that will affect the movements of the group...

I know your IsUnitChanneling and I even used it in my MuiDummyCasters, but since you used UnitIndexer which requires 2 additional libraries, I ignored it...

This system needs more caching of things, targets needs more filters, wait part needs to be more effecient and StrikeForceEx needs to engage
the enemy immediately when at range coz now it follows the dummy which causes a slight delay in engaging enemies which is not good...

EDIT:
v1.1
- Added a function that the attacker can attack ground or air units, else he will return to base
- The wait for hero/dummy function is more accurate now
- TargetID's is more efficient
 
Last edited:
Here are the first few things I noticed:
  • The spell should have some better documentation. For example, what is the point of it? I know you say that it is a sequel, but that still doesn't give a whole lot of information. Also, the methods should have a bit of documentation. (it can be brief if you want) I might be able to find out what they do by guessing from the name but it won't be really clear, so you should write that in the documentation.
  • The distances, such as 700000 and 600000, are a bit off. If you want a distance of 600, it would be 600*600 = 360000. :) For 700, it would be 490000. Perhaps you should make the distance of "IsTargetFar" configurable.
  • You should make some of the struct members private. Whichever members should not be accessed or have method operators for should be private/readonly.
  • You use a timer per instance rather than just one timer per struct. I guess it is okay, it is up to you whether you want to recycle/use a timer system. I recommend using only one timer per struct though.
  • You destroy groups. You should recycle them instead. (see this tutorial for the reason why) You can use a library like recycle for it, or better yet you can just integrate that method into your code so that you recycle groups.
  • You should probably update UnitAlive to the information you got from the thread you made.

Eh, that's all for now. I'll post later after testing the map and get down to the nitty gritty optimizations. :)
 
Level 29
Joined
Mar 10, 2009
Messages
5,016
The distances, such as 700000 and 600000, are a bit off. If you want a distance of 600, it would be 600*600 = 360000. :) For 700, it would be 490000. Perhaps you should make the distance of "IsTargetFar" configurable.
Lolz what am I thinking?, I write down 600k coz I multiply it by 1000...really stupid...

You use a timer per instance rather than just one timer per struct. I guess it is okay, it is up to you whether you want to recycle/use a timer system. I recommend using only one timer per struct though.
I didnt do 1 timer per instance...Im using only 1 timer per struct, maybe you misread...

You destroy groups. You should recycle them instead. (see this tutorial for the reason why) You can use a library like recycle for it, or better yet you can just integrate that method into your code so that you recycle groups.
But if I do that, then I'll be stacking groups, I'm just trying to avoid what TimerUtils for example does...
Groups was not my first priority anyway, but the heroe's index is but I just decided to make it like that coz it's easier to order units...

Other things you suggested Ima do...
 
I didnt do 1 timer per instance...Im using only 1 timer per struct, maybe you misread...

Ah I see now. That is fine then.

But if I do that, then I'll be stacking groups, I'm just trying to avoid what TimerUtils for example does...
Groups was not my first priority anyway, but the heroe's index is but I just decided to make it like that coz it's easier to order units...

Stacking groups may end up being better. All recycling does is just grab a new group if there are any. If not, it will create a new one. When it is done being used, the group can be freed for later use. That way, you aren't using a bunch of groups. It is true that sometimes you'll have a bunch of groups lying around for no reason, but there will eventually be a break-even point where the RAM leaked from destroying the groups passes the amount of RAM needed to keep the groups there.

Either way it is of very little difference since any computer can handle that (even the oldest ones), but it is a good coding practice to recycle groups. :)
 
Top