• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Detachments v1.2.0.0

This system is a combination of concepts from games Battle for Middle Earth and Company of Heroes.
It allows users to group unit types into lined up detachments or squads.
Every detachment is led by a leader and its members follow him/her unquestionably. Direct order issues to detachment members are rejected and the only way to control them is by controlling the leader so it acts as a whole group.
When near friendly building, the leader can reinforce the detachment by replacing missing members.
If the leader dies, his/her role is taken over by the first member available.

Code

Credits

How to use

JASS:
library Detachments /* v1.2.0.0
****************************************************************************************
*                         
*                     Detachments by Garfield1337
*
*    Allows grouping unit types into lined up detachments or squads.
*
****************************************************************************************
*
*    */uses/*
*        */ AutoIndex               /*    wc3c.net/showthread.php?t=105643
*        */ Table                   /*    hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
*        */ SimError                /*    wc3c.net/showthread.php?t=101260
*        */ TimerUtils              /*    wc3c.net/showthread.php?t=101322
*        */ II                      /*    hiveworkshop.com/forums/pastebin.php?id=x178a8
*    */optional/*
*        */ RegisterPlayerUnitEvent /*    hiveworkshop.com/forums/jass-resources-412/snippet-registerplayerunitevent-203338/
*
****************************************************************************************
*
*    How to use:
*    ---------- 
*
*    For each detachment type...
*    - Create a leading and a member unit type (possibly based on same unit, but using different model.)
*    - Give member units ward classification.
*    - Create a spell for transforming leader into member using bear form, you can copy the spell from this map.
*      In this spell, change the fields "Alternate Form Unit" and "Normal Form Unit" to member and leader unit type respectively.
*    - Create a spell for reinforcing the detachment, you can copy the spell from this map.
*      Other than aesthetics and text fields, nothing needs change here.
*    - Create a spell for canceling training, you can copy the spell from this map.
*      Same as for reinforcing, there are no important field changes here.
*    - Initialize detachments using functions from the API.
*
*    Tips:
*    - When the leader dies, a member turns into a leader and takes over. For balance, leader and members should have
*      same or similar stats.
*    - Giving detachment units smaller collision than usual will help them move more freely and not bump one into another.
*    - Members mimic the leader; if the leader casts a spell, they will too if they have it.
*
*    API:
*    ----
*
*    To set up a detachment type, call the following function (Soldiers are detachment members.):
*    
*    function InitDetachment takes integer LeaderRawCode, integer SoldierRawCode, integer ReinforceRawcode,
*       integer TransformRawcode, integer SoldierGoldCost, integer SoldierLumberCost, integer SoldierFoodCost, integer TrainTime
*        
*    You may optionally alter detachment size settings with the following function,
*    otherwise the detachment will use default values:
*    
*    function SetDetachmentSize takes integer LeaderRawCode, integer MaximumMemberCount, integer SoldiersPerRow
*                                     real VerticalSpacing*, real HorizontalSpacing*
*
*                                     *Spacing determines distance between soldiers
*
****************************************************************************************
*
*                                     Settings
*/
    globals
        private constant integer CANCEL                     = 'A004' //Rawcode of the cancel ability.
        private constant string BAR                         = "||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||"
        private constant string BAR_COLOR                   = "|c00006988" //Color code for the training bar.
        private constant real TRAIN_RANGE                   = 600.0 //Range from the nearest friendly building at which a unit will finish training.
        private constant real ARRIVAL_RANGE                 = 200.0 //How close will the newly trained unit to the leader.
        private constant real WANDER_DISTANCE               = 900.0 //How far may a unit go away from the leader before automatically going back.
        private constant integer DEFAULT_SIZE               = 12 //Default detachment size; can be changed per detachment on initialization.
        private constant integer DEFAULT_SOLDIERS_PER_ROW   = 4 //Member count in every row
        private constant real DEFAULT_VERTICAL_SPACING      = 150.0 //Distance between soldiers; Vertical is away from leader.
        private constant real DEFAULT_HORIZONTAL_SPACING    = 140.0 //Distance between soldiers; Horizontal is between soldiers in a row.
        private constant string ERROR_DETACHMENT_FULL       = "Detachment is full." //Error messages
        private constant string ERROR_INSUFFICIENT_GOLD     = "Not enough gold."
        private constant string ERROR_INSUFFICIENT_LUMBER   = "Not enough lumber."
        private constant string ERROR_INSUFFICIENT_FOOD     = "Not enough food."
        private group enumGroup                             = CreateGroup() //Group used for dynamic enumeration
    endglobals                                                              //If there is a global group with such role present in map, assign it here

/****************************************************************************************/
    
    globals
        private TableArray detachmentData
        private constant real BAR_CONSTANT = StringLength(BAR)/32.00
    endglobals
    
    function InitDetachment takes integer leader, integer soldier, integer reinforce, integer transform, integer gold, integer lumber, integer food, integer time returns nothing
        //Saving detachment type data into table
        set detachmentData[0].boolean[leader] = true
        set detachmentData[1].boolean[soldier] = true
        set detachmentData[2].boolean[reinforce] = true
        set detachmentData[0][leader] = soldier
        set detachmentData[1][leader] = transform
        set detachmentData[2][leader] = gold
        set detachmentData[3][leader] = lumber
        set detachmentData[4][leader] = food
        set detachmentData[5][leader] = time
        
        set detachmentData[6][leader] = DEFAULT_SIZE
        set detachmentData[7][leader] = DEFAULT_SOLDIERS_PER_ROW
        set detachmentData[0].real[leader] = DEFAULT_VERTICAL_SPACING
        set detachmentData[1].real[leader] = DEFAULT_HORIZONTAL_SPACING
    endfunction
    
    function SetDetachmentSize takes integer leader, integer size, integer row, real vS, real hS returns nothing
        //Additional detachment settings
        set detachmentData[6][leader] = size
        set detachmentData[7][leader] = row
        set detachmentData[0].real[leader] = vS
        set detachmentData[1].real[leader] = hS
    endfunction
    
    //Functions for retrieving detachment data
    
    private function isLeader takes unit u returns boolean
        return detachmentData[0].boolean[GetUnitTypeId(u)]
    endfunction
    
    private function isSoldier takes unit u returns boolean
        return detachmentData[1].boolean[GetUnitTypeId(u)]
    endfunction
    
    private function isReinforce takes integer a returns boolean
        return detachmentData[2].boolean[a]
    endfunction
    
    private function soldierCode takes integer l returns integer
        return detachmentData[0][l]
    endfunction
    
    private function transformSoldier takes unit s, integer l returns nothing
        call UnitAddAbility(s, detachmentData[1][l])
        call UnitRemoveAbility(s, detachmentData[1][l])
    endfunction
    
    private function soldierGoldCost takes integer l returns integer
        return detachmentData[2][l]
    endfunction
    
    private function soldierLumberCost takes integer l returns integer
        return detachmentData[3][l]
    endfunction
    
    private function soldierFoodCost takes integer l returns integer
        return detachmentData[4][l]
    endfunction
    
    private function soldierTrainTime takes integer l returns integer
        return detachmentData[5][l]
    endfunction
    
    private function detachmentSize takes integer l returns integer
        return detachmentData[6][l]
    endfunction
    
    private function detachmentSPR takes integer l returns integer
        return detachmentData[7][l]
    endfunction
    
    private function vSpacing takes integer l returns real
        return detachmentData[0].real[l]
    endfunction
    
    private function hSpacing takes integer l returns real
        return detachmentData[1].real[l]
    endfunction
    
    private keyword Leader
    
    private struct Soldier
        //Soldier struct instances are attached to members of each detachment
        
        //It holds pointers to next and previous soldiers in the linked list and its parent Leader struct
        thistype next = 0
        thistype prev = 0
        Leader leader
        
        //These fields are used to prevent orders issued by player and resume previous one
        integer orderId = OrderId("stop")
        boolean orderPoint = false
        real orderX = 0.00
        real orderY = 0.00
        widget orderTarget = null
        static boolean checkOrder = true //If true, the orders will be resumed (prevents infinite loops)
        
        static method order takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            set .checkOrder = false
            call IssueImmediateOrderById(.me, .orderId)
            set .checkOrder = true
            call ReleaseTimer(GetExpiredTimer())
        endmethod
        
        method resumeOrder takes nothing returns nothing
            set .checkOrder = false
            if .orderPoint then
                call IssuePointOrderById(.me, .orderId, .orderX, .orderY)
            elseif .orderTarget != null then
                call IssueTargetOrderById(.me, .orderId, .orderTarget)
            else
                call TimerStart(NewTimerEx(this), 0.00, false, function thistype.order)
            endif
            set .checkOrder = true
        endmethod
        
        static method create takes unit s, Leader l returns thistype
            local thistype this = thistype.allocate()
            set .me = s
            set .leader = l
            return this
        endmethod
        
        //AutoData is neeeded to attach structs to units
        implement AutoData
        
        method remove takes nothing returns nothing
            //On removal, the two adjacent nodes in the list are joined together
            set .next.prev = .prev
            set .prev.next = .next
            set .leader.memberCount = .leader.memberCount - 1
            if this == .leader.first then
                //If the dying member is first in the list, the next member becomes first
                set .leader.first = .next
            endif
            call .destroy()
        endmethod
        
    endstruct

    private struct Leader
        //Leader struct instances are attached to leading units
        
        integer memberCount = 0
        Soldier first = 0 //First member in the list from which the iteration begins
        
        //Fields used during training
        boolean training = false
        texttag tag
        integer trainCount = 0
        string bar
        string filled
        real trainSpeed
        real trainProgress = 0.00
        
        //Although these can be retrieved anytime, since they're used in calculations, for optimization, they're saved here
        integer spr
        real vDistance
        real hDistance
        
        static method createFilter takes unit u returns boolean
            return isLeader(u) //Unit of leading type that enter map are automatically assigned a Leader struct instance
        endmethod
        
        method iterate1 takes nothing returns nothing
            //This method is executed every second and is used to check if a member wanders far off
            local Soldier soldier = .first //Starting for first soldier in the list
            local real lx = GetUnitX(.me)
            local real ly = GetUnitY(.me)
            local real a
            loop
                exitwhen soldier == 0 //When hitting the end of the list, the loop finishes
                if not IsUnitInRange(soldier.me, .me, WANDER_DISTANCE) then
                    //If the member is too far, he/she is issued an order to go back
                    set a = Atan2(GetUnitY(soldier.me) - ly, GetUnitX(soldier.me) - lx)
                    set Soldier.checkOrder = false //To prevent infinite loop
                    set soldier.orderId = OrderId("move")
                    set soldier.orderPoint = true
                    set soldier.orderX = lx + ARRIVAL_RANGE*Cos(a)
                    set soldier.orderY = ly + ARRIVAL_RANGE*Sin(a)
                    set soldier.orderTarget = null
                    call IssuePointOrderById(soldier.me, soldier.orderId, soldier.orderX, soldier.orderY)
                    set Soldier.checkOrder = true
                endif
                set soldier = soldier.next
            endloop
        endmethod
        
        static method create takes unit u returns thistype
            local thistype this = thistype.allocate()
            local integer id = GetUnitTypeId(u)
            set .spr = detachmentSPR(id)
            set .vDistance = vSpacing(id)
            set .hDistance = hSpacing(id)
            set .trainSpeed = BAR_CONSTANT/soldierTrainTime(id)
            call .start1() //Starts the one second interval iteration to prevent wandering
            return this
        endmethod
        
        static player owner
        static unit building
        static real distance
        static thistype dis
        
        static method filter takes nothing returns boolean
            //This method is used in a unit enumeration to find the nearest friendly building
            local unit f = GetFilterUnit()
            local real x
            local real y
            local real d
            local thistype this = .dis
            if IsUnitType(f, UNIT_TYPE_STRUCTURE) and IsUnitAlly(f, .owner) then
                set x = GetUnitX(.me) - GetUnitX(f)
                set y = GetUnitY(.me) - GetUnitY(f)
                set d = SquareRoot(x*x + y*y) 
                if d < .distance then
                    set .building = f
                    set .distance = d
                endif
            endif
            set f = null
            return false
        endmethod
        
        method iterate32 takes nothing returns nothing
            //This method is executed 32 times a second and handles training bars
            local Soldier new
            local real a
            call SetTextTagPos(.tag, GetUnitX(.me) - 100.0, GetUnitY(.me), GetUnitFlyHeight(.me) + 160.0)
            if .bar == "" then //When the bar finishes, the training is done
                set .owner = GetOwningPlayer(.me)
                set .building = null
                set .distance = TRAIN_RANGE + 100.0
                set .dis = this
                //Finding nearest friendly building
                call GroupEnumUnitsInRange(enumGroup, GetUnitX(.me), GetUnitY(.me), TRAIN_RANGE, Filter(function thistype.filter))
                if .building != null then //If there is one
                    //A unit is trained
                    set .trainCount = .trainCount - 1
                    call SetPlayerState(.owner, PLAYER_STATE_RESOURCE_FOOD_USED, GetPlayerState(.owner, PLAYER_STATE_RESOURCE_FOOD_USED) - soldierFoodCost(GetUnitTypeId(.me)))
                    set new = Soldier.create(CreateUnit(.owner, soldierCode(GetUnitTypeId(.me)), GetUnitX(.building), GetUnitY(.building), 270.0), this)
                    //This unit becomes the first in the list
                    set new.next = .first
                    set new.prev = 0
                    set .first.prev = new
                    set .first = new
                    set .memberCount = .memberCount + 1
                    set a = Atan2(GetUnitY(new.me) - GetUnitY(.me), GetUnitX(new.me) - GetUnitX(.me))
                    set Soldier.checkOrder = false
                    //Unit is issued to move close to the leader
                    set new.orderId = OrderId("move")
                    set new.orderPoint = true
                    set new.orderX = GetUnitX(.me) + ARRIVAL_RANGE*Cos(a)
                    set new.orderY = GetUnitY(.me) + ARRIVAL_RANGE*Sin(a)
                    call IssuePointOrderById(new.me, new.orderId, new.orderX, new.orderY)
                    set Soldier.checkOrder = true
                    if .trainCount == 0 then
                        //If all the training is done, the periodic execution is stopped and bar removed
                        call .stop32()
                        call DestroyTextTag(.tag)
                        set .training = false
                    else
                        //Otherwise it starts a new bar
                        set .bar = BAR
                        set .filled = "|r"
                        set .trainProgress = 0.00
                        call SetTextTagText(.tag, BAR_COLOR+I2S(.trainCount)+" : "+.filled+.bar, 0.021)
                    endif
                endif
            else
                //The training bar consists of two strings, a filled bar (.filled) and empty, original bar (.bar)
                //Progress consists of extending the filled bar and shortening the original one
                set .trainProgress = .trainProgress + .trainSpeed
                if .trainProgress >= 1.00 then
                    set .trainProgress = 0.00
                    set .filled = SubString(.filled, 0, StringLength(.filled) - 2) + "||" + "|r"
                    set .bar = SubString(.bar, 0, StringLength(.bar) - 2)
                    call SetTextTagText(.tag, BAR_COLOR+I2S(.trainCount)+" : "+.filled+.bar, 0.021)
                endif
            endif
        endmethod
        
        //Modules for attaching instances and periodic iterations
        implement AutoCreate
        implement II32
        implement II1
        
        method queue takes nothing returns nothing
            //Queueing a unit for training
            set .trainCount = .trainCount + 1
            if not .training then
                //If there are no other units being trained, create the bar for it
                set .training = true
                set .tag = CreateTextTag()
                set .bar = BAR
                set .filled = "|r"
                set .trainProgress = 0.00
                call SetTextTagPos(.tag, GetUnitX(.me) - 100.0, GetUnitY(.me), GetUnitFlyHeight(.me) + 160.0)
                call SetTextTagVisibility(.tag, GetLocalPlayer() == GetOwningPlayer(.me))
                call .start32()
            endif
            call SetTextTagText(.tag, BAR_COLOR+I2S(.trainCount)+" : "+.filled+.bar, 0.021)
        endmethod
        
        method cancel takes nothing returns nothing
            //Canceling a training unit
            local player p
            local integer id
            if .training then
                set p = GetOwningPlayer(.me)
                set id = GetUnitTypeId(.me)
                //Returning the resources back to player
                call SetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD, GetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD) + soldierGoldCost(id))
                call SetPlayerState(p, PLAYER_STATE_RESOURCE_LUMBER, GetPlayerState(p, PLAYER_STATE_RESOURCE_LUMBER) + soldierLumberCost(id))
                call SetPlayerState(p, PLAYER_STATE_RESOURCE_FOOD_USED, GetPlayerState(p, PLAYER_STATE_RESOURCE_FOOD_USED) - soldierFoodCost(id))
                set p = null
                set .trainCount = .trainCount - 1
                if .trainCount == 0 then
                    //If that was the last unit, stop the periodic execution and remove the bar
                    call .stop32()
                    call DestroyTextTag(.tag)
                    set .training = false
                else
                    //Otherwise start with next unit
                    set .bar = BAR
                    set .filled = "|r"
                    set .trainProgress = 0.00
                    call SetTextTagText(.tag, BAR_COLOR+I2S(.trainCount)+" : "+.filled+.bar, 0.021)
                endif
            endif
        endmethod
        
        method replace takes nothing returns nothing
            //When the leader dies, a replacement has to be found
            local Soldier newLeader = .first //The first available member is picked
            if newLeader == 0 then //If the list is empty
                //Stop any training and running methods and destroy the instance
                if .training then
                    call .stop32()
                    call DestroyTextTag(.tag)
                endif
                call .stop1()
                call .destroy()
            else
                //If there is an available member, then transform it into leading unit type
                call transformSoldier(newLeader.me, GetUnitTypeId(.me))
                //Assign the Leader instance to the new leader
                set .me = newLeader.me
                set .first = newLeader.next //The next member becomes the new first
                call newLeader.remove() //Removing Soldier instance attached to the new leader
            endif
        endmethod
        
        static method train takes nothing returns boolean
            //Issuing training for a detachment
            local thistype this
            local integer id
            local player p
            //Checking if the casted ability is one of the reinforcing abilities
            if isReinforce(GetSpellAbilityId()) then
                set this = thistype[GetTriggerUnit()]
                set id = GetUnitTypeId(.me)
                set p = GetTriggerPlayer()
                //Inspecting the requirements and whether they are fulfilled
                if .memberCount + .trainCount < detachmentSize(id) then
                    if GetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD) >= soldierGoldCost(id) then
                        if GetPlayerState(p, PLAYER_STATE_RESOURCE_LUMBER) >= soldierLumberCost(id) then
                            if GetPlayerState(p, PLAYER_STATE_RESOURCE_FOOD_CAP) - GetPlayerState(p, PLAYER_STATE_RESOURCE_FOOD_USED) >= soldierFoodCost(id) then
                                //After all the checks, the resources are being deducted
                                call SetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD, GetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD) - soldierGoldCost(id))
                                call SetPlayerState(p, PLAYER_STATE_RESOURCE_LUMBER, GetPlayerState(p, PLAYER_STATE_RESOURCE_LUMBER) - soldierLumberCost(id))
                                call SetPlayerState(p, PLAYER_STATE_RESOURCE_FOOD_USED, GetPlayerState(p, PLAYER_STATE_RESOURCE_FOOD_USED) + soldierFoodCost(id))
                                //And a unit is queued for training
                                call .queue()
                            else
                                call SimError(p, ERROR_INSUFFICIENT_FOOD)
                            endif
                        else
                            call SimError(p, ERROR_INSUFFICIENT_LUMBER)
                        endif
                    else
                        call SimError(p, ERROR_INSUFFICIENT_GOLD)
                    endif
                else
                    call SimError(p, ERROR_DETACHMENT_FULL)
                endif
                set p = null
            elseif GetSpellAbilityId() == CANCEL then
                //If the casted ability was cancel, call the cancel method
                call thistype[GetTriggerUnit()].cancel()
            endif
            return false
        endmethod
        
        static method death takes nothing returns boolean
            //If a dying unit is a leader or a soldier, corresponding procedures are performed
            if isSoldier(GetTriggerUnit()) then
                call Soldier[GetTriggerUnit()].remove()
            elseif isLeader(GetTriggerUnit()) then
                call thistype[GetTriggerUnit()].replace()
            endif
            return false
        endmethod
        
        static method order takes nothing returns boolean
            //Mimicking leader's immediate orders
            local thistype this
            local Soldier soldier
            local integer id
            if isLeader(GetTriggerUnit()) then //If the issued unit is a leader
                set this = thistype[GetTriggerUnit()]
                if .memberCount > 0 then
                    set id = GetIssuedOrderId()
                    set soldier = .first
                    set Soldier.checkOrder = false
                    loop
                        exitwhen soldier == 0
                        //Issue the same order to every member
                        if IssueImmediateOrderById(soldier.me, id) then
                            //Issuing defend order alone interrupts previous orders so it has to be resumed
                            if id == OrderId("defend") or id == OrderId("undefend") then
                                call soldier.resumeOrder()
                                //Resuming order sets .checkOrder to true so it has to be set back to false
                                set Soldier.checkOrder = false
                            else
                                //If it wasn't a defend order, the standard procedure of saving issued order is carried out
                                set soldier.orderId = id
                                set soldier.orderPoint = false
                                set soldier.orderTarget = null
                            endif
                        endif
                        set soldier = soldier.next
                    endloop
                    set Soldier.checkOrder = true
                endif
            elseif isSoldier(GetTriggerUnit()) and Soldier.checkOrder then
                //If the issued unit was soldier and the order did not come from the leader, the previous order is resumed
                call Soldier[GetTriggerUnit()].resumeOrder()
            endif
            return false
        endmethod
        
        static method orderPoint takes nothing returns boolean
            //Mimicking leader's point orders
            local thistype this
            local Soldier soldier
            local integer id
            local integer i
            local integer rows
            local real leaderx
            local real leadery
            local real facing
            local real x
            local real y
            local real d
            local real a
            local integer c
            if isLeader(GetTriggerUnit()) then
                set this = thistype[GetTriggerUnit()]
                if .memberCount > 0 then
                    set id = GetIssuedOrderId()
                    //Caching order coordinates and angle towards it
                    set leaderx = GetOrderPointX()
                    set leadery = GetOrderPointY()
                    set facing = Atan2(leadery - GetUnitY(.me), leaderx - GetUnitX(.me))
                    set soldier = .first
                    set rows = (.memberCount + (.spr - 1)) / .spr //Getting number of rows (.spr - soldiers per row)
                    set i = 1
                    set Soldier.checkOrder = false
                    loop
                        //Iterating every row
                        exitwhen i > rows
                        set x = i*.vDistance //Vertical distance to the row
                        //Getting the number of soldiers in current row
                        if i == rows then //If it's the last row
                            //Then the number needs to be calculated
                            set c = .memberCount - (rows - 1)*.spr
                        else
                            //If it's not last row, then the number is standard
                            set c = .spr
                        endif
                        set y = (c - 1)*.hDistance/2 //Distance from the first soldier's position in the row to the middle of the row
                        set d = SquareRoot(x*x + y*y) //Distance between the leader and the soldier's position
                        set a = Atan2(y, x) + facing + bj_PI //Angle between the leader and the soldier's position
                        //Coordinates of the first soldier in the row
                        set x = leaderx + d*Cos(a)
                        set y = leadery + d*Sin(a)
                        set a = facing + bj_PI/2 //Row angle, used for lining soldiers up
                        loop
                            //Iterating row's soldiers and their positions
                            set c = c - 1
                            //Saving issued order and giving it to the soldiers
                            set soldier.orderPoint = true
                            set soldier.orderX = x + c*.hDistance*Cos(a)
                            set soldier.orderY = y + c*.hDistance*Sin(a)
                            set soldier.orderTarget = null
                            if IssuePointOrderById(soldier.me, id, soldier.orderX, soldier.orderY) then
                                set soldier.orderId = id
                            else
                                call IssuePointOrderById(soldier.me, OrderId("smart"), soldier.orderX, soldier.orderY)
                                set soldier.orderId = OrderId("smart")
                            endif
                            set soldier = soldier.next
                            exitwhen c == 0
                        endloop
                        set i = i + 1
                    endloop
                    set Soldier.checkOrder = true
                endif
            elseif isSoldier(GetTriggerUnit()) and Soldier.checkOrder then
                call Soldier[GetTriggerUnit()].resumeOrder()
            endif
            return false
        endmethod
        
        static method orderTarget takes nothing returns boolean
            //Mimicking leader's target orders
            local thistype this
            local Soldier soldier
            local integer id
            local widget target
            if isLeader(GetTriggerUnit()) then
                set this = thistype[GetTriggerUnit()]
                if .memberCount > 0 then
                    set id = GetIssuedOrderId()
                    set target = GetOrderTarget()
                    set soldier = .first
                    set Soldier.checkOrder = false
                    loop
                        exitwhen soldier == 0
                        //Saving leader's order and issuing it to members
                        set soldier.orderPoint = false
                        set soldier.orderTarget = target
                        if IssueTargetOrderById(soldier.me, id, target) then
                            set soldier.orderId = id
                        else
                            call IssueTargetOrderById(soldier.me, OrderId("smart"), target)
                            set soldier.orderId = OrderId("smart")
                        endif
                        set soldier = soldier.next
                    endloop
                    set Soldier.checkOrder = true
                    set target = null
                endif
            elseif isSoldier(GetTriggerUnit()) and Soldier.checkOrder then
                call Soldier[GetTriggerUnit()].resumeOrder()
            endif
            return false
        endmethod
        
        static method onInit takes nothing returns nothing
            static if LIBRARY_RegisterPlayerUnitEvent then
                call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_SPELL_EFFECT, function thistype.train)
                call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_DEATH, function thistype.death)
                call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER, function thistype.order)
                call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, function thistype.orderPoint)
                call RegisterPlayerUnitEvent(EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, function thistype.orderTarget)
            else
                local trigger t = CreateTrigger()
                call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
                call TriggerAddCondition(t, Condition(function thistype.train))
                set t = CreateTrigger()
                call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_DEATH)
                call TriggerAddCondition(t, Condition(function thistype.death))
                set t = CreateTrigger()
                call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_ORDER)
                call TriggerAddCondition(t, Condition(function thistype.order))
                set t = CreateTrigger()
                call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER)
                call TriggerAddCondition(t, Condition(function thistype.orderPoint))
                set t = CreateTrigger()
                call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
                call TriggerAddCondition(t, Condition(function thistype.orderTarget))
                set t = null
            endif
        endmethod
        
    endstruct
    
    private module initModule
        private static method onInit takes nothing returns nothing
            //Initializing table and soldier linked list end
            set detachmentData = TableArray[8]
            set Soldier(0).next = 0
            set Soldier(0).prev = 0
        endmethod
    endmodule
    private struct initStruct
        implement initModule
    endstruct

endlibrary
This system uses following external resources:
  • Copy the pages containing the system code and code used by the system. (Found in the map; also listed in system code under the "uses" and "optional" keywords.)
  • Follow the instructions given in the "How to use" section of system code and adjust the constants as needed.
  • Use functions from the API to set your detachments up. (If you're a GUI user, read the coming text)

If you're using GUI, you can use functions to setup detachments with Custom scripts.
In a trigger that runs on initialization, for each detachment type add this Custom script:
  • Custom script: call InitDetachment(udg_LeaderRawCode, udg_SoldierRawCode, udg_ReinforceRawcode, udg_TransformRawcode, udg_SoldierGoldCost, udg_SoldierLumberCost, udg_SoldierFoodCost, udg_TrainTime)
To simplify this, instead of editing the actual Custom script, create these variables in the variable editor:
  • LeaderRawCode (unit type, assign it the leader unit type)
  • SoldierRawCode (unit type, same as previous, but soldier unit type)
  • ReinforceRawCode (ability, assign it the Reinforce ability)
  • TransformRawcode (ability, same as previous, but transform ability)
  • SoldierGoldCost (integer, the gold cost of a soldier)
  • SoldierLumberCost
  • SoldierFoodCost
  • TrainTime (integer)
Before the Custom script, set the values of these variables.

There is another function you can use, this one lets you change maximum member count per detachment, soldiers per row and spacing between soldiers:
  • Custom script: call SetDetachmentSize(udg_LeaderRawCode, udg_MaximumMemberCount, udg_SoldiersPerRow, udg_VerticalSpacing, udg_HorizontalSpacing)
Variables:
  • LeaderRawCode (unit type, the one used in previous function, it is used to determine which detachment type you're changing)
  • MaximumMemberCount (integer)
  • SoldiersPerRow (integer)
  • VerticalSpacing (real, space between rows)
  • HorizontalSpacing (real, space between soldiers within row)
Like before, you have to set values for these variables before using the Custom script.



Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Squad Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group


Keywords:
Detachment, Squad, Battalion, Group, Team, Company, Division, Regiment, Force, Squadron, Troop, Formation, Order, Lieutenant, Captain, Leader, General
Contents

Detachments (Map)

Reviews
00:01, 23rd Jun 2013 PurgeandFire: Changes made. Approved and recommended. A solid, unique system.

Moderator

M

Moderator

00:01, 23rd Jun 2013
PurgeandFire: Changes made. Approved and recommended. A solid, unique system.
 
Level 33
Joined
Apr 24, 2012
Messages
5,113
you should link the Libraries with the /**/ parse.
JASS:
call IssueImmediateOrder(.me, "stop")
set .orderTarget = null
set .orderId = OrderId("stop")
->
JASS:
call IssueImmediateOrderById(.me, 851972)
set .orderPoint = false
set .orderId = 851972
http://www.hiveworkshop.com/forums/jass-resources-412/repo-order-ids-197002/

The number of ticks or "I" has can be configurable as well.
You can move the bar variable into a global
 
Level 20
Joined
Jul 6, 2009
Messages
1,885
you should link the Libraries with the /**/ parse.
What's that? ;o
order string -> order id
The performance boost opposed to improved readability of using orders in string rather than integer form is negligible. You can more easily see or change the issued order if it's in string form.
The number of ticks or "I" has can be configurable as well.
You can move the bar variable into a global
Well, bar fill speed is configurable so changing bar length shouldn't be needed.
But okay, I'll add that.
 
Level 20
Joined
Jul 6, 2009
Messages
1,885
- Pls make the RegisterPlayerUnitEvent NOT optional, the onInit is not good imo

Why would it not be optional? it gives user more freedom.
And what's wrong with onInit? I have to initialize the events.
- Put API methods at documentation

The public functions are already in the API.
- Why you didnt use GetUnitGoldCost and GetUnitWoodCost?

Training unit through reinforcement doesn't necessarily have to use object editor values. The user might want to train them a unit using object editor cost, separately from this system.
- Cache your PlayerState and alike via local variables

I'm using them twice only each in a function, is it worth creating a local variable for that?
- SimError notices be configurable

Ok, I'll add this.
 
Level 29
Joined
Mar 10, 2009
Messages
5,016
Why would it not be optional? it gives user more freedom.
And what's wrong with onInit? I have to initialize the events.
Yeah but since you have many requirements atm, might as well include that...

I'm using them twice only each in a function, is it worth creating a local variable for that?
Even if 2, it's still slower coz total is (player+state)*2 = 4, while caching does it twice...also I'm suggesting this for readability's sake...

EDIT:
also I didnt see the libraries until now, why are you making a group library without anything in it except the group alone or one variable only?, and why dont you put the Z inside the code itself and the group?

EDIT 2:
you could use Nesthauru's TimerTolls for period frequency in it...
 
Level 20
Joined
Jul 6, 2009
Messages
1,885
Yeah but since you have many requirements atm, might as well include that...

It is included, but it's optional. Why would you want it to be more limited?
Even if 2, it's still slower coz total is (player+state)*2 = 4, while caching does it twice...also I'm suggesting this for readability's sake...

What's "player+state"? The GetPlayerState function repeats once for each resource. It's very readable atm because of intuitive names of these functions; using local would add an unecessary line.
EDIT:
also I didnt see the libraries until now, why are you making a group library without anything in it except the group alone or one variable only?, and why dont you put the Z inside the code itself and the group?

Because if every library used its own global group or Z functions, you'd have unnecessary repetitions of these. Most systems or maps have a small library with Z functions, if they're present, the one used by this library can just be removed. Same goes for group, some libraries like GroupUtils have their own global group for such uses, so instead of importing GroupUtils, I added this group. If there's another library that has such group, the one used here can be removed.
EDIT 2:
you could use Nesthauru's TimerTolls for period frequency in it...

I'm not sure what TimerTools is for. The idea of merging timers with similar timeouts seems wrong to me. I'd rather have exact periods.
The resource I use for periodic iterations is something between T32 and CTL.
I didn't use CTL because it has what appears to me as many unneeded things.
 
Level 20
Joined
Jul 6, 2009
Messages
1,885
I must ask
JASS:
*        */ II                      /*
*        */ Group                   /*
*        */ Z                       /*

what are those libraries? they have no links to them, so I wonder

They aren't public libraries. Group provides a global group for enumeration (so I don't have to recreate it each time), Z provides functions such as GetUnitZ, SetUnitZ and GetPointZ and II is used for periodic instance iteration, something like T32 or CTL.
the onInit method is fine. not sure y it wouldnt be.

i dont like how ur struct doesnt extend an array. it produces a lot more code when u dont extend an array. so its less effecient and slower

Meh, it probably would be better if it extended an array, but I don't think it matters that much. The main difference is, as far as I know, allocation method. Since the instances aren't allocated periodically or something like that, I don't think the efficiency beats readability and intuitivity here.
 
Level 29
Joined
Mar 10, 2009
Messages
5,016
It is included, but it's optional. Why would you want it to be more limited?
it's not being limited, I can see also your Z, II and Group is not optional, why the Register is an exception?, now you've mentioned unecessary lines, well that is one, coz we have the register library already :D...

What's "player+state"? The GetPlayerState function repeats once for each resource. It's very readable atm because of intuitive names of these functions; using local would add an unecessary line.
GetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD), calls the (player and gold), then you did it again so 2*2=calls 4, unecessary lines doesnt hurt as much as unecessary width...

Because if every library used its own global group or Z functions, you'd have unnecessary repetitions of these. Most systems or maps have a small library with Z functions, if they're present, the one used by this library can just be removed. Same goes for group, some libraries like GroupUtils have their own global group for such uses, so instead of importing GroupUtils, I added this group. If there's another library that has such group, the one used here can be removed.
You dont need Group Utils, in fact also, bj_lastCreatedGroup can do, imo, it's a waste of importing libraries with that few code inside...also, moderators tend to approve systems with approved system resource...
 
Overall I like this system. It is creative. :) And by the way, ty for the awesome test map. I actually messed around with it for a while before I realized I was supposed to be moderating a resource. Here is my review:

Overall, this is a solid system. It has nice checks (meaning, it checks things for actual playable maps), great error messaging, and the code is very neat. The improvements I am going to suggest will be mostly minor.
  • In static method filter, you need to null f.
  • This following method is unorthodox but it is a nice little tip:
    JASS:
    if GetLocalPlayer() != GetOwningPlayer(.me) then
                        call SetTextTagVisibility(.tag, false)
                    endif
    Can become:
    JASS:
    call SetTextTagVisibility(.tag, GetLocalPlayer() == GetOwningPlayer(.me))
    Of course, it is a miniscule difference, but it looks pretty clean. However, if you would like to keep the prior method for readability purposes, that is completely fine.
  • For method iterate1, you may be able to take advantage of IsUnitInRangeXY() to eliminate some lines and eliminate the SquareRoot/multiplication:
    JASS:
    * * * * method iterate1 takes nothing returns nothing
    * * * * * * //This method is executed every second and is used to check if a member wanders far off
    * * * * * * local Soldier soldier = .first //Starting for first soldier in the list
    * * * * * * local real lx = GetUnitX(.me)
    * * * * * * local real ly = GetUnitY(.me)
    * * * * * * local real a
    * * * * * * loop
    * * * * * * * * exitwhen soldier == 0 //When hitting the end of the list, the loop finishes
    * * * * * * * * if not IsUnitInRange(soldier.me, .me, WANDER_DISTANCE) then
    * * * * * * * * * * //If the member is too far, he/she is issued an order to go back
    * * * * * * * * * * set a = Atan2(GetUnitY(soldier.me) - ly, GetUnitX(soldier.me) - lx)
    * * * * * * * * * * set Soldier.checkOrder = false //To prevent infinite loop
    * * * * * * * * * * set soldier.orderId = OrderId("move")
    * * * * * * * * * * set soldier.orderPoint = true
    * * * * * * * * * * set soldier.orderX = lx + ARRIVAL_RANGE*Cos(a)
    * * * * * * * * * * set soldier.orderY = ly + ARRIVAL_RANGE*Sin(a)
    * * * * * * * * * * set soldier.orderTarget = null
    * * * * * * * * * * call IssuePointOrderById(soldier.me, soldier.orderId, soldier.orderX, soldier.orderY)
    * * * * * * * * * * set Soldier.checkOrder = true
    * * * * * * * * endif
    * * * * * * * * set soldier = soldier.next
    * * * * * * endloop
    * * * * endmethod
    (^Sorry for the stars, something retarded happened to my text editor.)

    Something along those lines. Untested, but I assume it would look something like that.
  • In static method createFilter, you can just use return isLeader(u), unless you intended it to look like that for configuration purposes.
  • The library "Group" is a bit of a superfluous requirement. It is good to take advantage of a single global group as much as you can, but I think making libraries such as that is best left for map making. For your own map, you can make it require "Group" but for a system I'd just inline it and make one global group. Two reasons: (1) it is daunting when you go to use a system and you see a bunch of triggers to copy and paste (2) it has no benefit compared to having a single global group unless the user uses other systems that use it--or if they use it themselves. Still, this is entirely up to you. I'm just giving some feedback to help promote your system.
  • For II, it is a similar case. You may want to remove the versions that are not pertinent to this system (unless you plan on using that system for other spells/systems). You don't have to remove them completely, but make them regular comments instead of actual "//!" textmacro calls.

That's about it. Also, just a note: you don't have to use custom allocation (in regard to the posts before this).

@mckill: Optional is better than required. Extra dependencies is not necessarily a good thing (they usually aren't a bad thing either, but it is something to take in moderation). When you can make it optional without much of a hassle, then it is good to do so.
 
Level 20
Joined
Jul 6, 2009
Messages
1,885
Thanks for the great review and suggestions.

I have implemented them and updated the resource.

For the Group library, I have removed it and added a configurable variable. Currently, the library creates the group, but if there is a global group for dynamic enumerations, the user may assign it to the variable in the library.
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
the Z library looks quite interesting, I think you could submit it because it has the 3 very good function for Z coordinates. I think that it would be quite useful even tho its simple and short

The II looks like some period manipulator, it also looks quite interesting
 
Level 20
Joined
Jul 6, 2009
Messages
1,885
I think there are many libraries with Z functions, some posted in small code snippets. It should be standardized at some point...
I don't know if II would be approved, public libraries like CTL and/or TimerTools are supposed to be used and are probably more efficient, but CTL only supports 1 period and TimerTools isn't designed to iterate structs. I'm using II because of its simple API and support for multiple periods.
it would be nice if u could post those things in ur pastebin right here so we can see them easier.
They are in pastebin. Links are in library requirements.
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
as long as it provides something new it would be approved(unless full of bugs and/or leaks), there are 3 damage detection systems, each of them does the same thing, but each of them does it in other way

I think this is not that bad, you can give it a try ;)
 
Level 29
Joined
Oct 24, 2012
Messages
6,543
I think there are many libraries with Z functions, some posted in small code snippets. It should be standardized at some point...
I don't know if II would be approved, public libraries like CTL and/or TimerTools are supposed to be used and are probably more efficient, but CTL only supports 1 period and TimerTools isn't designed to iterate structs. I'm using II because of its simple API and support for multiple periods.

They are in pastebin. Links are in library requirements.

ik they are in pastebin but i dont like looking at one system when its on multiple pages.
 
Level 29
Joined
Mar 10, 2009
Messages
5,016
That's what II is doing, it uses a textmacro to create variables for every period needed.
Unless you mean something else, as I don't know a way with creating only extra 2 real variables.
JASS:
implement CTLExpire
   set .dur = .dur + 0.03125
   if .dur > 1.0 then
      set .dur = 0
      //run here
   endif
implement CTLEnd
I've used this method and seems no problem...
 
Level 16
Joined
May 2, 2011
Messages
1,345
BEAUTIFUL,

You also have a spell formation of the troops? Like.
Captain: FORM A LINE
Footman: aye Captain
Captain: Form a Rectangle
Footmen: Aye Captain
Captain: Capture the Death Knight
Footman: FOR LORDEAROOOOOOOOOOOOOON!!!!!!!!!!!!! *SURRONDS DEATH KNIGHT*

This will be cool as well
 
I really can't stand the neverending bigotry of the speed freaks, and other people advocating stuff that seriously damages readability for minuscule speed benefits. In systems like this performance is really, really not an issue, and i love that your code is so readable and well made.

That being said, there are some things outside of coding that i think you should change. First of all, your approach to replacing leader units breaks control groups. A better way would be to detect when the leader is dying, prevent it, and simply swap it with another random unit in the group. The swapped unit will take the leaders place and die, while the leader will have his HP set to that of the swapped unit. This also means the user doesn't have to add a bear form ability to each of the base units.

Second, although i do think using the ward classification is nifty, there are other ways to unable units to be selected. Here is what the Wc3c ability guide says about the locust ability:

Aloc (Locust): A unit that is given locust becomes unselectable, untargetable and ignores any pathing limitations. This means it can only be controlled and ordered with triggers. It won't show a life bar over its head too. Now to get rid of locust UnitRemoveAbility(u, 'Aloc') won't work therefore you'll need to chaos transform the unit into a different temporary unit type and immediately transform it back. However if the unit had been transformed with a chaos ability before then adding locust will make it unselectable but still targetable and it will also obey pathing rules. Another option to get rid of locust is the following hiding the unit, removing the ability and unhiding it again.

As you see, it is possible to use the locust ability, while still making the unit targetable and having it obey pathing rules.

Other than that, great system. Keep up the good work.
 
Level 4
Joined
Jul 31, 2013
Messages
50
Okay, Warcraft : Total War is going really good,just...
I need commands to make the units use another formation,because my Generals are always the first to die (it's normal,they are in the first line!), even if they have 100 more hp than the Soldiers :/
Also,it's good that I can define the number of Soldiers per type of Detachment,but
I would also put that you can define how much space there is between troops in the formation...yes,for each Detachment.

Also,how come it isn't calculated when I put Commanders on the map with the editor instead of recruiting them???
 
Level 3
Joined
Apr 26, 2014
Messages
37
I didn't get it. I've copied the whole system into a map. I changed your footmen and archers, and everything was ok. Then I created another commander and unit (with all spells and styff). I haven't done any scripts, so it was still ok. This new troops weren't working, wich is logical at this stage.
Then I created the variables, you've mentioned in this theme. I made them array (was that a mistake?) Copied the custom srcipts, put them after setting [1] variables. Now JNPG doesn't want to save my map and finds some mistakes.
Could you please and some screenshots to show how and where exactly do i need to put new variables? How custom units-commanders should look like in GUI?
Hope to hear from you soon.
 
Level 20
Joined
Jul 6, 2009
Messages
1,885
Your new units need to fit the description given in the How-to-use section of the code.
You get errors because you use an array, which isn't really needed and has different syntax from what is used in custom script.
Just make the variables listed in the variable editor. Then in an initialization trigger, set the values for these variables according to your new units and call the custom script right after.
 
Level 3
Joined
Apr 26, 2014
Messages
37
Thank you) I corretced mistakes, now everything is working)
This little challenge makes me feel like a real coda :)
Your system is perfect, but are you going to make some map using it?
Another side question - how could possibly AI use this system?
 
Level 3
Joined
Apr 26, 2014
Messages
37
There is some problems. After a fight, where detachments' leaders died several times I got this picture. Orcs in the red square don't move at all and don't follow any leaders, Trolls in the blie square are don't know whom to follow - orc commanders or their troll commanders.
N6APJC234I8.jpg
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
There is a bug with the detachment training of captain. If you have a full detachment I can still queue up 4 units it will reach full training time and then complain correctly that I can't train, however for the archer captain this option is not posible, it will complain as soon as I try to trian more then what is posible.

Could you make the squad units unselectable?
 
Top