• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Squad System v0.9.4

This bundle is marked as awaiting update. A staff member has requested changes to it before it can be approved.

Features​

  • Create squads that share orders
  • Train/hire squads
  • Selection reduced to one unit in the squad
  • Set squad leader
  • (optional) Share received damage across the squad
  • (optional) Create a special effect on each squad member, configurable per squad
  • (optional) Create a different special effect on the squad leader, configurable per squad
  • (optional) Chance to disband a squad when a member dies
  • (optional) Disband a squad when its leader dies
  • (optional) Surviving members flee when squad is disbanded

API​

JASS:
//===========================================================================
//
//  Squad System v0.9.4
//  by loktar and kokon
//
//    -------
//    * API *
//    -------
//    * Variables *
//    -------------
//  *   integer array squadUnits
//          - Unit types that generate a squad when trained or hired
//
//  *   integer array squadSize
//          - Squad size for squads generated with squadUnits
//
//  *   integer squadUnits_max
//          - Maximum index of squadUnits and squadSize
//
//  *   string squad_sfx_path
//          - Default path for special effect to attach to squad units
//          - Set to null to disable special effects
//          - Default: null
//
//  *   string squad_sfx_attach
//          - Default attachment point for special effect to attach to squad units
//          - Default: "overhead"
//
//  *   string squad_leader_sfx_path
//          - Default path for special effect to attach to squad leader
//          - Set to null to use same effect as regular squad members
//          - Default: null
//
//  *   string squad_leader_sfx_attach
//          - Default attachment point for special effect to attach to squad leader
//          - Default: "overhead"
//
//  *   integer squad_disband_multiplier
//          - Multiplier that determines the chance of a squad disbanding when one of its units dies
//          - Formula: squad_disband_multiplier/squad_size
//          - E.g. 100: 50% chance when 2 units left, 33% chance when 3 units left
//          - Set to 0 to disable disbanding
//          - Default: 0
//
//  *   real squad_flee_distance
//          - Distance units of a disbanded squad will flee in a random direction
//          - set to 0 to disable fleeing
//          - Default: 0
//
//  *   real squad_combat_overextension_distance
//          - Squad members will try to come closer to each other if they are too far away during comabat
//          - Default: 325
//            (+ 25 per unit)
//
//  *   boolean leader_death_disband
//          - If true, squads will disband if their leader dies
//          - Default: false
//
//  *   real squad_shared_damage_activation_chance
//          - Chance that part of the incoming attack damage is distributed between other squad members
//          - Default: 0.50 (50%)
//
//  *   real squad_max_percent_of_shared_damage
//          - Direct defender damage is reduced up to squad_max_percent_of_shared_damage
//          - Shared damage does not cause death. Only DAMAGE_TYPE_NORMAL is split
//          - Default: 0.50 (50%)
//
//  *   real squad_min_percent_of_shared_damage
//          - Default: 0.25 (25%)
//
//  *   real squad_min_amount_of_shared_damage
//          - If shared damage is less than squad_min_amount_of_shared_damage it will not be deducted from the original defender
//          - Default: 3
//
//    * Functions *
//    -------------
//  *   SquadAddUnit(group squad, unit whichUnit)
//          - Adds a unit to a squad
//          - Will remove the unit from any squad it is already in
//
//  *   SquadRemoveUnit(group squad, unit whichUnit)
//          - Removes a unit from a squad
//          - Destroys squad if only 1 or 0 members remain
//
//  *   SquadSetLeader(group squad, unit whichUnit)
//          - Sets the squad's leader
//          - Adds the unit to squad if not already a member
//
//  *   unit SquadGetLeader(group squad)
//          - Returns the leader of a squad
//
//  *   SquadSetSfx(group squad, string path, string attach)
//          - Sets the special effect to be attached to units of the squad
//          - Set to null to disable special effects for the squad
//
//  *   SquadSetLeaderSfx(group squad, string path, string attach)
//          - Sets the special effect to be attached to the leader of the squad
//          - Set to null to use same special effect as regular units
//
//  *   UnitCreateSquad(unit whichUnit)
//          - If the unit is one of the squadUnits types, creates a squad
//
//  *   group UnitTypeCreateSquad(player whichPlayer, integer unitid, integer size, real x, real y, real facing)
//          - Create a squad of unit type
//          - Returns the squad
//
//  *   DisbandSquad(group squad)
//          - Removes all units from the squad and destroys it
//          - Does not cause fleeing
//
//  *   group UnitGetSquad(unit whichUnit)
//          - Returns the unit's squad
//          - Returns null if unit doesn't have a squad
//
//  *   IndividualOrderTarget(unit whichUnit, string order, widget target)
//          - Issues target order to unit without ordering the squad
//
//  *   IndividualOrderImmediate(unit whichUnit, string order)
//          - Issues immediate order to unit without ordering the squad
//
//  *   IndividualOrderPoint(unit whichUnit, string order, real x, real y)
//          - Issues point order to unit without ordering the squad
//
//===========================================================================

Test map credits​

Ranks & Insignias by ILH


v0.9.4
  • Added shared damage feature (thanks kokon)
  • Fixed some leaks
v0.9.3
  • Squad will now only follow attacking members when distance exceeds squad_combat_overextension_distance
  • Added leader functionality
  • Some improvements to script efficiency
v0.9.2
  • Added functions for issuing orders to individual squad members
  • When a squad member auto-attacks a nearby enemy, its squad will now follow
v0.9.1
  • Fixed selection adjustment with shared unit control
Contents

Squad System (Map)

Reviews
Wrda
squad_sfx_path squad_sfx_attach squad_leader_sfx_path squad_leader_sfx_attach squad_combat_overextension_distance leader_death_disband squad_shared_damage_activation_chance squad_max_percent_of_shared_damage squad_min_percent_of_shared_damage These...
Fairly simple system. Other squad systems uploaded in Spells & Systems are pretty old, and potentially not compatible with latest patch, so I made a new one. I can add features / fix bugs as suggested :)

edit: btw, credit to kokon for some of the ideas, from their thread.
 
Last edited:
1. When one squad memeber engages via aggro. Other squad member should try to engage too.
When one of the ai squad units gets magnetized to nearby fighting, other squad units should go more closely to the magnetized unit.
Ie they should go more like a group that has an attack order. May be a point attack order.

2. Can we reduce agro radius for a squad? Ie enable defensive stance.
May be all units in a group should hold position except one unit. And when this unit starts moving hold position is cancelled for the rest.
Or hold position until enemy is really close and then issue an attack order.

3. Can I create a squad completely out of existing units? I could not find a way to do so.

4. It could be useful if I could order a unit individually in a special way via Jass, circumventing a group order.

PS:
Smth interesting and cool the I see as a positive sideffect is a crossplayer squad. I control ai units of my ally that are in my sqad
 
Last edited:
1. When one squad memeber engages via aggro. Other squad member should try to engage too.
When one of the ai squad units gets magnetized to nearby fighting, other squad units should go more closely to the magnetized unit.
Ie they should go more like a group that has an attack order. May be a point attack order.
Hmm, I'm not sure this is possible. Or at least, I don't know how to do it.

2. Can we reduce agro radius for a squad? Ie enable defensive stance.
May be all units in a group should hold position except one unit. And when this unit starts moving hold position is cancelled for the rest.
Or hold position until enemy is really close and then issue an attack order.
You can set the agro radius with this:
  • Unit - Set some_unit acquisition range to 200.00
or
JASS:
call SetUnitAcquireRange(some_unit, 200.00)
As for the rest, I'm again not sure if it's possible. The problem is that there is no event fired when a unit automatically does something (like attack nearby units). I'll try to come up with some "defense mode" stance, but I'm not sure I'll be able to do it.

3. Can I create a squad completely out of existing units? I could not find a way to do so.
Yes, you can do this with SquadAddUnit. I'll add an example in the map.

4. It could be useful if I could order a unit individually in a special way via Jass, circumventing a group order.
That's a good idea, I'm not 100% sure it's possible with how the script it written at the moment, but maybe I can find a way around it.

CountUnitsInGroup is it a wc3 native function btw? I see it in your library.
There is a BlzGroupGetSize that I usually use
Oh I didn't know that existed lol. CountUnitsInGroup is not a native, but it's a built-in blizzard function.
 
Last edited:
There can be a check if one group members does damage, another one should come closer if their distance is more than 350.
And there can be a periodic check say if group member has distance more than 400 the group should receive a group order (fix the formation)

6. If the strongest unit in a group dies. There can be a disband chance if the group is configured to have a leader.

>Oh I didn't know that existed lol. CountUnitsInGroup is not a native, but it's a built-in blizzard function.
It seem to be safe. Even if the group is null it will return 0
 
Last edited:
I added functions for giving orders to individual squad members. I also added a trigger when a unit is attacked by a squad member, its squad will be ordered to attack too.

6. If the strongest unit in a group dies. There can be a disband chance if the group is configured to have a leader.
Nice idea, I'll try to add it later. Maybe the squad leader can have a different special effect from the rest of the squad.
 
Code:
//===========================================================================
//
//  Squad System v0.9.3
//  by loktar
//
//    -------
//    * API *
//    -------
//    * Variables *
//    -------------
//  *   integer array squadUnits
//          - Unit types that generate a squad when trained or hired
//
//  *   integer array squadSize
//          - Squad size for squads generated with squadUnits
//
//  *   integer squadUnits_max
//          - Maximum index of squadUnits and squadSize
//
//  *   string squad_sfx_path
//          - Default path for special effect to attach to squad units
//          - Set to null to disable special effects
//          - Default: null
//
//  *   string squad_sfx_attach
//          - Default attachment point for special effect to attach to squad units
//          - Default: "overhead"
//
//  *   integer squad_disband_multiplier
//          - Multiplier that determines the chance of a squad disbanding when one of its units dies
//          - Formula: squad_disband_multiplier/squad_size
//          - Set to 0 to disable disbanding
//          - Default: 0
//
//  *   real squad_flee_distance
//          - Distance units of a disbanded squad will flee in a random direction
//          - set to 0 to disable fleeing
//          - Default: 0
//  *   real squad_combat_overextension_distance
//          - Squad members will try to come closer to each other if they are too far away during comabat
//          - Default: 325
//            (+ 25 per unit)
//
//    * Functions *
//    -------------
//  *   SquadAddUnit(group squad, unit whichUnit)
//          - Adds a unit to a squad
//          - Will remove the unit from any squad it is already in
//
//  *   SquadRemoveUnit(group squad, unit whichUnit)
//          - Removes a unit from a squad
//          - Destroys squad if only 1 or 0 members remain
//
//  *   SquadSetSfx(group squad, string path, string attach)
//          - Sets the special effect to be attached to units of the squad
//          - Set to null to disable special effects for the squad
//
//  *   UnitCreateSquad(unit whichUnit)
//          - If the unit is one of the squadUnits types, creates a squad
//
//  *   group UnitTypeCreateSquad(player whichPlayer, integer unitid, integer size, real x, real y, real facing)
//          - Create a squad of unit type
//          - Returns the squad
//
//  *   DisbandSquad(group squad)
//          - Removes all units from the squad and destroys it
//          - Does not cause fleeing
//
//  *   group UnitGetSquad(unit whichUnit)
//          - Returns the unit's squad
//          - Returns null if unit doesn't have a squad
//
//  *   IndividualOrderTarget(unit whichUnit, string order, widget target)
//          - Issues target order to unit without ordering the squad
//
//  *   IndividualOrderImmediate(unit whichUnit, string order)
//          - Issues immediate order to unit without ordering the squad
//
//  *   IndividualOrderPoint(unit whichUnit, string order, real x, real y)
//          - Issues point order to unit without ordering the squad
//
//===========================================================================
library SquadSystem initializer InitSquadSystem
    globals
        // Config
        integer array squadUnits
        integer array squadSize
        integer squadUnits_max = -1
        string squad_sfx_path = null
        string squad_sfx_attach = "overhead"
        integer squad_disband_multiplier = 0
        real squad_flee_distance = 0
        real squad_combat_overextension_distance = 325
        
        // Misc
        private group pauseSelection = CreateGroup()
        private group pauseGroupOrder = CreateGroup()
        
        // Unit hashtable
        private hashtable htbUnit = InitHashtable()
        private constant integer SQUAD = 0
        private constant integer SFX = 1
        private constant integer SELECTING_PLAYER = 2
        
        // Squad hashtable
        private hashtable htbSquad = InitHashtable()
        private constant integer SFX_PATH = 0
        private constant integer SFX_ATTACH = 1
    endglobals
    //=========================================================
    // Util
    //=========================================================
    private function PlayerHasControl takes unit whichUnit, player whichPlayer returns boolean
        local player owningPlayer = GetOwningPlayer(whichUnit)
        local boolean result = owningPlayer == whichPlayer or GetPlayerAlliance(owningPlayer, whichPlayer, ALLIANCE_SHARED_CONTROL)
        set owningPlayer = null
        return result
    endfunction
    
    
    function MaxDistanceBetweenUnitsInGroup takes group g returns real
        local unit u1
        local unit u2
        local real maxDistance = 0.0
        local real dx
        local real dy
        local real distance
        local integer i
        local integer j
        local integer groupSize = BlzGroupGetSize(g)
    
        if groupSize < 2 then
            return 0.0
        endif
    
        set i = 0
        loop
            exitwhen i >= groupSize - 1
            
            set u1 = BlzGroupUnitAt(g, i)
            set j = i + 1
            loop
                exitwhen j >= groupSize
                
                set u2 = BlzGroupUnitAt(g, j)
                
                set dx = GetUnitX(u2) - GetUnitX(u1)
                set dy = GetUnitY(u2) - GetUnitY(u1)
                set distance = SquareRoot(dx * dx + dy * dy)
                
                if distance > maxDistance then
                    set maxDistance = distance
                endif
                
                set j = j + 1
            endloop
            
            set i = i + 1
        endloop
    
        return maxDistance
    endfunction
    


    //=========================================================
    // Squad functions
    //=========================================================
    // Get a unit's squad
    function UnitGetSquad takes unit whichUnit returns group
        local integer handleId = GetHandleId(whichUnit)
        
        if HaveSavedHandle(htbUnit, handleId, SQUAD) then
            return LoadGroupHandle(htbUnit, handleId, SQUAD)
        endif
        
        return null
    endfunction
    
    // Remove unit from squad
    function SquadRemoveUnit takes group squad, unit whichUnit returns nothing
        local integer handleId = GetHandleId(whichUnit)
        local integer squad_size
        
        call GroupRemoveUnit(squad, whichUnit)
        set squad_size = BlzGroupGetSize(squad)
        
        if squad_size == 1 then // Remove last member if only 1 is left
            call SquadRemoveUnit(squad, FirstOfGroup(squad))
        elseif squad_size == 0 then // Destroy squad if empty
            call FlushChildHashtable(htbSquad, GetHandleId(squad))
            call DestroyGroup(squad)
        endif
        
        if HaveSavedHandle(htbUnit, handleId, SFX) then
            call DestroyEffect(LoadEffectHandle(htbUnit, handleId, SFX))
        endif
        
        call FlushChildHashtable(htbUnit, handleId)
    endfunction
    
    // Add a unit to a squad
    function SquadAddUnit takes group squad, unit whichUnit returns nothing
        local trigger trg
        local group oldSquad
        local integer unitHandleId = GetHandleId(whichUnit)
        local integer squadHandleId = GetHandleId(squad)
        local string sfx_path = null
        local string sfx_attach
        
        set oldSquad = UnitGetSquad(whichUnit)
        if oldSquad != null then
            call SquadRemoveUnit(oldSquad, whichUnit)
        endif
        
        call GroupAddUnit(squad, whichUnit)
        call SaveGroupHandle(htbUnit, unitHandleId, SQUAD, squad)
        
        // Get SFX path
        if HaveSavedString(htbSquad, squadHandleId, SFX_PATH) then
            set sfx_path = LoadStr(htbSquad, squadHandleId, SFX_PATH)
        else
            set sfx_path = squad_sfx_path
        endif
        // Get SFX attachment point
        if HaveSavedString(htbSquad, squadHandleId, SFX_ATTACH) then
            set sfx_attach = LoadStr(htbSquad, squadHandleId, SFX_ATTACH)
        else
            set sfx_attach = squad_sfx_attach
        endif
        
        // Add effect
        if sfx_path != null then
            call SaveEffectHandle(htbUnit, unitHandleId, SFX, AddSpecialEffectTarget(sfx_path, whichUnit, sfx_attach))
        endif
        
        set trg = null
        set oldSquad = null
    endfunction
    
    // Create squad from unit
    function UnitCreateSquad takes unit whichUnit returns nothing
        local group squad
        local player owningPlayer
        local integer typeId
        local real x
        local real y
        local real facing
        local boolean isSquadUnit = false
        local integer index = 0
        local integer size_index = 0
        
        if squadUnits_max >= 0 then
            set typeId = GetUnitTypeId(whichUnit)
            
            loop
                set isSquadUnit = squadUnits[index] == typeId
                set index = index + 1
                exitwhen index > squadUnits_max or isSquadUnit
            endloop
            
            if isSquadUnit then
                set squad = CreateGroup()
                set owningPlayer = GetOwningPlayer(whichUnit)
                set x = GetUnitX(whichUnit)
                set y = GetUnitY(whichUnit)
                set facing = GetUnitFacing(whichUnit)
                set size_index = index-1
                set index = 0
                
                call SquadAddUnit(squad, whichUnit)
                loop
                    call SquadAddUnit(squad, CreateUnit(owningPlayer, typeId, x, y, facing))
                    set index = index + 1
                    exitwhen index == squadSize[size_index]-1
                endloop
            endif
        endif
        
        set squad = null
        set owningPlayer = null
    endfunction
    
    // Create a squad with units of type for player
    function UnitTypeCreateSquad takes player whichPlayer, integer unitid, integer size, real x, real y, real facing returns group
        local group squad = CreateGroup()
        local integer index = 0
        
        loop
            call SquadAddUnit(squad, CreateUnit(whichPlayer, unitid, x, y, facing))
            set index = index + 1
            exitwhen index == size
        endloop
        
        return squad
    endfunction
    
    // Remove all units from squad
    function DisbandSquad takes group squad returns nothing
        local unit pickedUnit
        
        loop
            set pickedUnit = FirstOfGroup(squad)
            exitwhen pickedUnit == null
            call SquadRemoveUnit(squad, pickedUnit)
        endloop
        
        set pickedUnit = null
    endfunction
    
    // Destroy existing effect and create/save new one
    private function SetSfxEnum takes nothing returns nothing
        local unit enumUnit = GetEnumUnit()
        local integer unitHandleId = GetHandleId(enumUnit)
        local integer squadHandleId = GetHandleId(UnitGetSquad(enumUnit))
        local string path = LoadStr(htbSquad, squadHandleId, SFX_PATH)
        
        if HaveSavedHandle(htbUnit, unitHandleId, SFX) then
            call DestroyEffect(LoadEffectHandle(htbUnit, unitHandleId, SFX))
            call RemoveSavedHandle(htbUnit, unitHandleId, SFX)
        endif
        
        if path != null then
            call SaveEffectHandle(htbUnit, unitHandleId, SFX, AddSpecialEffectTarget(path, enumUnit, LoadStr(htbSquad, squadHandleId, SFX_ATTACH)))
        endif
        
        set enumUnit = null
    endfunction
    
    // Set the special effect for a squad
    function SquadSetSfx takes group squad, string path, string attach returns nothing
        local integer handleId = GetHandleId(squad)
        
        call SaveStr(htbSquad, handleId, SFX_PATH, path)
        call SaveStr(htbSquad, handleId, SFX_ATTACH, attach)
        
        call ForGroup(squad, function SetSfxEnum)
    endfunction
    
    // Issue target order to unit without ordering the squad
    function IndividualOrderTarget takes unit whichUnit, string order, widget target returns nothing
        call GroupAddUnit(pauseGroupOrder, whichUnit)
        call IssueTargetOrder(whichUnit, order, target)
        call GroupRemoveUnit(pauseGroupOrder, whichUnit)
    endfunction
    
    // Issue immediate order to unit without ordering the squad
    function IndividualOrderImmediate takes unit whichUnit, string order returns nothing
        call GroupAddUnit(pauseGroupOrder, whichUnit)
        call IssueImmediateOrder(whichUnit, order)
        call GroupRemoveUnit(pauseGroupOrder, whichUnit)
    endfunction
    
    // Issue point order to unit without ordering the squad
    function IndividualOrderPoint takes unit whichUnit, string order, real x, real y returns nothing
        call GroupAddUnit(pauseGroupOrder, whichUnit)
        call IssuePointOrder(whichUnit, order, x, y)
        call GroupRemoveUnit(pauseGroupOrder, whichUnit)
    endfunction
    
    //=========================================================
    // Triggers
    //=========================================================
    // Unit sold: create squad
    private function UnitSold takes nothing returns boolean
        call UnitCreateSquad(GetSoldUnit())
        return false
    endfunction
    
    // Unit sold: create squad
    private function UnitTrained takes nothing returns boolean
        call UnitCreateSquad(GetTrainedUnit())
        return false
    endfunction
    
    // Unit selected: modify selection
    private function UnitSelected takes nothing returns nothing
        local unit triggerUnit = GetTriggerUnit()
        local player triggerPlayer = GetTriggerPlayer()
        local group squad = UnitGetSquad(triggerUnit)
        local group grp
        local unit pickedUnit
        
        // Only proceed if unit has squad and player has control over unit
        if squad != null and not IsUnitInGroup(triggerUnit, pauseSelection) and PlayerHasControl(triggerUnit, triggerPlayer) then
            call GroupAddGroup(squad, pauseSelection)
            
            set grp = CreateGroup()
            call GroupAddGroup(squad, grp)
            loop
                set pickedUnit = FirstOfGroup(grp)
                exitwhen pickedUnit == null
                call GroupRemoveUnit(grp, pickedUnit)
                if pickedUnit != triggerUnit then
                    call SelectUnitRemoveForPlayer(pickedUnit, triggerPlayer)
                endif
            endloop
            call DestroyGroup(grp)
            
            call TriggerSleepAction(0.01) // this is necessary to prevent all selected squad units from firing the event
            call GroupRemoveGroup(squad, pauseSelection)
        endif
        
        set triggerUnit = null
        set triggerPlayer = null
        set squad = null
        set grp = null
        set pickedUnit = null
    endfunction
    
    // Unit target order: order squad
    private function SquadTargetOrder takes nothing returns boolean
        local trigger trg
        local string order
        local unit orderedUnit = GetOrderedUnit()
        local unit targetUnit
        local group squad = UnitGetSquad(orderedUnit)
        
        if squad != null and not IsUnitInGroup(orderedUnit, pauseGroupOrder) then
            set trg = GetTriggeringTrigger()
            set order = OrderId2String(GetIssuedOrderId())
            set targetUnit = GetOrderTargetUnit()
            
            call DisableTrigger(trg) // Prevent infinite loop
            // smart order on friendly unit doesn't work
            if order == "smart" and targetUnit != null and IsPlayerAlly(GetOwningPlayer(orderedUnit), GetOwningPlayer(targetUnit)) then
                call GroupPointOrder(squad, order, GetUnitX(targetUnit), GetUnitY(targetUnit))
            else
                call GroupTargetOrder(squad, order, GetOrderTarget())
            endif
            call EnableTrigger(trg)
        endif
        
        set trg = null
        set orderedUnit = null
        set targetUnit = null
        set squad = null
        return false
    endfunction
    
    // Unit immediate order: order squad
    private function SquadImmediateOrder takes nothing returns boolean
        local trigger trg
        local unit orderedUnit = GetOrderedUnit()
        local group squad = UnitGetSquad(orderedUnit)
        
        if squad != null and not IsUnitInGroup(orderedUnit, pauseGroupOrder) then
            set trg = GetTriggeringTrigger()
            call DisableTrigger(trg) // Prevent infinite loop
            call GroupImmediateOrder(squad, OrderId2String(GetIssuedOrderId()))
            call EnableTrigger(trg)
        endif
        
        set trg = null
        set orderedUnit = null
        set squad = null
        return false
    endfunction
    
    // Unit point order: order squad
    private function SquadPointOrder takes nothing returns boolean
        local trigger trg
        local unit orderedUnit = GetOrderedUnit()
        local group squad = UnitGetSquad(orderedUnit)
        
        if squad != null and not IsUnitInGroup(orderedUnit, pauseGroupOrder) then
            set trg = GetTriggeringTrigger()
            call DisableTrigger(trg) // Prevent infinite loop
            call GroupPointOrder(squad, OrderId2String(GetIssuedOrderId()), GetOrderPointX(), GetOrderPointY())
            call EnableTrigger(trg)
        endif
        
        set trg = null
        set orderedUnit = null
        set squad = null
        return false
    endfunction
    
    // Make enumerated units flee to random point
    private function FleeEnum takes nothing returns nothing
        local unit enumUnit = GetEnumUnit()
        local real x = GetRandomReal(-squad_flee_distance, squad_flee_distance)
        local real y = SquareRoot(Pow(squad_flee_distance, 2) - Pow(x, 2))
        
        // y is always positive, allow for negative value
        if GetRandomInt(0, 1) == 0 then
            set y = -y
        endif
        
        call IssuePointOrder(enumUnit, "move", GetUnitX(enumUnit)+x, GetUnitY(enumUnit)+y)
    endfunction
    
    // Unit dies: remove unit from squad and adjust selection
    private function UnitDies takes nothing returns boolean
        local unit dyingUnit = GetDyingUnit()
        local group squad = UnitGetSquad(dyingUnit)
        local group grp
        local player indexPlayer
        local integer squad_size
        local integer index = 0
        
        if squad != null then
            set squad_size = BlzGroupGetSize(squad)
            
            // Chance the squad disbands
            if squad_disband_multiplier/(squad_size-1) >= GetRandomInt(1, 100) then
                if squad_flee_distance > 0 then
                    set grp = CreateGroup()
                    call GroupAddGroup(squad, grp)
                    call GroupRemoveUnit(grp, dyingUnit)
                endif
                
                call DisbandSquad(squad)
                
                if squad_flee_distance > 0 then
                    call ForGroup(grp, function FleeEnum)
                    call DestroyGroup(grp)
                endif
                
            // Adjust selection and remove unit from squad
            else
                if squad_size > 1 then
                    loop
                        set indexPlayer = Player(index)
                        
                        if IsUnitSelected(dyingUnit, indexPlayer) and PlayerHasControl(dyingUnit, indexPlayer) then
                            set grp = CreateGroup()
                            call GroupAddGroup(squad, grp)
                            call GroupRemoveUnit(grp, dyingUnit)
                            call SelectUnitAddForPlayer(FirstOfGroup(grp), indexPlayer)
                            call DestroyGroup(grp)
                        endif
                        
                        set index = index + 1
                        exitwhen index == bj_MAX_PLAYER_SLOTS
                    endloop
                endif
                
                call SquadRemoveUnit(squad, dyingUnit)
            endif
        endif
        
        set dyingUnit = null
        set squad = null
        set grp = null
        set indexPlayer = null
        return false
    endfunction
    
    // Unit is attacked: make sure attacking unit's squad follows through
    private function UnitAttacked takes nothing returns boolean
        local group squad = UnitGetSquad(GetAttacker())
        
        if squad != null then
            if MaxDistanceBetweenUnitsInGroup(squad) > squad_combat_overextension_distance + BlzGroupGetSize(squad) * 25 then
                call GroupTargetOrder(squad, "attack", GetTriggerUnit())
               //call GroupPointOrder(squad, "attack", GetUnitX(GetTriggerUnit()), GetUnitY(GetTriggerUnit()))
            endif
        endif
        
        set squad = null
        
        return false
    endfunction
    //=========================================================
    // Initializer
    //=========================================================
    private function InitSquadSystem takes nothing returns nothing
        local trigger trgUnitSold = CreateTrigger()
        local trigger trgUnitTrained = CreateTrigger()
        local trigger trgUnitSelected = CreateTrigger()
        local trigger trgSquadTargetOrder = CreateTrigger()
        local trigger trgSquadImmediateOrder = CreateTrigger()
        local trigger trgSquadPointOrder = CreateTrigger()
        local trigger trgUnitDies = CreateTrigger()
        local trigger trgUnitAttacked = CreateTrigger()
        local player indexPlayer
        local integer index = 0
        
        loop
            set indexPlayer = Player(index)
            call TriggerRegisterPlayerUnitEvent(trgUnitSold, indexPlayer, EVENT_PLAYER_UNIT_SELL, null)
            call TriggerRegisterPlayerUnitEvent(trgUnitTrained, indexPlayer, EVENT_PLAYER_UNIT_TRAIN_FINISH, null)
            call TriggerRegisterPlayerUnitEvent(trgUnitSelected, indexPlayer, EVENT_PLAYER_UNIT_SELECTED, null)
            call TriggerRegisterPlayerUnitEvent(trgSquadTargetOrder, indexPlayer, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
            call TriggerRegisterPlayerUnitEvent(trgSquadImmediateOrder, indexPlayer, EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
            call TriggerRegisterPlayerUnitEvent(trgSquadPointOrder, indexPlayer, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
            call TriggerRegisterPlayerUnitEvent(trgUnitDies, indexPlayer, EVENT_PLAYER_UNIT_DEATH, null)
            call TriggerRegisterPlayerUnitEvent(trgUnitAttacked, indexPlayer, EVENT_PLAYER_UNIT_ATTACKED, null)
            set index = index + 1
            exitwhen index == bj_MAX_PLAYER_SLOTS
        endloop
        
        call TriggerAddCondition(trgUnitSold, function UnitSold)
        call TriggerAddCondition(trgUnitTrained, function UnitTrained)
        call TriggerAddAction(trgUnitSelected, function UnitSelected)
        call TriggerAddCondition(trgSquadTargetOrder, function SquadTargetOrder)
        call TriggerAddCondition(trgSquadImmediateOrder, function SquadImmediateOrder)
        call TriggerAddCondition(trgSquadPointOrder, function SquadPointOrder)
        call TriggerAddCondition(trgUnitDies, function UnitDies)
        call TriggerAddCondition(trgUnitAttacked, function UnitAttacked)
        
        set trgUnitSold = null
        set trgUnitTrained = null
        set trgUnitSelected = null
        set trgSquadTargetOrder = null
        set trgSquadImmediateOrder = null
        set trgSquadPointOrder = null
        set trgUnitDies = null
        set trgUnitAttacked = null
        set indexPlayer = null
    endfunction
endlibrary



@loktar I've added
squad_combat_overextension_distance

attack order should not be spammed on every attack. it should only be triggered if overextension is in place
 
Last edited:
attack order should not be spammed on every attack. it should only be triggered if overextension is in place
Ah yes, I can check for distance when the trigger fires.
How do you think we attach and remove buffs from squad members?
Ie Squad membership can provide additional regen and attack speed for example.
It's not possible to add buffs through triggers/scripts, only option is to add an aura ability, but that will also give buff to nearby non-squad units. Alternatively I can increase stats manually, but then there is no buff.
 
Ah yes, I can check for distance when the trigger fires.

It's not possible to add buffs through triggers/scripts, only option is to add an aura ability, but that will also give buff to nearby non-squad units. Alternatively I can increase stats manually, but then there is no buff.
MaxDistanceBetweenUnitsInGroup already there.

There is a NewBonus library that is relatively good. I mean there should be a callback prbably when joining the squad and especially when the sqad is disbanded...
 
@loktar I've added
squad_combat_overextension_distance

attack order should not be spammed on every attack. it should only be triggered if overextension is in place
Added it to the script. Also added Leader functionality. Squad can disband/flee when leader dies, leader can have different special effect, and leader will be the one selected when selecting multiple units of a squad (including leader).

edit: hmmmmmm there's a bug where if you select the squad leader and then select a different unit in the squad, it will deselect the unit.
edit2: fixed
 
I thought about dungeon crawler formation. Where melee units go as one group and ranged and caster unit subgroup follow that group.
Can be an optional formation. Or ranged units follow melee units individually. But i think we need group disbanded callback first. Btw i'm also wc1/2 fan. Wc1 had 4 units in a "squad"
 
Last edited:
I've added a feature that allows to split portion of incoming damage between squad members.
The more squad member is damaged the smaller chance that he will help to absorb part of the damage.
It can prolong the life of individual squad members a little.

New config options:

Code:
//  *   real squad_shared_damage_activation_chance
//          - Chance that part of the incoming attack damage is distributed between other squad members
//          - Defailt: 0.25 (25%)
//  *   real squad_max_percent_of_shared_damage
//          . Direct defender damage is reduced up to squad_max_percent_of_shared_damage
//          - Shared damage does not cause death. Only DAMAGE_TYPE_NORMAL is split
//          - Default: 0.50 (50%)
//  *   real squad_min_percent_of_shared_damage
//          - Defailt: 0.40 (40%)
//  *   real squad_min_amount_of_shared_damage
//          - If shared damage is less than squad_min_amount_of_shared_damage it will not be deducted from the original defender
//          - Default: 3

Main lib file


Code:
//===========================================================================
//
//  Squad System v0.9.4
//  by loktar
//  and kokon
//
//    -------
//    * API *
//    -------
//    * Variables *
//    -------------
//  *   integer array squadUnits
//          - Unit types that generate a squad when trained or hired
//
//  *   integer array squadSize
//          - Squad size for squads generated with squadUnits
//
//  *   integer squadUnits_max
//          - Maximum index of squadUnits and squadSize
//
//  *   string squad_sfx_path
//          - Default path for special effect to attach to squad units
//          - Set to null to disable special effects
//          - Default: null
//
//  *   string squad_sfx_attach
//          - Default attachment point for special effect to attach to squad units
//          - Default: "overhead"
//
//  *   integer squad_disband_multiplier
//          - Multiplier that determines the chance of a squad disbanding when one of its units dies
//          - Formula: squad_disband_multiplier/squad_size
//          - Set to 0 to disable disbanding
//          - Default: 0
//
//  *   real squad_flee_distance
//          - Distance units of a disbanded squad will flee in a random direction
//          - set to 0 to disable fleeing
//          - Default: 0
//  *   real squad_combat_overextension_distance
//          - Squad members will try to come closer to each other
//          - Default: 300
//            (+ 30 per unit)
//  *   real squad_shared_damage_activation_chance
//          - Chance that part of the incoming attack damage is distributed between other squad members
//          - Defailt: 0.50 (50%)
//  *   real squad_max_percent_of_shared_damage
//          . Direct defender damage is reduced up to squad_max_percent_of_shared_damage
//          - Shared damage does not cause death. Only DAMAGE_TYPE_NORMAL is split
//          - Default: 0.50 (50%)
//  *   real squad_min_percent_of_shared_damage
//          - Defailt: 0.25 (25%)
//  *   real squad_min_amount_of_shared_damage
//          - If shared damage is less than squad_min_amount_of_shared_damage it will not be deducted from the original defender
//          - Default: 3
//
//    * Functions *
//    -------------
//  *   SquadAddUnit(group squad, unit whichUnit)
//          - Adds a unit to a squad
//          - Will remove the unit from any squad it is already in
//
//  *   SquadRemoveUnit(group squad, unit whichUnit)
//          - Removes a unit from a squad
//          - Destroys squad if only 1 or 0 members remain
//
//  *   SquadSetSfx(group squad, string path, string attach)
//          - Sets the special effect to be attached to units of the squad
//          - Set to null to disable special effects for the squad
//
//  *   UnitCreateSquad(unit whichUnit)
//          - If the unit is one of the squadUnits types, creates a squad
//
//  *   group UnitTypeCreateSquad(player whichPlayer, integer unitid, integer size, real x, real y, real facing)
//          - Create a squad of unit type
//          - Returns the squad
//
//  *   DisbandSquad(group squad)
//          - Removes all units from the squad and destroys it
//          - Does not cause fleeing
//
//  *   group UnitGetSquad(unit whichUnit)
//          - Returns the unit's squad
//          - Returns null if unit doesn't have a squad
//
//  *   IndividualOrderTarget(unit whichUnit, string order, widget target)
//          - Issues target order to unit without ordering the squad
//
//  *   IndividualOrderImmediate(unit whichUnit, string order)
//          - Issues immediate order to unit without ordering the squad
//
//  *   IndividualOrderPoint(unit whichUnit, string order, real x, real y)
//          - Issues point order to unit without ordering the squad
//
//===========================================================================
library SquadSystem initializer InitSquadSystem
    globals
        // Config
        integer array squadUnits
        integer array squadSize
        integer squadUnits_max = -1
        string squad_sfx_path = null
        string squad_sfx_attach = "overhead"
        integer squad_disband_multiplier = 0
        real squad_flee_distance = 0
        real squad_combat_overextension_distance = 300.0
        real squad_shared_damage_activation_chance = 0.5
        real squad_max_percent_of_shared_damage = 0.5
        real squad_min_percent_of_shared_damage = 0.25
        real squad_min_amount_of_shared_damage = 3.0
  
        // Misc
        private group pauseSelection = CreateGroup()
        private group pauseGroupOrder = CreateGroup()
  
        // Unit hashtable
        private hashtable htbUnit = InitHashtable()
        private constant integer SQUAD = 0
        private constant integer SFX = 1
        private constant integer SELECTING_PLAYER = 2
  
        // Squad hashtable
        private hashtable htbSquad = InitHashtable()
        private constant integer SFX_PATH = 0
        private constant integer SFX_ATTACH = 1
    endglobals
    //=========================================================
    // Util
    //=========================================================
    private function PlayerHasControl takes unit whichUnit, player whichPlayer returns boolean
        local player owningPlayer = GetOwningPlayer(whichUnit)
        local boolean result = owningPlayer == whichPlayer or GetPlayerAlliance(owningPlayer, whichPlayer, ALLIANCE_SHARED_CONTROL)
        set owningPlayer = null
        return result
    endfunction
 
 
    function MaxDistanceBetweenUnitsInGroup takes group g returns real
        local unit u1
        local unit u2
        local real maxDistance = 0.0
        local real dx
        local real dy
        local real distance
        local integer i
        local integer j
        local integer groupSize = BlzGroupGetSize(g)
 
        if groupSize < 2 then
            return 0.0
        endif
 
        set i = 0
        loop
            exitwhen i >= groupSize - 1
      
            set u1 = BlzGroupUnitAt(g, i)
            set j = i + 1
            loop
                exitwhen j >= groupSize
          
                set u2 = BlzGroupUnitAt(g, j)
          
                set dx = GetUnitX(u2) -GetUnitX(u1)
                set dy = GetUnitY(u2) -GetUnitY(u1)
                set distance = SquareRoot(dx * dx + dy * dy)
          
                if distance > maxDistance then
                    set maxDistance = distance
                endif
          
                set j = j + 1
            endloop
      
            set i = i + 1
        endloop
 
        return maxDistance
    endfunction
 
    function GetIndexOfLowestHPUnit takes group g returns integer
        local integer lowestIndex = -1
        local real lowestHP = 99999999.0
        local integer i = 0
        local integer groupSize = BlzGroupGetSize(g)
        local unit u
        local real hp
 
        loop
            exitwhen i >= groupSize
            set u = BlzGroupUnitAt(g, i)
            set hp = GetUnitState(u, UNIT_STATE_LIFE)
      
            if hp < lowestHP then
                set lowestHP = hp
                set lowestIndex = i
            endif
      
            set i = i + 1
        endloop
 
        set u = null
        return lowestIndex
    endfunction
    private function isRangedUnit takes unit u returns boolean
        if BlzGetUnitWeaponRealField(u, UNIT_WEAPON_RF_ATTACK_RANGE, 0) > 200 or BlzGetUnitWeaponRealField(u, UNIT_WEAPON_RF_ATTACK_RANGE, 1) > 200 then
            return true
        endif
        return false
    endfunction
    private function GetAverageHPInGroup takes group g returns real
        local integer groupSize = BlzGroupGetSize(g)
        local integer i = 0
        local real totalHP = 0.0
        local unit u
 
        if groupSize == 0 then
            return 0.0
        endif
 
        loop
            exitwhen i >= groupSize
            set u = BlzGroupUnitAt(g, i)
            set totalHP = totalHP + GetUnitState(u, UNIT_STATE_LIFE)
            set i = i + 1
        endloop
 
        set u = null
        return totalHP / groupSize
    endfunction

    //=========================================================
    // Squad functions
    //=========================================================
    // Get a unit's squad
    function UnitGetSquad takes unit whichUnit returns group
        local integer handleId = GetHandleId(whichUnit)
  
        if HaveSavedHandle(htbUnit, handleId, SQUAD) then
            return LoadGroupHandle(htbUnit, handleId, SQUAD)
        endif
  
        return null
    endfunction
 
    // Remove unit from squad
    function SquadRemoveUnit takes group squad, unit whichUnit returns nothing
        local integer handleId = GetHandleId(whichUnit)
        local integer squad_size
  
        call GroupRemoveUnit(squad, whichUnit)
        set squad_size = BlzGroupGetSize(squad)
  
        if squad_size == 1 then // Remove last member if only 1 is left
            call SquadRemoveUnit(squad, FirstOfGroup(squad))
        elseif squad_size == 0 then // Destroy squad if empty
            call FlushChildHashtable(htbSquad, GetHandleId(squad))
            call DestroyGroup(squad)
        endif
  
        if HaveSavedHandle(htbUnit, handleId, SFX) then
            call DestroyEffect(LoadEffectHandle(htbUnit, handleId, SFX))
        endif
  
        call FlushChildHashtable(htbUnit, handleId)
    endfunction
 
    // Add a unit to a squad
    function SquadAddUnit takes group squad, unit whichUnit returns nothing
        local trigger trg
        local group oldSquad
        local integer unitHandleId = GetHandleId(whichUnit)
        local integer squadHandleId = GetHandleId(squad)
        local string sfx_path = null
        local string sfx_attach
  
        set oldSquad = UnitGetSquad(whichUnit)
        if oldSquad != null then
            call SquadRemoveUnit(oldSquad, whichUnit)
        endif
  
        call GroupAddUnit(squad, whichUnit)
        call SaveGroupHandle(htbUnit, unitHandleId, SQUAD, squad)
  
        // Get SFX path
        if HaveSavedString(htbSquad, squadHandleId, SFX_PATH) then
            set sfx_path = LoadStr(htbSquad, squadHandleId, SFX_PATH)
        else
            set sfx_path = squad_sfx_path
        endif
        // Get SFX attachment point
        if HaveSavedString(htbSquad, squadHandleId, SFX_ATTACH) then
            set sfx_attach = LoadStr(htbSquad, squadHandleId, SFX_ATTACH)
        else
            set sfx_attach = squad_sfx_attach
        endif
  
        // Add effect
        if sfx_path != null then
            call SaveEffectHandle(htbUnit, unitHandleId, SFX, AddSpecialEffectTarget(sfx_path, whichUnit, sfx_attach))
        endif
  
        set trg = null
        set oldSquad = null
    endfunction
 
    // Create squad from unit
    function UnitCreateSquad takes unit whichUnit returns nothing
        local group squad
        local player owningPlayer
        local integer typeId
        local real x
        local real y
        local real facing
        local boolean isSquadUnit = false
        local integer index = 0
        local integer size_index = 0
  
        if squadUnits_max >= 0 then
            set typeId = GetUnitTypeId(whichUnit)
      
            loop
                set isSquadUnit = squadUnits[index] == typeId
                set index = index + 1
                exitwhen index > squadUnits_max or isSquadUnit
            endloop
      
            if isSquadUnit then
                set squad = CreateGroup()
                set owningPlayer = GetOwningPlayer(whichUnit)
                set x = GetUnitX(whichUnit)
                set y = GetUnitY(whichUnit)
                set facing = GetUnitFacing(whichUnit)
                set size_index = index - 1
                set index = 0
          
                call SquadAddUnit(squad, whichUnit)
                loop
                    call SquadAddUnit(squad, CreateUnit(owningPlayer, typeId, x, y, facing))
                    set index = index + 1
                    exitwhen index == squadSize[size_index] -1
                endloop
            endif
        endif
  
        set squad = null
        set owningPlayer = null
    endfunction
 
    // Create a squad with units of type for player
    function UnitTypeCreateSquad takes player whichPlayer, integer unitid, integer size, real x, real y, real facing returns group
        local group squad = CreateGroup()
        local integer index = 0
  
        loop
            call SquadAddUnit(squad, CreateUnit(whichPlayer, unitid, x, y, facing))
            set index = index + 1
            exitwhen index == size
        endloop
  
        return squad
    endfunction
 
    // Remove all units from squad
    function DisbandSquad takes group squad returns nothing
        local unit pickedUnit
  
        loop
            set pickedUnit = FirstOfGroup(squad)
            exitwhen pickedUnit == null
            call SquadRemoveUnit(squad, pickedUnit)
        endloop
  
        set pickedUnit = null
    endfunction
 
    // Destroy existing effect and create/save new one
    private function SetSfxEnum takes nothing returns nothing
        local unit enumUnit = GetEnumUnit()
        local integer unitHandleId = GetHandleId(enumUnit)
        local integer squadHandleId = GetHandleId(UnitGetSquad(enumUnit))
        local string path = LoadStr(htbSquad, squadHandleId, SFX_PATH)
  
        if HaveSavedHandle(htbUnit, unitHandleId, SFX) then
            call DestroyEffect(LoadEffectHandle(htbUnit, unitHandleId, SFX))
            call RemoveSavedHandle(htbUnit, unitHandleId, SFX)
        endif
  
        if path != null then
            call SaveEffectHandle(htbUnit, unitHandleId, SFX, AddSpecialEffectTarget(path, enumUnit, LoadStr(htbSquad, squadHandleId, SFX_ATTACH)))
        endif
  
        set enumUnit = null
    endfunction
 
    // Set the special effect for a squad
    function SquadSetSfx takes group squad, string path, string attach returns nothing
        local integer handleId = GetHandleId(squad)
  
        call SaveStr(htbSquad, handleId, SFX_PATH, path)
        call SaveStr(htbSquad, handleId, SFX_ATTACH, attach)
  
        call ForGroup(squad, function SetSfxEnum)
    endfunction
 
    // Issue target order to unit without ordering the squad
    function IndividualOrderTarget takes unit whichUnit, string order, widget target returns nothing
        call GroupAddUnit(pauseGroupOrder, whichUnit)
        call IssueTargetOrder(whichUnit, order, target)
        call GroupRemoveUnit(pauseGroupOrder, whichUnit)
    endfunction
 
    // Issue immediate order to unit without ordering the squad
    function IndividualOrderImmediate takes unit whichUnit, string order returns nothing
        call GroupAddUnit(pauseGroupOrder, whichUnit)
        call IssueImmediateOrder(whichUnit, order)
        call GroupRemoveUnit(pauseGroupOrder, whichUnit)
    endfunction
 
    // Issue point order to unit without ordering the squad
    function IndividualOrderPoint takes unit whichUnit, string order, real x, real y returns nothing
        call GroupAddUnit(pauseGroupOrder, whichUnit)
        call IssuePointOrder(whichUnit, order, x, y)
        call GroupRemoveUnit(pauseGroupOrder, whichUnit)
    endfunction
 
    //=========================================================
    // Triggers
    //=========================================================
    // Unit sold: create squad
    private function UnitSold takes nothing returns boolean
        call UnitCreateSquad(GetSoldUnit())
        return false
    endfunction
 
    // Unit sold: create squad
    private function UnitTrained takes nothing returns boolean
        call UnitCreateSquad(GetTrainedUnit())
        return false
    endfunction
 
    // Unit selected: modify selection
    private function UnitSelected takes nothing returns nothing
        local unit triggerUnit = GetTriggerUnit()
        local player triggerPlayer = GetTriggerPlayer()
        local group squad = UnitGetSquad(triggerUnit)
        local group grp
        local unit pickedUnit
  
        // Only proceed if unit has squad and player has control over unit
        if squad != null and not IsUnitInGroup(triggerUnit, pauseSelection) and PlayerHasControl(triggerUnit, triggerPlayer) then
            call GroupAddGroup(squad, pauseSelection)
      
            set grp = CreateGroup()
            call GroupAddGroup(squad, grp)
            loop
                set pickedUnit = FirstOfGroup(grp)
                exitwhen pickedUnit == null
                call GroupRemoveUnit(grp, pickedUnit)
                if pickedUnit != triggerUnit then
                    call SelectUnitRemoveForPlayer(pickedUnit, triggerPlayer)
                endif
            endloop
            call DestroyGroup(grp)
      
            call TriggerSleepAction(0.01) // this is necessary to prevent all selected squad units from firing the event
            call GroupRemoveGroup(squad, pauseSelection)
        endif
  
        set triggerUnit = null
        set triggerPlayer = null
        set squad = null
        set grp = null
        set pickedUnit = null
    endfunction
 
    // Unit target order: order squad
    private function SquadTargetOrder takes nothing returns boolean
        local trigger trg
        local string order
        local unit orderedUnit = GetOrderedUnit()
        local unit targetUnit
        local group squad = UnitGetSquad(orderedUnit)
  
        if squad != null and not IsUnitInGroup(orderedUnit, pauseGroupOrder) then
            set trg = GetTriggeringTrigger()
            set order = OrderId2String(GetIssuedOrderId())
            set targetUnit = GetOrderTargetUnit()
      
            call DisableTrigger(trg) // Prevent infinite loop
            // smart order on friendly unit doesn't work
            if order == "smart" and targetUnit != null and IsPlayerAlly(GetOwningPlayer(orderedUnit), GetOwningPlayer(targetUnit)) then
                call GroupPointOrder(squad, order, GetUnitX(targetUnit), GetUnitY(targetUnit))
            else
                call GroupTargetOrder(squad, order, GetOrderTarget())
            endif
            call EnableTrigger(trg)
        endif
  
        set trg = null
        set orderedUnit = null
        set targetUnit = null
        set squad = null
        return false
    endfunction
 
    // Unit immediate order: order squad
    private function SquadImmediateOrder takes nothing returns boolean
        local trigger trg
        local unit orderedUnit = GetOrderedUnit()
        local group squad = UnitGetSquad(orderedUnit)
  
        if squad != null and not IsUnitInGroup(orderedUnit, pauseGroupOrder) then
            set trg = GetTriggeringTrigger()
            call DisableTrigger(trg) // Prevent infinite loop
            call GroupImmediateOrder(squad, OrderId2String(GetIssuedOrderId()))
            call EnableTrigger(trg)
        endif
  
        set trg = null
        set orderedUnit = null
        set squad = null
        return false
    endfunction
 
    // Unit point order: order squad
    private function SquadPointOrder takes nothing returns boolean
        local trigger trg
        local unit orderedUnit = GetOrderedUnit()
        local group squad = UnitGetSquad(orderedUnit)
  
        if squad != null and not IsUnitInGroup(orderedUnit, pauseGroupOrder) then
            set trg = GetTriggeringTrigger()
            call DisableTrigger(trg) // Prevent infinite loop
            call GroupPointOrder(squad, OrderId2String(GetIssuedOrderId()), GetOrderPointX(), GetOrderPointY())
            call EnableTrigger(trg)
        endif
  
        set trg = null
        set orderedUnit = null
        set squad = null
        return false
    endfunction
 
    // Make enumerated units flee to random point
    private function FleeEnum takes nothing returns nothing
        local unit enumUnit = GetEnumUnit()
        local real x = GetRandomReal(-squad_flee_distance, squad_flee_distance)
        local real y = SquareRoot(Pow(squad_flee_distance, 2) -Pow(x, 2))
  
        // y is always positive, allow for negative value
        if GetRandomInt(0, 1) == 0 then
            set y = -y
        endif
  
        call IssuePointOrder(enumUnit, "move", GetUnitX(enumUnit) + x, GetUnitY(enumUnit) + y)
    endfunction
 
    // Unit dies: remove unit from squad and adjust selection
    private function UnitDies takes nothing returns boolean
        local unit dyingUnit = GetDyingUnit()
        local group squad = UnitGetSquad(dyingUnit)
        local group grp
        local player indexPlayer
        local integer squad_size
        local integer index = 0
  
        if squad != null then
            set squad_size = BlzGroupGetSize(squad)
      
            // Chance the squad disbands
            if squad_disband_multiplier / (squad_size - 1) >= GetRandomInt(1, 100) then
                if squad_flee_distance > 0 then
                    set grp = CreateGroup()
                    call GroupAddGroup(squad, grp)
                    call GroupRemoveUnit(grp, dyingUnit)
                endif
          
                call DisbandSquad(squad)
          
                if squad_flee_distance > 0 then
                    call ForGroup(grp, function FleeEnum)
                    call DestroyGroup(grp)
                endif
          
                // Adjust selection and remove unit from squad
            else
                if squad_size > 1 then
                    loop
                        set indexPlayer = Player(index)
                  
                        if IsUnitSelected(dyingUnit, indexPlayer) and PlayerHasControl(dyingUnit, indexPlayer) then
                            set grp = CreateGroup()
                            call GroupAddGroup(squad, grp)
                            call GroupRemoveUnit(grp, dyingUnit)
                            call SelectUnitAddForPlayer(FirstOfGroup(grp), indexPlayer)
                            call DestroyGroup(grp)
                        endif
                  
                        set index = index + 1
                        exitwhen index == bj_MAX_PLAYER_SLOTS
                    endloop
                endif
          
                call SquadRemoveUnit(squad, dyingUnit)
            endif
        endif
  
        set dyingUnit = null
        set squad = null
        set grp = null
        set indexPlayer = null
        return false
    endfunction

    private function UnitDamaged takes nothing returns nothing
        local integer i = 0
        local unit defender = GetTriggerUnit()
        local group squad = UnitGetSquad(defender)
        local integer groupSize = BlzGroupGetSize(squad)
        local real sharedDamage
        local real ratio
        local real squadUnit = 0
        local integer currentIndex
        local integer safetyIndex = 0
        local real skipChance = 0.0
        local unit currentUnit
        local integer splitCount
        local real averageHpInSquad
        local real defenderLife
        local integer appliedDamageSplitCount = 0
        if groupSize < 2 then
            return
        endif
        if BlzGetEventDamageType() != DAMAGE_TYPE_NORMAL then
            return
        endif
        if GetRandomReal(0, 1) < squad_shared_damage_activation_chance then
            return
        endif
        set defenderLife = GetUnitState(defender, UNIT_STATE_LIFE)
        set averageHpInSquad = GetAverageHPInGroup(squad)
        if defenderLife >= averageHpInSquad * 1.333 then
            return
        elseif defenderLife >= averageHpInSquad * 1.1 and GetRandomInt(0,2) == 2 then
            return
        endif
        set ratio = GetRandomReal(squad_min_percent_of_shared_damage, squad_max_percent_of_shared_damage)
        set sharedDamage = GetEventDamage() * ratio
        if sharedDamage >= squad_min_amount_of_shared_damage and defenderLife > 0 then
            set splitCount = groupSize - 1
            if (splitCount >= 2) then
                set splitCount = 2 - GetRandomInt(0, 1)
            endif
            loop
                exitwhen appliedDamageSplitCount >= splitCount
                set currentIndex = GetRandomInt(0, groupSize - 1)
                set currentUnit = BlzGroupUnitAt(squad, currentIndex)
                if currentUnit != defender and GetUnitState(currentUnit, UNIT_STATE_LIFE) > sharedDamage then
                    set skipChance = (1 - GetUnitState(currentUnit, UNIT_STATE_LIFE) / GetUnitState(currentUnit, UNIT_STATE_MAX_LIFE))
                    if GetIndexOfLowestHPUnit(squad) == currentIndex then
                        set skipChance = skipChance + 0.20
                    endif
                    if averageHpInSquad > GetUnitState(currentUnit, UNIT_STATE_LIFE) * 0.9 then
                        set skipChance = skipChance + 0.20
                    endif
                    if defenderLife > GetUnitState(currentUnit, UNIT_STATE_LIFE) then
                        set skipChance = skipChance + 0.20
                    endif
                    if isRangedUnit(currentUnit) == true then
                        set skipChance = skipChance + 0.20
                    endif
                    if GetRandomReal(0, 1.0) > skipChance then
                        call UnitDamageTarget(GetEventDamageSource(), currentUnit, sharedDamage / splitCount * 0.99, true, false, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_UNKNOWN, WEAPON_TYPE_WHOKNOWS)
                        set appliedDamageSplitCount = appliedDamageSplitCount + 1
                    endif
                endif
                set safetyIndex = safetyIndex + 1
                exitwhen safetyIndex >= 12
            endloop
            if appliedDamageSplitCount > 0 then
                call SetUnitState(defender, UNIT_STATE_LIFE, defenderLife + sharedDamage)
            endif
        endif
  
        set squad = null
    endfunction
    // Unit is attacked: make sure attacking unit's squad follows through
    private function UnitAttacked takes nothing returns boolean
        local group squad = UnitGetSquad(GetAttacker())
  
        if squad != null then
            if MaxDistanceBetweenUnitsInGroup(squad) > squad_combat_overextension_distance + BlzGroupGetSize(squad) * 25 then
                call GroupTargetOrder(squad, "attack", GetTriggerUnit())
                //call GroupPointOrder(squad, "attack", GetUnitX(GetTriggerUnit()), GetUnitY(GetTriggerUnit()))
            endif
        endif
  
        set squad = null
  
        return false
    endfunction
    //=========================================================
    // Initializer
    //=========================================================
    private function InitSquadSystem takes nothing returns nothing
        local trigger trgUnitSold = CreateTrigger()
        local trigger trgUnitTrained = CreateTrigger()
        local trigger trgUnitSelected = CreateTrigger()
        local trigger trgSquadTargetOrder = CreateTrigger()
        local trigger trgSquadImmediateOrder = CreateTrigger()
        local trigger trgSquadPointOrder = CreateTrigger()
        local trigger trgUnitDies = CreateTrigger()
        local trigger trgUnitAttacked = CreateTrigger()
        local trigger trgUnitDamaging = CreateTrigger()
        local player indexPlayer
        local integer index = 0
  
        loop
            set indexPlayer = Player(index)
            call TriggerRegisterPlayerUnitEvent(trgUnitSold, indexPlayer, EVENT_PLAYER_UNIT_SELL, null)
            call TriggerRegisterPlayerUnitEvent(trgUnitTrained, indexPlayer, EVENT_PLAYER_UNIT_TRAIN_FINISH, null)
            call TriggerRegisterPlayerUnitEvent(trgUnitSelected, indexPlayer, EVENT_PLAYER_UNIT_SELECTED, null)
            call TriggerRegisterPlayerUnitEvent(trgSquadTargetOrder, indexPlayer, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
            call TriggerRegisterPlayerUnitEvent(trgSquadImmediateOrder, indexPlayer, EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
            call TriggerRegisterPlayerUnitEvent(trgSquadPointOrder, indexPlayer, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
            call TriggerRegisterPlayerUnitEvent(trgUnitDies, indexPlayer, EVENT_PLAYER_UNIT_DEATH, null)
            call TriggerRegisterPlayerUnitEvent(trgUnitAttacked, indexPlayer, EVENT_PLAYER_UNIT_ATTACKED, null)
            call TriggerRegisterPlayerUnitEvent(trgUnitDamaging, indexPlayer, EVENT_PLAYER_UNIT_DAMAGED, null)
            set index = index + 1
            exitwhen index == bj_MAX_PLAYER_SLOTS
        endloop
  
        call TriggerAddCondition(trgUnitSold, function UnitSold)
        call TriggerAddCondition(trgUnitTrained, function UnitTrained)
        call TriggerAddAction(trgUnitSelected, function UnitSelected)
        call TriggerAddCondition(trgSquadTargetOrder, function SquadTargetOrder)
        call TriggerAddCondition(trgSquadImmediateOrder, function SquadImmediateOrder)
        call TriggerAddCondition(trgSquadPointOrder, function SquadPointOrder)
        call TriggerAddCondition(trgUnitDies, function UnitDies)
        call TriggerAddCondition(trgUnitAttacked, function UnitAttacked)
        call TriggerAddAction(trgUnitDamaging, function UnitDamaged)
  
        set trgUnitSold = null
        set trgUnitTrained = null
        set trgUnitSelected = null
        set trgSquadTargetOrder = null
        set trgSquadImmediateOrder = null
        set trgSquadPointOrder = null
        set trgUnitDies = null
        set trgUnitAttacked = null
        set indexPlayer = null
    endfunction
endlibrary
 
Last edited:
I've added a feature that allows to split portion of incoming damage between squad members.
Cool, I'll add it to the bundle and add you as author if you don't mind.
Edit: I fixed some memory leaks btw, and made the new trigger a Condition instead of Action (this is slightly more efficient).

Can you add an option to turn formation on/off for a squad btw?
The formation is done automatically with object editor value Stats - Formation Rank. I don't think it's possible to turn this off through triggers.
 
Last edited:
The formation is done automatically with object editor value Stats - Formation Rank. I don't think it's possible to turn this off through triggers.

May be there should be an API function that commands all units of the group with a certain command but each of them recieve this order separately.
So just to have an option to move group in a non-organized way.

P.S.:
// - E.g. 100: 50% chance when 2 units left, 33% chance when 3 units left

my reforged editor breaks on it there is a broken symbol on this line or smth
 
Last edited:

SpasMaster

Hosted Project: SC
Level 24
Joined
Jan 29, 2010
Messages
1,986
Greetings.

This looks like a neat little system you guys are cooking here.
I stumbled upon it when looking for a "pet" or "follower" system.

What you have done so far will satisfy my needs, but there is something that I'd personally look for and make as a suggestion:

In a "Hunter + Pet" type of setup, I'd like to have the Pet(s) always follow the orders and commands of the Hunter, but never the reverse.
What are your thoughts on having a boolean flag that allows for a configuration where the Squad Members always follow the Leader's orders, but the Leader never follows the Squad Members?

Cheers!
 
May be there should be an API function that commands all units of the group with a certain command but each of them recieve this order separately.
So just to have an option to move group in a non-organized way.
Hm, should be doable. Loop through the group and give them separate orders.

P.S.:
// - E.g. 100: 50% chance when 2 units left, 33% chance when 3 units left

my reforged editor breaks on it there is a broken symbol on this line or smth
Weird, it works fine for me. Maybe it's the % sign?

In a "Hunter + Pet" type of setup, I'd like to have the Pet(s) always follow the orders and commands of the Hunter, but never the reverse.
What are your thoughts on having a boolean flag that allows for a configuration where the Squad Members always follow the Leader's orders, but the Leader never follows the Squad Members?
Yeah shouldn't be a problem. I am worried that there are starting to be too many config variables haha.

I'll try to add these this week.
 
Here is the 0.9.5 version.
Squads now have a better handling of sticking together if some part of the group aggro to an enemy. There were several problems with archers/healers/ranged before.
 

Attachments

  • SquadSystem v0.9.5.w3m
    42.4 KB · Views: 0
Last edited:

Wrda

Spell Reviewer
Level 28
Joined
Nov 18, 2012
Messages
1,993
JASS:
squad_sfx_path
squad_sfx_attach
squad_leader_sfx_path
squad_leader_sfx_attach
squad_combat_overextension_distance
leader_death_disband
squad_shared_damage_activation_chance
squad_max_percent_of_shared_damage
squad_min_percent_of_shared_damage
These appear to act like constants. Why are some set in the library and some on the example script ?
Please follow the JPAG convention, constants must be capitalized.
Regarding the configuration of the variables, I think it's better if you put the documentation matching each variable instead of being completely separated. The functions' documentation are fine and may stay as they are.

JASS:
squadUnits
squadSize
squadUnits_max
They're global variables that are supposed to be accessed by the user, better capitalize the first letter of each word, and decide if you want squadUnitsMax or the other two with underscore.

JASS:
// By kokon
    function MaxDistanceBetweenUnitsInGroup takes group g returns real
        local unit u1
        local unit u2
        local real maxDistance = 0.0
        local real dx
        local real dy
        local real distance
        local integer i
        local integer j
        local integer groupSize = BlzGroupGetSize(g)
    
        if groupSize < 2 then
            return 0.0
        endif
    
        set i = 0
        loop
            exitwhen i >= groupSize - 1
            
            set u1 = BlzGroupUnitAt(g, i)
            set j = i + 1
            loop
                exitwhen j >= groupSize
                
                set u2 = BlzGroupUnitAt(g, j)
                
                set dx = GetUnitX(u2) - GetUnitX(u1)
                set dy = GetUnitY(u2) - GetUnitY(u1)
                set distance = SquareRoot(dx * dx + dy * dy)
                
                if distance > maxDistance then
                    set maxDistance = distance
                endif
                
                set j = j + 1
            endloop
            
            set i = i + 1
        endloop
        
        set u1 = null
        set u2 = null
    
        return maxDistance
    endfunction
Missing -1 in exitwhen j >= groupSize


JASS:
// By kokon
    function GetIndexOfLowestHPUnit takes group g returns integer
        local integer lowestIndex = -1
        local real lowestHP = 99999999.0
        local integer i = 0
        local integer groupSize = BlzGroupGetSize(g)
        local unit u
        local real hp
 
        loop
            exitwhen i >= groupSize
            set u = BlzGroupUnitAt(g, i)
            set hp = GetUnitState(u, UNIT_STATE_LIFE)
      
            if hp < lowestHP then
                set lowestHP = hp
                set lowestIndex = i
            endif
      
            set i = i + 1
        endloop
 
        set u = null
        return lowestIndex
    endfunction
Again, missing -1 in exitwhen i >= groupSize



JASS:
// By kokon
    private function GetAverageHPInGroup takes group g returns real
        local integer groupSize = BlzGroupGetSize(g)
        local integer i = 0
        local real totalHP = 0.0
        local unit u
 
        if groupSize == 0 then
            return 0.0
        endif
 
        loop
            exitwhen i >= groupSize
            set u = BlzGroupUnitAt(g, i)
            set totalHP = totalHP + GetUnitState(u, UNIT_STATE_LIFE)
            set i = i + 1
        endloop
 
        set u = null
        return totalHP / groupSize
    endfunction
Again, missing -1 in exitwhen i >= groupSize


JASS:
// By kokon
    private function IsRangedUnit takes unit u returns boolean
        if BlzGetUnitWeaponRealField(u, UNIT_WEAPON_RF_ATTACK_RANGE, 0) > 200 or BlzGetUnitWeaponRealField(u, UNIT_WEAPON_RF_ATTACK_RANGE, 1) > 200 then
            return true
        endif
        return false
    endfunction
->
JASS:
j// By kokon
    private function IsRangedUnit takes unit u returns boolean
        return BlzGetUnitWeaponRealField(u, UNIT_WEAPON_RF_ATTACK_RANGE, 0) > 200 or BlzGetUnitWeaponRealField(u, UNIT_WEAPON_RF_ATTACK_RANGE, 1) > 200
    endfunction

JASS:
// Add leader to squad
    function SquadSetLeader takes group squad, unit whichUnit returns nothing
        local string sfx_path
        local string sfx_attach
        local integer squadHandleId = GetHandleId(squad)
        local integer unitHandleId = GetHandleId(whichUnit)
        
        call SaveUnitHandle(htbSquad, squadHandleId, LEADER, whichUnit)
        
        if not IsUnitInGroup(whichUnit, squad) then
            call SquadAddUnit(squad, whichUnit)
        endif
        
        // Get SFX path
        if HaveSavedString(htbSquad, squadHandleId, LEADER_SFX_PATH) then
            set sfx_path = LoadStr(htbSquad, squadHandleId, LEADER_SFX_PATH)
        else
            set sfx_path = squad_leader_sfx_path
        endif
        // Get SFX attachment point
        if HaveSavedString(htbSquad, squadHandleId, LEADER_SFX_ATTACH) then
            set sfx_attach = LoadStr(htbSquad, squadHandleId, LEADER_SFX_ATTACH)
        else
            set sfx_attach = squad_leader_sfx_attach
        endif
        
        // Add/replace effect
        if sfx_path != null then
            if HaveSavedHandle(htbUnit, unitHandleId, SFX) then
                call DestroyEffect(LoadEffectHandle(htbUnit, unitHandleId, SFX))
                call RemoveSavedHandle(htbUnit, unitHandleId, SFX)
            endif
            call SaveEffectHandle(htbUnit, unitHandleId, SFX, AddSpecialEffectTarget(sfx_path, whichUnit, sfx_attach))
        endif
    endfunction
This doesn't update the special effects of the previous leader.

UnitCreateSquad function doesn't return a "squad" group when it is expected to.


JASS:
// Unit selected: modify selection
    private function UnitSelected takes nothing returns nothing
        local unit triggerUnit = GetTriggerUnit()
        local player triggerPlayer = GetTriggerPlayer()
        local group squad = UnitGetSquad(triggerUnit)
        local unit pickedUnit
        local unit leader
        local integer squad_size
        local integer index = 0
        
        // Only proceed if unit has squad and player has control over unit
        if squad != null and not IsUnitInGroup(triggerUnit, pauseSelection) and PlayerHasControl(triggerUnit, triggerPlayer) then
            call TriggerSleepAction(0.01) // this is necessary to update selection in case leader was previously selected
            call GroupAddGroup(squad, pauseSelection)
            
            // If leader is selected, set triggerUnit to leader
            set leader = SquadGetLeader(squad)
            if leader != null and IsUnitSelected(leader, triggerPlayer) then
                set triggerUnit = leader
            endif
            
            set squad_size = BlzGroupGetSize(squad)
            loop
                exitwhen index >= squad_size
                
                set pickedUnit = BlzGroupUnitAt(squad, index)
                if pickedUnit != triggerUnit then
                    call SelectUnitRemoveForPlayer(pickedUnit, triggerPlayer)
                endif
                
                set index = index + 1
            endloop
            
            call TriggerSleepAction(0.01) // this is necessary to prevent all selected squad units from firing the event
            call GroupRemoveGroup(squad, pauseSelection)
        endif
        
        set triggerUnit = null
        set triggerPlayer = null
        set squad = null
        set pickedUnit = null
        set leader = null
    endfunction
use BlzGroupAddGroupFast instead of GroupAddGroup, and BlzGroupRemoveGroupFast instead of GroupRemoveGroup.
exitwhen index >= squad_size must have -1. The group index starts at 0.

local real y = SquareRoot(Pow(squad_flee_distance, 2) - Pow(x, 2))[/CODE][CODE]
Don't use Pow, just SquareRoot(squad_flee_distance*squad_flee_distance - x*x)


JASS:
// Unit dies: remove unit from squad and adjust selection
    private function UnitDies takes nothing returns boolean
        local unit dyingUnit = GetDyingUnit()
        local group squad = UnitGetSquad(dyingUnit)
        local group squad_copy
        local player indexPlayer
        local boolean isLeader
        local integer squad_size
        local integer index = 0
        
        if squad != null then
            set squad_size = BlzGroupGetSize(squad)-1
            
            if squad_size > 0 then
                // Get a copy of the squad in case only 1 unit is left and it is disbanded
                set squad_copy = CreateGroup()
                call GroupAddGroup(squad, squad_copy)
                call GroupRemoveUnit(squad_copy, dyingUnit)
                set isLeader = dyingUnit == SquadGetLeader(squad)
                
                // Remove the unit from squad
                call SquadRemoveUnit(squad, dyingUnit)
                
                // Chance the squad disbands
                if squad_disband_multiplier/squad_size >= GetRandomInt(1, 100) or (leader_death_disband and isLeader) then
                    if squad_flee_distance > 0 then
                        call GroupAddGroup(squad_copy, pauseGroupOrder)
                        call ForGroup(squad_copy, function FleeEnum)
                        call GroupRemoveGroup(squad_copy, pauseGroupOrder)
                    endif
                    
                    call DisbandSquad(squad_copy)
                    
                // Adjust selection
                else
                    loop
                        set indexPlayer = Player(index)
                        
                        if IsUnitSelected(dyingUnit, indexPlayer) and PlayerHasControl(dyingUnit, indexPlayer) then
                            call SelectUnitAddForPlayer(FirstOfGroup(squad_copy), indexPlayer)
                        endif
                        
                        set index = index + 1
                        exitwhen index == bj_MAX_PLAYER_SLOTS
                    endloop
                endif
            endif
        endif
        
        set dyingUnit = null
        set squad = null
        set squad_copy = null
        set indexPlayer = null
        return false
    endfunction

You don't need a squad_copy variable, just SquadRemoveUnit after you find out its leader.
Currently leaks squad_copy on the else part.

UnitDamaged function:
if defenderLife >= averageHpInSquad * 1.333 then
Would be nice to be configurable, in addition, things like skipChance = skipChance + 0.20. SkipAmount or whatever you want you call it.
jelseif defenderLife >= averageHpInSquad * 1.1 and GetRandomInt(0,2) == 2 then
I'm not exactly sure what this line is supposed to do? Similar to the previous, but triggers only with 33% chance?
sharedDamage / splitCount * 0.99 what's up with 0.99?


The squad is a nice idea with these features, it's a little over the place and a few things not so explanatory.
Nice collaboration, keep it up!
 
exitwhen i >= groupSize - 1

No there should be no -1 in any of the places you mentioned...
Will check the rest a bit later.

Intermediate ver is on the github. See i want to put j files as txt files.
But i cant figure out how to make it work with relative paths. Include seem to work with absolute paths only.
 
Last edited:
Top