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

Threat System (a.k.a Aggro System)


attachment.php

What is a threat system?
Basicly, it's a system that takes over Mob AI. The unit that dealt the most damage to the creep will be the one that gets attacked.
Although that is only half the truth, it should be enough to get the idea.


ZTS is the easiest to use and most stable threat system currently available on the web.

Requirements:
- vJass preprocessor

What is different from this to other systems:
- First of all, this system is almost (if not completely) 100% fail-safe. It does checks on killed AND removed units and clears them automaticly.

- Second, it features a unique camp squad functionality, which ensures that units that are preplaced in camps will always attack and return together. If units return, they will be rendered invulnerable until they reach their original camp position again. If they can not reach their camping position within a certain duration, they will get instantly teleported back.
Like this, there is almost no more kiting or bug-abusing possible, as it was with other systems.

- Third: It does not block other Spellcasting systems. The system will only give orders to units, if the unit either has no order (i.e. auto-engage) or an "attack" order. If you want the unit to use a spell instead of attacking, you can do so whenever you want without having to fear interference of the system.
In fact, there is a special function provided so that people can create their own Spell-AI directly in a custom trigger

- Fourth: It provides a lot of useful functions and Getters (Like ApplyHealThreat or GetCombatState), to make triggering spells even more easy.

- Fifth: It is, by far, the fastest and most flexible threat system out there. I completely remastered the system from the last version and the entire script (except for certain enumerations that can not be avoided) is now O(1) complexity.


Version history:
2.2b
- fixed a small logic bug with GetThreatUnitAmount and GetThreatUnitPosition

2.2
- Now uses "smart" order instead of "attack" to issue attack orders ... it turned out that the "smart" order returns false if the unit can not reach the target (for example when rooted) - weird, as it doesn't work for "attack" for some reason
- because of that, the AddRootAbility function and the Range Setter and Getters were removed, as they have become obsolete

2.1b
- fixed a small logical bug with GetCombatState sometimes returning a false positive

2.1
- rebuilt Update function to avoid some useless enumerations - depending on the number of PlayerUnits registered, the system should now be MULTIPLE TIMES faster - As a side effect, I could also remove some useless variables

2.0
- Initial release

Comments & Discussion:

Why hashtables instead of global arrays and structs?
- hashtables are not limited in terms of max size, unlike and array of structs with a unit array, which reaches the 8000 limit very fast ... this was basicly the most important point on that decision
- hashtables are more flexible and easier to use (Flush functions, etc.)
- hashtables were benchmarked to be only 60-80% slower than getting UnitUserData alone

Where is the sort function?
- version 2.0 and higher does not use sorting anymore; instead, when threat is applied to a unit, it uses an insertion method to keep the order of the list

What is new compared to pre 2.0 versions?
- Aside from the fact that the system now is a dozen times faster than before, I also improved the AI by using the "smart" instead of "attack" order. It turned out that - in a weird way - "smart" is indeed smarter than other orders, as it returns false if the unit can't reach the target (i.e. because of root)
- There is now a way to directly get an Order event by the threat system, to make creating spell-AI easier.

Does it matter how many units are registered to the system at the same time?
- In terms of speed, no; it only affects memory usage, but that should not have an impact on game performance at all, even with tousands of units registered - only the number of currently fighting units affects runtime

I still do not really understand how to use the system...
- just check the demo map and you'll get the idea

JASS:
//------------------------------------------------------------------------------------------------------//
//  ZTS - ZWIEBELCHEN'S THREAT SYSTEM        v. 2.2b                                                     //
//------------------------------------------------------------------------------------------------------//         
//------------------------------------------------------------------------------------------------------//
//                                                                                                      //
//  Requires: vJass Pre-Processor                                                                       //
//                                                                                                      //
//  Special thanks to TEC-Ghost, who inspired me on creating this system.                               //
//------------------------------------------------------------------------------------------------------//
//                                              MANUAL                                                  //
//------------------------------------------------------------------------------------------------------//
// 1.   How to Install:
//
//      - Create a trigger called "ZTS"
//      - Convert it to custom Text
//      - Replace everything inside with this code
//
//
// 2.   How to set it up:
//
//  2.1 Constants
//
//      There are a bunch of global constants below this manual, you can edit to your liking.
//      I commented everything you need to know about those constants right beside them. If you need additional information,
//      please tell me, so I can improve this manual. However, I think most of it should be pretty clear.
//
//  2.2. Gameplay Constants
//
//      It is recommended to edit certain Gameplay Constants entries, to use the full potential of
//      the system.
//      The most important entries are: (with selected "Show Raw-Data")
//      CallForHelp                 --> Set this to 0, if possible; the system will manage this - it isn't a problem if you don't do this, though
//                                      You don't have to do it if your threat-system controlled units are neutral hostile
//      CreepCallForHelp            --> Set this to 0, if possible; the system will manage this - it isn't a problem if you don't do this, though
//      GuardDistance               --> Set this to something higher than ReturnRange (see below)
//      MaxGuardDistance            --> Set this to something higher than ReturnRange (see below)
//      GuardReturnTime             --> Set this to something very high, so that the standard AI doesn't interfere (i.e. 60 seconds)       
//
//  2.3. Damage Detection
//
//      Of course, a threat system is pretty useless without a trigger, that adds threat depending on
//      the damage dealt by a player unit.
//      I recommend using a damage detection script like IDDS:
//      ([url]http://www.wc3c.net/showthread.php?t=100618[/url])
//      Check up the demo map for a very simple (but leaking) damage detection trigger
//      The only function you actually need then is:
//      call ModifyThreat(GetEventDamageSource(), GetTriggerUnit(), GetEventDamage(), true)
//      The function does all required checks on its own. There is no need to run something else.
//
// 3. How to use it:
//
//  3.1. Core functions
//
//      call ZTS_AddThreatUnit(unit npc) returns nothing:
//
//          This function registers the unit as an AI-controlled unit.
//          ThreatUnits will automaticly attack the highest-in-threat attacker.
//          When adding a ThreatUnit, its current position gets saved and be considered camp-position.
//          It will always return to this position if pulled to far or on victory.
//          Nearby units will be considered in the same camp group. Camp members will always retreat and attack together.
//
//      call ZTS_AddPlayerUnit(unit pu) returns nothing:
//
//          Units add by this way will generate threat on ThreatUnits.
//          If the unit is not registered as a PlayerUnit, it will not be attacked by ThreatUnits.
//
//      call ZTS_RemoveThreatUnit(unit npc) returns nothing:
//
//          Removes a ThreatUnit from the system. The unit will no longer be controlled by the threat system.
//          Also, the threat list for that unit will be cleared.
//          Dead or removed units will automaticly be cleared. You need to add them again after revival/recreation.
//
//      call ZTS_RemovePlayerUnit(unit pu) returns nothing:
//
//          Removes a player unit from the system. The unit will no longer generate threat on ThreatUnits.
//          The unit will also be instantly removed from all threat lists.
//          If the unit was the last unit in combat with the same hostile camp, all units
//          of that camp group will immediately return to their camp positions.
//          You can use this, followed by AddPlayerUnit to that unit out of combat and reset all threat.
//          Dead or removed units will automaticly be cleared. You need to add them again after revival/recreation.
//
//      call ZTS_ModifyThreat(unit pu, unit npc, real amount, boolean add) returns nothing:
//
//          Adds, sets or substracts threat from npc's threat list caused by pu.
//          Set 'add' to true to add or substract amount from the current value.
//          Set 'add' to false to set the new threat value to amount.
//          To reduce threat, use negative amount values with add == true.
//          Remember: If a unit has 0 threat, it is still considered in-combat -
//          this also means, that adding "0" to the units threat causes them to attack!
//
//      call ZTS_ApplyHealThreat(unit pu, unit ally, real amount, boolean add, boolean divide) returns nothing:
//
//          Adds Healing Threat to all units, that have ally on threat-list
//          This can be abused to apply global threat to a unit by passing the same unit to p and ally.
//          Parameter divide = true means that the amount is split by the number of units attacking the target;
//          for example if 3 units are currently attacking the targeted ally, it adds amount/3 threat from pu to all of them.
//          Parameter divide = false means that every attacking unit gets 'amount' of threat applied.
//          use add = false to set the amount of threat to 'amount', instead of increasing/decreasing it
//          negative values are allowed in combination with 'add' to reduce threat.
//          You can also use this with add = false and amount = 0 with pu = ally to set total threat generated back to zero for this unit.
//
//
//  3.2. Getter functions
//
//      call ZTS_GetCombatState(unit PU) returns boolean:
//
//          Returns the combat state of a player unit.
//          Returns true, if the unit is registered and in combat.
//          Returns false, if the unit is not registered or out of combat.
//
//
//      call ZTS_GetThreatUnitPosition(unit NPC, unit PU) returns integer:
//
//          Returns the position of unit PU in unit NPC's threat list
//          Returns "0" if the unit was not found, NPC does not feature a threat list or in case of invalid input data
//
//      call ZTS_GetThreatUnitAmount(unit NPC, unit PU) returns real:
//
//          Returns the amount of threat unit PU has in unit NPC's threat list
//          Returns "0" if the unit was not found, NPC does not feature a threat list or in case of invalid input data
//
//      call ZTS_GetThreatSlotUnit(unit NPC, integer position) returns unit:
//
//          Returns the unit in threat-slot position
//          Returns null if the NPC does not feature a threat list, the number is too large
//          or in case of invalid input data
//
//      call ZTS_GetThreatSlotAmount(unit NPC, integer position) returns real:
//
//          Returns the threat amount of the threat-slot position
//          Returns "0" if the NPC does not feature a threat list, the number is too large
//          or in case of invalid input data
//
//      call ZTS_GetAttackers(unit U) returns group:
//
//          If used on a ThreatUnit, this returns a group of all units in threat list;
//          if used on a PlayerUnit, this returns a group of all units aggroed.
//          Returns an empty group, in case of invalid input data or empty lists.
//
//  
//  3.3. Advanced User features
//
//      call ZTS_IsEvent() returns boolean
//
//          When using "A unit is issued an order without target" or "A unit is issued a target order" events,
//          this function returns true when the order was issued by the threat system.
//          You can use this to setup your own spell-AI for units.
//          Let's say you want the unit to cast Summon Water Elemental whenever the cooldown is ready:
//          Just use the mentioned events and add:
//              Custom script:   if not ZTS_IsEvent() then
//              Custom script:   return
//              Custom script:   endif
//          at the beginning of you trigger's actions and you're done.
//          You can now issue the order to the triggering unit:
//              Unit - Order (Triggering unit) to Human Archmage - Summon Water Elemental
//          In combination with some of the Getter functions, you can trigger nice spell AI like this.
//          NOTE: ZTS_IsEvent will only return true once(!) for every fired event, so if you need it again inside that trigger,
//                make sure to save it to a variable.
//
//------------------------------------------------------------------------------------------------------//

library ZTS initializer InitThreatSystem

globals   
    private constant real UpdateIntervall = 0.5 //The intervall for issueing orders and performing AttackRange check. recommended value: 0.5
    private constant real HelpRange = 400 //The range between units considered being in the same camp. If a unit of the same camp gets attacked, all others will help.
                                          //Set CallForHelp to something lower in Gameplay Constants.
    private constant real OrderReturnRange = 4000 //The range the unit's target can be away from the original camping position, before being ordered to return.
    private constant real ReturnRange = 1500 //The range the unit can move away from the original camping position, before being ordered to return.
    private constant real TimeToPort = 10 //This timer expires once a unit tries to return to its camping position.
                                          //If it reaches 0 before reaching the camp position, the unit will be teleported immediately.
    private constant boolean HealUnitsOnReturn = true //If this is true, returning units will be healed to 100% health.
    
    
//      Do not edit below here!
//------------------------------------------------------------------------------------------------------//  
    private boolexpr BOOLEXPR = null
    private constant timer Updater = CreateTimer()
    private constant group NPCgroup = CreateGroup()
    private constant hashtable NPClist = InitHashtable()
    private constant hashtable PUlist = InitHashtable()
    
    //temporary variables for enumerations and forgroups
    private unit TSub = null
    private unit TMod = null
    private constant group TGroupSub = CreateGroup()
    private unit THealer = null
    private real THealthreat = 0
    private boolean TBool = false
    private constant group TGroupUpd = CreateGroup()
    private group TGroupGet = null
    private boolean EventBool = false
endglobals

public function IsEvent takes nothing returns boolean
    if EventBool then
        set EventBool = false
        return true
    endif
    return false
endfunction

private function Pos2Key takes integer position returns integer //converts threat list position into hashtable childkey
    return 8+(position*2)
endfunction

private function Key2Pos takes integer key returns integer //converts hashtable childkey into threat list position
    return (key-8)/2
endfunction

public function GetCombatState takes unit u returns boolean
    if GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD) then //unit dead or null
        return false
    elseif HaveSavedInteger(NPClist, GetHandleId(u), 0) then //unit is npc
        return LoadInteger(NPClist, GetHandleId(u), 0) > 0
    elseif HaveSavedHandle(PUlist, GetHandleId(u), 0) then //unit is player unit
        return LoadInteger(PUlist, GetHandleId(u), 1) > 0
    endif
    return false
endfunction

public function GetThreatUnitPosition takes unit npc, unit pu returns integer
    if GetUnitTypeId(npc) == 0 or IsUnitType(npc, UNIT_TYPE_DEAD) or GetUnitTypeId(pu) == 0 or IsUnitType(pu, UNIT_TYPE_DEAD) then //units dead or null
        return 0
    elseif not (HaveSavedInteger(NPClist, GetHandleId(npc), 0) and HaveSavedHandle(PUlist, GetHandleId(pu), 0)) then //units not added
        return 0
    elseif HaveSavedInteger(PUlist, GetHandleId(pu), GetHandleId(npc)) then
        return LoadInteger(PUlist, GetHandleId(pu), GetHandleId(npc))
    endif
    return 0
endfunction

public function GetThreatUnitAmount takes unit npc, unit pu returns real
    if GetUnitTypeId(npc) == 0 or IsUnitType(npc, UNIT_TYPE_DEAD) or GetUnitTypeId(pu) == 0 or IsUnitType(pu, UNIT_TYPE_DEAD) then //units dead or null
        return 0.
    elseif not (HaveSavedInteger(NPClist, GetHandleId(npc), 0) and HaveSavedHandle(PUlist, GetHandleId(pu), 0)) then //units not added
        return 0.
    elseif HaveSavedInteger(PUlist, GetHandleId(pu), GetHandleId(npc)) then
        return LoadReal(NPClist, GetHandleId(npc), Pos2Key(LoadInteger(PUlist, GetHandleId(pu), GetHandleId(npc))))
    endif
    return 0.
endfunction

public function GetThreatSlotUnit takes unit npc, integer position returns unit
    if GetUnitTypeId(npc) == 0 or IsUnitType(npc, UNIT_TYPE_DEAD) or position <= 0 then //unit dead or null or invalid slot
        return null
    elseif not HaveSavedInteger(NPClist, GetHandleId(npc), 0) then //unit not added
        return null
    elseif HaveSavedHandle(NPClist, GetHandleId(npc), Pos2Key(position)) then
        return LoadUnitHandle(NPClist, GetHandleId(npc), Pos2Key(position))
    endif
    return null
endfunction

public function GetThreatSlotAmount takes unit npc, integer position returns real
    if GetUnitTypeId(npc) == 0 or IsUnitType(npc, UNIT_TYPE_DEAD) or position <= 0 then //unit dead or null or invalid slot
        return 0.
    elseif not HaveSavedInteger(NPClist, GetHandleId(npc), 0) then //unit not added
        return 0.
    elseif HaveSavedReal(NPClist, GetHandleId(npc), Pos2Key(position)+1) then
        return LoadReal(NPClist, GetHandleId(npc), Pos2Key(position)+1)
    endif
    return 0.
endfunction

private function GetAttackersSub takes nothing returns nothing
    call GroupAddUnit(TGroupGet, GetEnumUnit())
endfunction

public function GetAttackers takes unit u returns group
    local group g = CreateGroup()
    local integer key = 10
    local integer max
    if GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD) then //unit dead or null
        return g
    endif
    if HaveSavedInteger(NPClist, GetHandleId(u), 0) then //unit is npc
        set max = Pos2Key(LoadInteger(NPClist, GetHandleId(u), 5))
        loop
            exitwhen key > max
            call GroupAddUnit(g, LoadUnitHandle(NPClist, GetHandleId(u), key))
            set key = key+2
        endloop
    elseif HaveSavedHandle(PUlist, GetHandleId(u), 0) then //unit is player unit
        set TGroupGet = g
        call ForGroup(LoadGroupHandle(PUlist, GetHandleId(u), 0), function GetAttackersSub)
        set g = TGroupGet
        set TGroupGet = null
    endif
    return g
endfunction

private function Swap takes integer npcID, integer key1, integer key2 returns nothing
    local unit u = LoadUnitHandle(NPClist, npcID, key1)
    local real r = LoadReal(NPClist, npcID, key1+1)
    call SaveUnitHandle(NPClist, npcID, key1, LoadUnitHandle(NPClist, npcID, key2))
    call SaveReal(NPClist, npcID, key1+1, LoadReal(NPClist, npcID, key2+1))
    call SaveInteger(PUlist, GetHandleId(LoadUnitHandle(NPClist, npcID, key1)), npcID, Key2Pos(key1)) //update position list
    call SaveUnitHandle(NPClist, npcID, key2, u)
    call SaveReal(NPClist, npcID, key2+1, r)
    call SaveInteger(PUlist, GetHandleId(u), npcID, Key2Pos(key2)) //update position list
    set u = null
endfunction

private function CampThreat takes nothing returns nothing
    local integer npcID = GetHandleId(GetEnumUnit())
    local integer puID = GetHandleId(TMod)
    local integer key
    local integer listlength
    if GetEnumUnit() == TSub then
        return
    elseif HaveSavedInteger(PUlist, puID, npcID) then //original pu unit already listed in EnumUnit's threat list
        return
    elseif LoadInteger(NPClist, npcID, 0) > 1 or IsUnitType(GetEnumUnit(), UNIT_TYPE_DEAD) then //do not add threat to dead or units that are status: returning
        return
    endif
    set listlength = LoadInteger(NPClist, npcID, 5)+1
    call SaveInteger(NPClist, npcID, 5, listlength) //add to list length of EnumUnit
    set key = Pos2Key(listlength)
    call SaveUnitHandle(NPClist, npcID, key, TMod) //add original pu unit to end of EnumUnit's threat list
    call SaveReal(NPClist, npcID, key+1, 0)
    call SaveInteger(PUlist, puID, npcID, listlength) //add EnumUnit to slot list
    call GroupAddUnit(LoadGroupHandle(PUlist, puID, 0), GetEnumUnit()) //add EnumUnit to slot list group
    call SaveInteger(PUlist, puID, 1, LoadInteger(PUlist, puID, 1)+1) //increase group size count
    if LoadInteger(NPClist, npcID, 0) == 0 then
        call SaveInteger(NPClist, npcID, 0, 1) //set unit status: combat
        call GroupAddUnit(NPCgroup, GetEnumUnit()) //add the unit to incombat group
    endif
endfunction

public function ModifyThreat takes unit pu, unit npc, real amount, boolean add returns nothing
    local integer npcID = GetHandleId(npc)
    local integer puID = GetHandleId(pu)
    local integer key
    local integer listlength
    local integer i = 0
    local real newamount
    local real oldamount = 0
    local boolean b = false
    if not (HaveSavedInteger(NPClist, npcID, 0) and HaveSavedHandle(PUlist, puID, 0)) then //units not added
        return
    elseif IsUnitType(pu, UNIT_TYPE_DEAD) or IsUnitType(npc, UNIT_TYPE_DEAD) then //units dead
        return
    elseif GetUnitTypeId(pu) == 0 or GetUnitTypeId(npc) == 0 then //null units
        return
    elseif LoadInteger(NPClist, npcID, 0) > 1 then //do not add threat to units that are status: returning
        return
    endif
    if not HaveSavedInteger(PUlist, puID, npcID) then //pu not listed in npc's threat list
        set listlength = LoadInteger(NPClist, npcID, 5)+1
        call SaveInteger(NPClist, npcID, 5, listlength) //add to list length of npc
        set key = Pos2Key(listlength)
        call SaveUnitHandle(NPClist, npcID, key, pu) //add pu to end of npc's threat list
        call SaveInteger(PUlist, puID, npcID, listlength) //add npc to slot list
        call GroupAddUnit(LoadGroupHandle(PUlist, puID, 0), npc) //add npc to slot list group
        call SaveInteger(PUlist, puID, 1, LoadInteger(PUlist, puID, 1)+1) //increase group size count
        if LoadInteger(NPClist, npcID, 0) == 0 then
            call SaveInteger(NPClist, npcID, 0, 1) //set unit status: combat
            call GroupAddUnit(NPCgroup, npc) //add the unit to incombat group
        endif
        set b = true
    else
        set key = Pos2Key(LoadInteger(PUlist, puID, npcID))
        set oldamount = LoadReal(NPClist, npcID, key+1)
    endif
    if add then
        set newamount = oldamount+amount
    else
        set newamount = amount
    endif
    if newamount < 0 then
        set newamount = 0
    endif
    call SaveReal(NPClist, npcID, key+1, newamount)
    if newamount > oldamount then //check lower keys
        loop
            if HaveSavedReal(NPClist, npcID, key-1-i) then
                if LoadReal(NPClist, npcID, key-1-i) < newamount then //lower key amount is smaller
                    call Swap(npcID, key-2-i, key-i)
                else
                    exitwhen true
                endif
                set i = i + 2
            else
                exitwhen true
            endif
        endloop
    elseif newamount < oldamount then //check higher keys
        loop
            if HaveSavedReal(NPClist, npcID, key+3+i) then
                if LoadReal(NPClist, npcID, key+3+i) > newamount then //upper key amount is larger
                    call Swap(npcID, key+2+i, key+i)
                else
                    exitwhen true
                endif
                set i = i + 2
            else
                exitwhen true
            endif
        endloop
    endif
    if b then //set all units of the same camp to status: combat and apply 0 threat from pu to them
        set TSub = npc
        set TMod = pu
        call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampThreat)
    endif
endfunction

public function AddPlayerUnit takes unit u returns nothing
    local integer ID = GetHandleId(u)
    if HaveSavedInteger(NPClist, ID, 0) or HaveSavedHandle(PUlist, ID, 0) then //unit already added
        return
    elseif GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD) then //unit dead or null
        return
    endif
    call SaveGroupHandle(PUlist, ID, 0, CreateGroup()) //slot list group
    call SaveInteger(PUlist, ID, 1, 0) //list group count
endfunction

private function AcquireTarget takes nothing returns nothing
    local unit npc = GetTriggerUnit()
    local unit pu
    if GetEventTargetUnit() != null then
        set pu = GetEventTargetUnit()
    else
        set pu = GetOrderTargetUnit()
    endif
    if IsUnitEnemy(pu, GetOwningPlayer(npc)) then
        if LoadInteger(NPClist, GetHandleId(npc), 0) == 0 then //pull out of combat units only
            call ModifyThreat(pu, npc, 0, true)
        endif
    endif
    set pu = null
    set npc = null
endfunction

private function FilterUnitsWithCampGroup takes nothing returns boolean
    return HaveSavedHandle(NPClist, GetHandleId(GetFilterUnit()), 4) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_DEAD)
endfunction

public function AddThreatUnit takes unit u returns nothing
    local integer ID = GetHandleId(u)
    local group g = null
    local trigger t = null
    if HaveSavedInteger(NPClist, ID, 0) or HaveSavedHandle(PUlist, ID, 0) then //unit already added
        return
    elseif GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD) then //unit dead or null
        return
    endif
    call SaveInteger(NPClist, ID, 0, 0) //status
    call SaveReal(NPClist, ID, 1, GetUnitX(u)) //return X
    call SaveReal(NPClist, ID, 2, GetUnitY(u)) //return Y
    call SaveReal(NPClist, ID, 3, TimeToPort) //return countdown
    call SaveInteger(NPClist, ID, 5, 0) //list length
    set t = CreateTrigger()
    call TriggerRegisterUnitEvent(t, u, EVENT_UNIT_ISSUED_TARGET_ORDER)
    call TriggerRegisterUnitEvent(t, u, EVENT_UNIT_ACQUIRED_TARGET)
    call TriggerAddAction(t, function AcquireTarget)
    call SaveTriggerHandle(NPClist, ID, 6, t) //acquire target event trigger
    call GroupEnumUnitsInRange(TGroupSub, GetUnitX(u), GetUnitY(u), HelpRange, Condition(function FilterUnitsWithCampGroup))
    if FirstOfGroup(TGroupSub) != null then
        set g = LoadGroupHandle(NPClist, GetHandleId(FirstOfGroup(TGroupSub)), 4)
    else //no unit in range has a camp group assigned, so create a new one
        set g = CreateGroup()
    endif
    call GroupAddUnit(g, u)
    call SaveGroupHandle(NPClist, ID, 4, g) //camp group
    set t = null
    set g = null
endfunction

public function RemoveThreatUnit takes unit u returns nothing
    local integer ID = GetHandleId(u)
    local integer OtherID
    local group g = null
    local integer key = 10
    if not HaveSavedInteger(NPClist, ID, 0) then //unit not added
        return
    elseif GetUnitTypeId(u) == 0 then
        return
    endif
    if LoadInteger(NPClist, ID, 0) > 1 then //unit status is: returning
        call IssueImmediateOrder(u, "stop")
        call SetUnitInvulnerable(u, false)
        if IsUnitPaused(u) then
            call PauseUnit(u, false)
        endif
    endif
    
    loop //remove the entry in the player unit's position list and list group and decrease list group count
        if HaveSavedHandle(NPClist, ID, key) then
            set OtherID = GetHandleId(LoadUnitHandle(NPClist, ID, key))
            call RemoveSavedInteger(PUlist, OtherID, ID)
            call GroupRemoveUnit(LoadGroupHandle(PUlist, OtherID, 0), u)
            call SaveInteger(PUlist, OtherID, 1, LoadInteger(PUlist, OtherID, 1)-1)
            set key = key+2
        else //last entry reached
            exitwhen true
        endif
    endloop
    
    set g = LoadGroupHandle(NPClist, ID, 4)
    call GroupRemoveUnit(g, u)
    if FirstOfGroup(g) == null then //camp group is empty
        call DestroyGroup(g)
    endif
    call DestroyTrigger(LoadTriggerHandle(NPClist, ID, 6))
    call FlushChildHashtable(NPClist, ID)
    if IsUnitInGroup(u, NPCgroup) then
        call GroupRemoveUnit(NPCgroup, u) //remove unit from incombat group
    endif
    set g = null
endfunction

private function RemovePlayerUnitEntries takes nothing returns nothing
    local integer ID = GetHandleId(TSub)
    local integer OtherID = GetHandleId(GetEnumUnit())
    local integer key = Pos2Key(LoadInteger(PUlist, ID, OtherID))
    loop //remove the entry in u's threat list and fill the gap
        if HaveSavedHandle(NPClist, OtherID, key+2) then //move up next entry
            call SaveUnitHandle(NPClist, OtherID, key, LoadUnitHandle(NPClist, OtherID, key+2))
            call SaveReal(NPClist, OtherID, key+1, LoadReal(NPClist, OtherID, key+3))
            call SaveInteger(PUlist, GetHandleId(LoadUnitHandle(NPClist, OtherID, key)), OtherID, Key2Pos(key)) //update position in player unit list
            set key = key+2
        else //last entry reached
            call RemoveSavedHandle(NPClist, OtherID, key)
            call RemoveSavedReal(NPClist, OtherID, key+1)
            call SaveInteger(NPClist, OtherID, 5, Key2Pos(key-2)) //decrease list length
            exitwhen true
        endif
    endloop
endfunction

public function RemovePlayerUnit takes unit u returns nothing
    local integer ID = GetHandleId(u)
    if not HaveSavedHandle(PUlist, ID, 0) then //unit not added
        return
    elseif GetUnitTypeId(u) == 0 then
        return
    endif
    set TSub = u
    call ForGroup(LoadGroupHandle(PUlist, ID, 0), function RemovePlayerUnitEntries)
    
    call DestroyGroup(LoadGroupHandle(PUlist, ID, 0))
    call FlushChildHashtable(PUlist, ID)
endfunction

private function HealThreatSub takes nothing returns nothing
    call ModifyThreat(THealer, GetEnumUnit(), THealthreat, TBool)
endfunction

public function ApplyHealThreat takes unit pu, unit ally, real amount, boolean add, boolean divide returns nothing
    local integer puID = GetHandleId(pu)
    local integer allyID = GetHandleId(ally)
    if not (HaveSavedHandle(PUlist, puID, 0) and HaveSavedHandle(PUlist, allyID, 0)) then //units not added
        return
    elseif IsUnitType(pu, UNIT_TYPE_DEAD) or IsUnitType(ally, UNIT_TYPE_DEAD) then //units dead
        return
    elseif GetUnitTypeId(pu) == 0 or GetUnitTypeId(ally) == 0 then //null units
        return
    endif
    if divide and LoadInteger(PUlist, allyID, 1) > 1 then
        set THealthreat = amount/LoadInteger(PUlist, allyID, 1)
    else
        set THealthreat = amount
    endif
    set TBool = add
    set THealer = pu
    call ForGroup(LoadGroupHandle(PUlist, allyID, 0), function HealThreatSub)
endfunction

private function CampCommand takes nothing returns nothing
    local unit u = GetEnumUnit()
    local integer ID = GetHandleId(u)
    local integer OtherID
    local integer status = LoadInteger(NPClist, ID, 0)
    local integer key = 10
    if status == 1 then
        call SaveInteger(NPClist, GetHandleId(u), 0, 2) //set status: returning
        loop //remove the entry in the player unit's position list and list group and decrease list group count
            if HaveSavedHandle(NPClist, ID, key) then
                set OtherID = GetHandleId(LoadUnitHandle(NPClist, ID, key))
                call RemoveSavedInteger(PUlist, OtherID, ID)
                call GroupRemoveUnit(LoadGroupHandle(PUlist, OtherID, 0), u)
                call SaveInteger(PUlist, OtherID, 1, LoadInteger(PUlist, OtherID, 1)-1)
                call RemoveSavedHandle(NPClist, ID, key)
                call RemoveSavedReal(NPClist, ID, key+1)
                set key = key+2
            else //last entry reached
                exitwhen true
            endif
        endloop
        call SaveInteger(NPClist, ID, 5, 0) //also set list length to zero
        call IssueImmediateOrder(u, "stop") //cancels even spellcast with casting time
        call IssuePointOrder(u, "move", LoadReal(NPClist, ID, 1), LoadReal(NPClist, ID, 2))
        call SaveReal(NPClist, ID, 3, TimeToPort)
        call SetUnitInvulnerable(u, true)
        if HealUnitsOnReturn then
            call SetUnitState(u, UNIT_STATE_LIFE, GetUnitState(u, UNIT_STATE_MAX_LIFE))
            call SetUnitState(u, UNIT_STATE_MANA, GetUnitState(u, UNIT_STATE_MAX_MANA))
        endif
    elseif status == 3 then
        call SaveInteger(NPClist, GetHandleId(u), 0, 0) //set status: out of combat
        call SetUnitInvulnerable(u, false)
        call PauseUnit(u, false)
        if HealUnitsOnReturn then
            call SetUnitState(u, UNIT_STATE_LIFE, GetUnitState(u, UNIT_STATE_MAX_LIFE))
            call SetUnitState(u, UNIT_STATE_MANA, GetUnitState(u, UNIT_STATE_MAX_MANA))
        endif
        call GroupRemoveUnit(NPCgroup, u) //remove from combat group
    endif
    set u = null
endfunction

private function CampStatus takes nothing returns nothing
    if LoadInteger(NPClist, GetHandleId(GetEnumUnit()), 0) != 3 then
        set TBool = false
    endif
endfunction

private function IssueOrder takes nothing returns nothing
    local unit npc = GetEnumUnit()
    local integer npcID = GetHandleId(npc)
    local integer status = LoadInteger(NPClist, npcID, 0)
    local integer i = 0
    local boolean b = true
    local unit target = null
    if status == 1 then //unit in combat
        if IsUnitInRangeXY(npc, LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2), ReturnRange) and HaveSavedHandle(NPClist, npcID, 10) then
            set target = LoadUnitHandle(NPClist, npcID, 10)
            if IsUnitInRangeXY(target, LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2), OrderReturnRange) then
                if GetUnitCurrentOrder(npc) == 851983 or GetUnitCurrentOrder(npc) == 0 or GetUnitCurrentOrder(npc) == 851971 then //attack order or no order or smart order
                    set EventBool = true
                    call IssueTargetOrder(npc, "smart", target)
                    set EventBool = false
                endif
            else //target of unit to far away from camp position
                call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampCommand) //set camp returning
            endif
        else //unit left return range or killed all player units
            call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampCommand) //set camp returning
        endif
    elseif status == 2 then //unit is returning
        if LoadReal(NPClist, npcID, 3) > 0 then
            if not IsUnitInRangeXY(npc, LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2), 35) then
                call IssuePointOrder(npc, "move", LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2))
                call SaveReal(NPClist, npcID, 3, LoadReal(NPClist, npcID, 3) - UpdateIntervall)
                call SetUnitInvulnerable(npc, true)
            else //unit within close range to camp position
                if GetUnitCurrentOrder(npc) == 851986 then //move order
                    call SaveReal(NPClist, npcID, 3, LoadReal(NPClist, npcID, 3) - UpdateIntervall)
                    call SetUnitInvulnerable(npc, true)
                else //Something blocks the exact spot or the unit has arrived
                    call SaveInteger(NPClist, npcID, 0, 3) //set status: returned
                    set TBool = true
                    call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampStatus)
                    if TBool then //all units in camp have status: returned to camp position
                        call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampCommand) //set camp ooc
                    else
                        call PauseUnit(npc, true) //make sure it doesn't move or attack when invulnerable
                    endif
                endif
            endif
        else //counter expired - perform instant teleport
            call SetUnitPosition(npc, LoadReal(NPClist, npcID, 1), LoadReal(NPClist, npcID, 2))
            call SaveInteger(NPClist, npcID, 0, 3) //set status: returned
            set TBool = true
            call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampStatus)
            if TBool then //all units in camp have status: returned to camp position
                call ForGroup(LoadGroupHandle(NPClist, npcID, 4), function CampCommand) //set camp ooc
            else
                call PauseUnit(npc, true) //make sure it doesn't move or attack when invulnerable
            endif
        endif
    endif
    set npc = null
    set target = null
endfunction

private function Update takes nothing returns nothing
    call ForGroup(NPCgroup, function IssueOrder) //issues orders to all units in combat
endfunction

private function rettrue takes nothing returns boolean
    return true
endfunction

private function RemovedUnitFound takes unit u returns nothing
    if HaveSavedInteger(NPClist, GetHandleId(u), 0) then
        call RemoveThreatUnit(u)
    endif
    if HaveSavedHandle(PUlist, GetHandleId(u), 0) then
        call RemovePlayerUnit(u)
    endif
endfunction

hook RemoveUnit RemovedUnitFound

private function OnDeath takes nothing returns nothing
    if HaveSavedInteger(NPClist, GetHandleId(GetTriggerUnit()), 0) then
        call RemoveThreatUnit(GetTriggerUnit())
    endif
    if HaveSavedHandle(PUlist, GetHandleId(GetTriggerUnit()), 0) then
        call RemovePlayerUnit(GetTriggerUnit())
    endif
endfunction

private function InitThreatSystem takes nothing returns nothing
    local trigger t = CreateTrigger()
    local integer index = 0
    set BOOLEXPR=Condition(function rettrue) //prevent booleanexpressions from leaking
    call TimerStart(Updater, UpdateIntervall, true, function Update)
    loop
        call TriggerRegisterPlayerUnitEvent(t, Player(index), EVENT_PLAYER_UNIT_DEATH, BOOLEXPR)
        set index = index + 1
        exitwhen index == bj_MAX_PLAYER_SLOTS
    endloop
    call TriggerAddAction(t, function OnDeath)
    set t = null
endfunction

endlibrary
 

Attachments

  • ZTSThreatv2_2b.w3x
    59.6 KB · Views: 88
What is the point in this whole library as one? Use a damage detection system like ADamage, don't code it yourself. It'd make the library much shorter, and when used in conjunction with other systems that use damage detection, it's much slower if all of them have it hardcoded. Otherwise, I don't have time to check all that code for little errors, but the testmap works right.
 
What is the point in this whole library as one? Use a damage detection system like ADamage, don't code it yourself. It'd make the library much shorter, and when used in conjunction with other systems that use damage detection, it's much slower if all of them have it hardcoded. Otherwise, I don't have time to check all that code for little errors, but the testmap works right.
There is no damage detection in this system o_O ... what the hell are you talking about? Did you even understand what this system does?
 
Yes, this does seem useful but do you really think this belongs here?
Since it's more of a system than a straight script, I think you should submit it in the spells section.
I'm a mod there too so I can personally review it.

Anyways, I don't like public functions, they break the Jass naming convention and just look ugly. I'm sure you can find something more suitable for the function names (or just remove the public keyword). But after all, that is a preference thing, though I do see more people having the same preference as me.

And another thing that bugs me is how people constantly inline events. The only place I see this is hive and it baffles me since you are gaining basically nothing from doing it and loses readability at the same time.

The boolexpr leak prevention is not needed, afaik they are stored in a table and are re-used. Therefor they never leak. And the new patch fixed them leaking in GroupEnums too.

RemoveUnit is not the only way units get removed from the map, so it is prone to flaws and you're going to need to find a way around that. And hooking it is always slowing down the RemoveUnit function significantly.

That's all for now, please consider porting this to the spells section.
 
There is no damage detection in this system o_O ... what the hell are you talking about? Did you even understand what this system does?

Sorry, i assumed it used the damage dealt by units to calculate how much aggro they have. As I said, I didn't read through the code, I just assumed it would use damage detection. No need to get aggressive.
 
Yes, this does seem useful but do you really think this belongs here?
Since it's more of a system than a straight script, I think you should submit it in the spells section.
I'm a mod there too so I can personally review it.

Anyways, I don't like public functions, they break the Jass naming convention and just look ugly. I'm sure you can find something more suitable for the function names (or just remove the public keyword). But after all, that is a preference thing, though I do see more people having the same preference as me.

And another thing that bugs me is how people constantly inline events. The only place I see this is hive and it baffles me since you are gaining basically nothing from doing it and loses readability at the same time.

The boolexpr leak prevention is not needed, afaik they are stored in a table and are re-used. Therefor they never leak. And the new patch fixed them leaking in GroupEnums too.

RemoveUnit is not the only way units get removed from the map, so it is prone to flaws and you're going to need to find a way around that. And hooking it is always slowing down the RemoveUnit function significantly.

That's all for now, please consider porting this to the spells section.
Why should it be moved into the spells section? Its not a spell, its a system, clearly and obvious.

Yes I know there are things like transport events, but, seriously, if people want to use such thing, its no problem to just desetup the unit that is load into transport, etc.
The remove unit hook is just there to make your life easier. There is no need to rely on that.

About the boolexpr thing: Does it really matter? Its not like a simple function that returns true is a problem and I feel better knowing that it creates no leak by 100%. I do not know exactly what is new about boolexprs, but I experienced enough to not trust blizzard blindly.
 
Why should it be moved into the spells section? Its not a spell, its a system, clearly and obvious.

The spells section also supports systems.
Furthermore, you will probably get more attention there.

Yes I know there are things like transport events, but, seriously, if people want to use such thing, its no problem to just desetup the unit that is load into transport, etc.
The remove unit hook is just there to make your life easier. There is no need to rely on that.

Then use a static if to make it configurable. I wouldn't use this with that hook.
Remember, hooks can be inside functions.

About the boolexpr thing: Does it really matter? Its not like a simple function that returns true is a problem and I feel better knowing that it creates no leak by 100%. I do not know exactly what is new about boolexprs, but I experienced enough to not trust blizzard blindly.

Fair enough.
 
Hmm ... about the hook, it's not hard for people to simply remove it, even with no understanding of JASS ... it's just that a lot of spells remove dummy units, etc., but almost nobody uses those rare events that also remove units (and besides, I'm not even sure if transports are actually "removing" the unit ... the handle ID remains and so it is absolutely no problem for the unit to stay registered to the system. the unit will get freed some time, after all and will then still be considered registered and treated properly).

If you are using a unit indexing system, you could also replace the hook to the matching OnDeindex event.

The system is not really suited for beginners, so I think it's okay to require at least some minor knowlegde about JASS for full control.
If you use GUI, it will still work fine, you just need special caution in some (extremely rare) occasions.


PS: You can move it to the spells section, if you think that's better.
 
Top